Compare commits

...

199 Commits
1.3.4 ... 1.3.9

Author SHA1 Message Date
Kleofass
3dbe27ea57 Update lv.rs (#11298) 2025-03-31 13:46:38 +08:00
Mahdi Rahimi
a2725df7cd Update Arabic translation in ar.rs (#11281) 2025-03-31 13:46:25 +08:00
Mahdi Rahimi
f32988b454 Updated Persian translations in fa.rs (#11280) 2025-03-31 13:46:15 +08:00
bovirus
adf83a1b25 Italian language update (#11278) 2025-03-31 13:45:59 +08:00
Mr-Update
ea74ed12b8 Update de.rs (#11269) 2025-03-30 14:45:17 +08:00
Lynilia
5f3b980373 Update fr.rs (#11266) 2025-03-29 21:55:28 +08:00
fufesou
23e70c0fd1 refact: remote printer, adapter dll, free data ptr (#11279)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-28 21:58:46 +08:00
fufesou
4b14f86134 refact: remote printer, log (#11271)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-28 10:56:19 +08:00
fufesou
ee2478168c fix: remote printer (#11270)
* fix: remote printer, log

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

* fix: remote printer, avoid double sign

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

* Spawn a new thread to handle the print job.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-28 10:36:42 +08:00
fufesou
f4bbf82363 feat: remote printer (#11231)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-27 15:34:27 +08:00
Kleofass
1cb53c1f7a Update lv.rs (#11250) 2025-03-27 14:42:31 +08:00
RustDesk
eea9e0fa43 Update README-DE.md (#11247) 2025-03-26 15:23:40 +08:00
RustDesk
ac630c2ca6 Update README-ZH.md 2025-03-25 15:41:36 +08:00
RustDesk
9831f93430 Update README.md 2025-03-24 21:59:08 +08:00
Lynilia
c074a1d6af Update fr.rs (#11184) 2025-03-20 15:12:03 +08:00
asereze
47c93f8544 Update sc.rs (#11176)
* Update sc.rs

* Update sc.rs
2025-03-19 22:26:14 +08:00
rustdesk
c06ac9341a improve check_software_update 2025-03-18 15:12:41 +08:00
Alex Rijckaert
8d231b4605 Update nl.rs (#11172) 2025-03-18 14:48:48 +08:00
Alex Rijckaert
745ba1673d Update nl.rs (#11155) 2025-03-17 22:04:18 +08:00
solokot
2ef1dd99de Update ru.rs (#11132) 2025-03-16 15:11:18 +08:00
XLion
960d9a042f Update tw.rs (#11126) 2025-03-15 21:37:39 +08:00
bovirus
10457dfe45 Italian language update (#11123) 2025-03-14 17:14:05 +08:00
21pages
971d4e6976 ipc example for test (#11127)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-03-14 00:21:05 +08:00
Mr-Update
2dbff45588 Update de.rs (#11106)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-03-13 09:36:27 +08:00
flusheDData
2bdb621417 Update es.rs (#11111)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-03-13 09:36:00 +08:00
Alex Rijckaert
47b00054d2 Update nl.rs (#11102)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-03-13 09:35:21 +08:00
21pages
d1c8b331c5 Option allow-d3d-render and fix ios ci (#11107)
* option `allow-d3d-render`, default false

Add this option because it fails on some machines

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

* only add nokhwa to windows and linux dependencies

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-03-13 09:34:13 +08:00
fufesou
1403c939db fix: msi, silent install, do not launch app (#11115)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-13 09:33:30 +08:00
rustdesk
f1d2073d43 revert back, because useless 2025-03-12 20:06:35 +08:00
rustdesk
f7c930e153 fix https://github.com/rustdesk/rustdesk/discussions/11104 2025-03-12 19:07:25 +08:00
bovirus
22005bac75 Italian language update (#11088) 2025-03-12 15:14:47 +08:00
XLion
8f7bb5a032 Update tw.rs (#11087) 2025-03-12 15:14:21 +08:00
21pages
e0fd698101 opt dropdown button of connection page (#11086)
* Use menu style of the peer card
* Add margin between connection button and dropdown button
* Use thinner icon

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-03-11 16:29:02 +08:00
21pages
b2cc9eac23 set rgba_data.valid to false when open a new single display on the old session (#11078)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-03-10 21:12:13 +08:00
Naveenkumar
5f521c80a7 Create ta.rs (#11068)
* Create ta.rs

* Update lang.rs
2025-03-10 21:07:15 +08:00
21pages
f0f999dc27 view camera (#11040)
* view camera

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

* `No cameras` prompt if no cameras available,  `peerGetSessionsCount` use
connType as parameter

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

* fix, use video_service_name rather than display_idx as key in qos,etc

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Adwin White <adwinw01@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-03-10 21:06:53 +08:00
fufesou
df4a101316 fix: build macos, default feature (#11075)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-10 10:16:17 +08:00
rustdesk
bdc53f0190 improve lock, https://github.com/rustdesk/rustdesk/issues/11067 2025-03-10 11:10:09 +09:00
Madis Otenurm
cef4175961 Update et.rs (#11054)
* Update et.rs

* Update et.rs

Added more missing translations

* Update et.rs

* Update et.rs

* Update et.rs
2025-03-08 17:38:58 +08:00
asereze
4ff75412c3 Update sc.rs (#11046) 2025-03-07 21:24:45 +08:00
dependabot[bot]
f1329ca69e Git submodule: Bump libs/hbb_common from 7cf11f7 to 83419b6 (#11042)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `7cf11f7` to `83419b6`.
- [Commits](7cf11f7b77...83419b6549)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 09:21:40 +08:00
ANB5Dev
c95aaf563e fixed Dutch names for macOS permissions (#11020) 2025-03-06 09:49:44 +08:00
Mani Ka
11fed81c4d Update reference to flutter web UI (#11029)
- the src js web UI has moved to directory v1 in flutter/web/js as v2 is under development . The link in README.md is returning 404
2025-03-06 09:49:25 +08:00
zuiyu
561bc18f49 Fix unused import with dxgiformat (#11032) 2025-03-06 09:49:04 +08:00
Ivan Beà
6946b863f7 Update ca.rs (#11024)
Update catalan localization
2025-03-05 16:14:42 +08:00
Yuni
8f68861920 Update CONTRIBUTING-KR.md (#11016)
* Update CONTRIBUTING-KR.md

Signed-off-by: yeonhee7935 <yeonhee7935@naver.com>

* Update CONTRIBUTING-KR.md

Signed-off-by: yeonhee7935 <yeonhee7935@naver.com>

---------

Signed-off-by: yeonhee7935 <yeonhee7935@naver.com>
2025-03-04 17:37:54 +08:00
21pages
171d178b09 make errorText of DialogTextField selectable (#11013)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-03-04 14:11:18 +08:00
rustdesk
7305b6bd1c https://github.com/rustdesk/rustdesk/discussions/937#discussioncomment-12373814 try to support citrix session 2025-03-03 19:06:01 +08:00
marboroman
6600c8c648 Update ru.rs (#10984)
Changed to the translation which makes sense.
2025-03-03 14:55:32 +08:00
marboroman
32b77f8968 Update ru.rs (#10979)
Made long translation short to fit in user interface.
2025-03-03 14:55:21 +08:00
marboroman
0bda90f8fb Update ru.rs (#10978)
update wrong translation for privacy mode activation
2025-03-03 14:55:11 +08:00
Lynilia
2b68c46fdc French localization rework (#10966) 2025-03-02 18:04:22 +08:00
fufesou
bfbf00f18c fix: custom client, settings button (#10974)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-01 19:43:01 +08:00
asereze
e17ab74040 Sardinian translation (#10941)
* Sardinian translation

Sardinian ("Sardu", ISO 639-1 code: "sc") translation.

* Update lang.rs

* Corrected typo
2025-03-01 18:39:58 +08:00
fufesou
41cd375e3c fix: potential memleak (#10955)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-28 12:14:40 +08:00
fufesou
0d919157c9 Fix/win build (#10954)
* fix: win build

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

* fix: win, build

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-28 11:56:17 +08:00
fufesou
00293a9902 Feat/macos clipboard file (#10939)
* feat: macos, clipboard file

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

* Can't reuse file transfer

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

* handle paste task

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-28 00:46:46 +08:00
rustdesk
bc3a58f6f4 1.3.9 2025-02-26 18:00:31 +08:00
rustdesk
d8496aba0b refactor is_custom_client 2025-02-26 00:32:54 +08:00
rustdesk
280c12942f improve android build 2025-02-25 00:30:29 +08:00
Andrzej Rudnik
3a5b30a5e7 Update pl.rs (#10901) 2025-02-24 23:08:48 +08:00
dependabot[bot]
c46023bbde Git submodule: Bump libs/hbb_common from 16900b9 to 7cf11f7 (#10895)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `16900b9` to `7cf11f7`.
- [Commits](16900b9b06...7cf11f7b77)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:58:25 +08:00
Kleofass
93feedc212 Update lv.rs (#10883) 2025-02-23 13:39:43 +08:00
luzpaz
6f1a769741 fix: source typo in flutter/lib/common/widgets/address_book.dart (#10884)
Found via codespell
2025-02-23 09:01:52 +08:00
princeyogesh
e191d11f74 before docker run command added git submoulde update command in docker build Updated README.md (#10878)
there is dependency on submodule libs/hbb_common, 
we need to initialize submodule after cloning repo and before running container
2025-02-22 13:17:11 +08:00
fufesou
fc396d2166 fix: check text editing controlling, if selection is valid (#10868)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-21 11:00:19 +08:00
Theofanis Sarmidis
2575e14811 Update el.rs (#10866) 2025-02-21 10:42:12 +08:00
fufesou
0b9a6a280e fix: remote id, update text and reserve selection (#10867)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-21 10:41:57 +08:00
fufesou
343f12b380 fix: load local peers, called two times on select tab (#10859)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-20 22:47:42 +08:00
fufesou
ce1e4863cb fix: load peers, always push event data (#10856)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-20 22:44:18 +08:00
fufesou
f631c1c28d refact: Remote ID editor, only select text on focus (#10854)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-20 19:35:04 +08:00
fufesou
8b9a7a3506 refact: optimize, ID search peers (#10853)
* refact: optimize, preload peers

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

* Update dialogs.dart

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-02-20 18:31:12 +08:00
fufesou
055b351164 refact: optimize, loading recent peers (#10847)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-20 11:53:36 +08:00
fufesou
2e89a33210 fix: android, back function (#10843)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-20 01:02:24 +08:00
Alex Rijckaert
965cc6af26 Update nl.rs (#10834) 2025-02-20 00:25:23 +08:00
rustdesk
16e191f913 fix de 2025-02-20 00:24:24 +08:00
fufesou
1d1e79c802 revert, peers card, sort by online status (#10829)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-19 14:14:02 +08:00
Mr-Update
9ffe516f54 Update de.rs (#10827) 2025-02-19 08:42:15 +08:00
solokot
ac20d2fb56 Update ru.rs (#10794) 2025-02-19 01:11:36 +08:00
bovirus
2217152216 Italian language update (#10796) 2025-02-19 01:11:22 +08:00
XLion
ee288280b3 Update tw.rs (#10799) 2025-02-19 01:11:08 +08:00
John Fowler
cccdb2f289 Update hu.rs (#10804)
Translate new strings.
Clarification of some translations.
2025-02-19 01:10:52 +08:00
ANB5Dev
1ddab27c0e NL lang further improvements (#10813) 2025-02-19 01:10:41 +08:00
Alex Rijckaert
451b6dc651 Update nl.rs (#10812) 2025-02-19 01:10:32 +08:00
rustdesk
86b327ee41 they always forget to remove :21114 for https, so I remove for them 2025-02-18 16:18:41 +08:00
rustdesk
6e305d4865 improve sysinfo update 2025-02-18 16:09:25 +08:00
21pages
77af6c4ce1 clear selected device group or user when search text changes (#10815)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-02-18 09:08:38 +08:00
21pages
fa49c72835 fix, accessible peers filter considering device group name (#10809)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-02-17 17:36:47 +08:00
rustdesk
c150143d86 device_group_name in devices.py 2025-02-17 16:28:47 +08:00
fufesou
023d46b48c refact: android, handle right click (#10806)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-16 20:45:09 +08:00
rustdesk
356adbcd8c more about allow - 2025-02-15 19:00:43 +08:00
rustdesk
33b47dd6e3 allow dash in id 2025-02-15 18:51:30 +08:00
fufesou
a548e9c94d fix: android, controlled side, gesture (#10792)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-15 18:33:26 +08:00
21pages
cefda0dec1 device group (#10781)
1. Rename `Group` tab to `Accessible devices`
2. Add accessible device groups at the top of search list
3. option `preset-device-group-name` and command line `--assign --device_group_name`

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-02-15 12:13:11 +08:00
rustdesk
8f545491a2 verify_login, but not eable yet 2025-02-14 16:39:09 +08:00
rustdesk
263bbfc66f missed clear 2025-02-12 17:04:56 +08:00
fufesou
a039741e5a fix: win10, border (#10753)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2025-02-10 13:50:28 +08:00
fufesou
2a0e8c109b fix: macos, main window, dark theme, border (#10749)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-02-10 00:25:11 +08:00
rustdesk
9614bf266a update READEME 2025-02-08 16:03:04 +08:00
rustdesk
aa63ebc7e5 Misuse Disclaimer 2025-02-05 15:28:30 +08:00
fufesou
fbba8f0b34 refact: file copy&paste, cross platform (no macOS) (#10671)
* feat: unix, file copy&paste

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

* refact: unix file c&p, check peer version

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

* Update pubspec.yaml

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-02-04 20:33:02 +08:00
ANB5Dev
a27fa43081 Update NL translation: spelling, capitalization, missing entries (#10661) 2025-02-03 22:18:20 +08:00
rustdesk
7a5941de98 remove devcontainer since not maintained yet 2025-02-02 20:31:44 +08:00
rustdesk
db3ca6a373 remove useless code 2025-02-01 13:07:27 +08:00
rustdesk
ce5f0d513f 2024 -> 2025 2025-01-31 16:54:57 +08:00
rustdesk
05b0f95b79 restore entrypoint.sh 2025-01-30 13:53:02 +08:00
rustdesk
8b24b195a2 remove useless files 2025-01-30 13:49:37 +08:00
rustdesk
5fc8e8c428 remove PUBLIC_RS_PUB_KE 2025-01-29 16:57:28 +08:00
RustDesk
25f917a7b4 misused by bad guys (#10614) 2025-01-28 16:16:00 +08:00
fufesou
55005f8129 fix: win, file clipboard, try empty (#10609)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-01-27 16:16:44 +08:00
fufesou
f08cb0412d fix: windows, dll, pre-loading attack (#10608)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-01-26 19:39:38 +08:00
XLion
fc2e27bcf0 Create dependabot.yml (#10593) 2025-01-26 14:18:26 +08:00
Theofanis Sarmidis
7aa4592669 Update and fixes el.rs (#10600) 2025-01-25 16:39:16 +08:00
Y-Ploni
d656ae2956 Update he.rs (#10594) 2025-01-24 15:09:36 +08:00
RustDesk
e4f00361f6 Update README.md (#10587) 2025-01-23 13:24:14 +08:00
RustDesk
1b49d49df2 Update README.md (#10586) 2025-01-23 13:23:20 +08:00
bjoernp
80f759c1ed norwegian translation (#10579)
Signed-off-by: bjoernp116 <bjoernpollen@gmail.com>
2025-01-23 13:22:25 +08:00
21pages
da80f3352a fix vaapi create 2 times at first (#10576)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-22 20:27:00 +08:00
Vasyl Gello
ec3ba5be8e Fix issues spotted during 1.3.7 fdroid build (#10570)
* bridge.yml: Explicitly install cargo-expand of certain version

@linsui spotted this trying to fix the build failure of 1.3.7
on f-droid:

https://gitlab.com/fdroid/fdroiddata/-/merge_requests/18766

* flutter-build.yml: drop workarounds for flutter 3.13

@fufesou has removed them from build_fdroid.sh in #10040 but
forgot to remove them in main flutter_build.yml. flutter 3.13
is not used anymore, and those who want to build the old version
using flutter 3.13 can happily check out the appropriate commit
from Git history.

* Bump vcpkg baseline to 2025.01.13

@linsui addressed the missing vcpkg-tools.json file inside vcpkg
revision (microsoft side, not rustdesk's!) by updating the vcpkg
baseline.
2025-01-22 09:26:03 +08:00
21pages
d04756ad70 replace self-hosted arm64 linux with ubuntu-22.04-arm (#10555)
https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-21 17:09:24 +08:00
21pages
0eba939cd6 fix windows crash (#10562)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-21 16:57:07 +08:00
21pages
1f02bc9d3e bump to 1.3.7 (#10548)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-20 23:12:00 +08:00
21pages
5fa8c25e65 opt qos (#10459)
* Adjust bitrate and fps based on TestDelay messages.
* Bitrate is adjusted every 3 seconds, fps is adjusted every second and when receiving test lag.
* Latency optimized at high resolutions. However, when the network is poor, the delay when just connecting or sliding static pages is still obvious.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-20 17:59:36 +08:00
21pages
c44803f5b0 replace hbb_common with submodule (#10543)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-20 17:33:41 +08:00
Samuel FORESTIER
4b066b1fba fix(debian): makes postinst/prerm scripts idempotent (#10541)
* fix(debian): makes `postinst` script idempotent

* fix(debian): makes `prerm` script idempotent
2025-01-20 07:59:40 +08:00
fufesou
dd004f1a2d fix: clipboard, client side, update is required on conn (#10464)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-01-17 02:27:20 +08:00
fufesou
222dbf12cd fix: mobile, don't reset canvas on metrics changed (#10463)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-01-15 18:24:50 +08:00
是果宝呐
b5d54debce Fix a translation error (#10500) 2025-01-14 22:16:17 +08:00
flusheDData
08cdf7134d Update es.rs (#10468) 2025-01-10 21:14:59 +08:00
add-uos
be5037bd03 fix: [translations] Add the translation in tw.rs (#10452)
Add the translation in tw.rs

Log: Add the translation in tw.rs
2025-01-08 14:16:16 +08:00
rustdesk
f9915df926 update readme 2025-01-08 00:23:17 +08:00
rustdesk
f96c759cf5 fix https://github.com/rustdesk/rustdesk/issues/10440 2025-01-07 11:52:43 +08:00
Xiaobo Liu
8f329ebc1a scrap: style (#10445) 2025-01-07 11:21:43 +08:00
Xiaobo Liu
4a3c11e711 scrap: fixed build warnning (#10442)
```shell
warning: elided lifetime has a name
   --> src/common/mod.rs:192:21
    |
187 |     pub fn to<'a>(
    |               -- lifetime `'a` declared here
...
192 |     ) -> ResultType<EncodeInput> {
    |                     ^^^^^^^^^^^ this elided lifetime gets resolved as `'a`
    |
    = note: `#[warn(elided_named_lifetimes)]` on by default
```
2025-01-07 11:14:20 +08:00
Xiaobo Liu
0dbd3094ec hbb_common: simplify is_compressed_file (#10436)
* hbb_common: simplify is_compressed_file

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>

* `exts` rename to `compressed_exts`

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-01-06 18:20:18 +08:00
21pages
40999c3211 fix ffmpeg videotoolbox wrong log (#10413)
* Fix ffmpeg videotoolbox wrong log when changing bitrate
* Let qsv support abr, and it's safe for qsv to changing bitrate.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-01-02 22:19:30 +08:00
rustdesk
7c2d62237f missed file 2025-01-01 23:11:38 +08:00
rustdesk
ef90ab2bd4 compelete fix https://github.com/rustdesk/rustdesk/discussions/10210
rather than the awful workaround
2025-01-01 23:05:52 +08:00
Dimitris Apostolou
4f3b821883 fix: fix crate vulnerabilities (#10407) 2025-01-01 10:15:57 +08:00
21pages
98b00cdb3d Fix image blur occurring at the moment of changing quality (#10399)
1. Fix this issue occurs on FFmepg qsv, FFmpeg nvenc and SDK mfx, other
   codecs don't have this problem. Clear cache is needed.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-30 11:51:36 +08:00
rustdesk
8e4127b6a0 remove all stupid canLaunchUrl 2024-12-29 23:43:31 +08:00
21pages
b1f54acf90 fix andriod update button cannot be clicked (#10394)
1. Remove `canLaunchUrl`, which fix the issue
2. Remove `unregisterEventHandler` of `kCheckSoftwareUpdateFinish` when
   connection page dispose, it's registered on main.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-29 23:37:52 +08:00
rustdesk
39a430f96f upgrade url_launch 2024-12-28 22:03:34 +08:00
Kleofass
a9f2e14091 Update lv.rs (#10381) 2024-12-27 14:47:01 +08:00
Vasyl Gello
77baba3122 Fix missing locked arg in cargo install (#10374)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-12-26 15:54:46 +08:00
fufesou
1c62a28ef3 fix: build (#10364)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-25 16:36:13 +08:00
fufesou
9ed2499666 fix: file clipboard, init disabled (#10361)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-25 15:18:06 +08:00
XLion
06bc554216 Fix: DEBIAN Control md5sums (#10356)
* Fix: DEBIAN Control md5 sums

* I forgot import
2024-12-25 00:04:34 +08:00
Jernej Simončič
090f5b65ac Update sl.rs (#10346) 2024-12-24 14:15:22 +08:00
Integral
49dabd3533 refactor: replace &PathBuf with &Path to enhance generality (#10332) 2024-12-23 20:28:04 +08:00
RustDesk
7289dbc80f Update flutter-build.yml (#10337) 2024-12-22 11:35:55 +08:00
rustdesk
72f5184ee0 unused 2024-12-22 11:20:38 +08:00
fufesou
e9c5e0d26b fix: android, mouse mode, right menu, unexpected click (#10330)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-21 15:09:03 +08:00
rustdesk
03999d900e 1.3.6 2024-12-21 15:00:16 +08:00
fufesou
b24551da7b refact: linux, move rustdesk into /usr/share (#10327)
* refact: linux, move rustdesk into /usr/share

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

* linux, upgrade, try remove old empty folders

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-21 14:53:28 +08:00
21pages
bc461fe99b Revert "Revert "revert linux use cpal "" (#10326)
* Revert "Revert "revert linux use cpal (#10260)" (#10262)"

This reverts commit 827b5f6a4c.

* update Cargo.lock

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-20 22:46:42 +08:00
rustdesk
25e438a663 crate 2024-12-20 22:24:53 +08:00
jkh0kr
1f5aeda41d Update ko.rs (#10320) 2024-12-20 15:09:33 +08:00
fufesou
9114743577 fix: linux, flutter, workaround freeze (#10324)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-20 09:24:08 +08:00
fufesou
7830a9e9f3 refact: linux, install path (#10316)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-19 15:05:24 +08:00
fufesou
5fa8485130 fix: macos, show remote cursor (#10314)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-18 21:17:04 +08:00
Iacopo Modica
ed9cb37283 Fix translation issues in Italian language file (#10312)
Corrected multiple translation errors and typos in the Italian language resource file.
2024-12-18 21:02:53 +08:00
21pages
e4b270a581 update hwcodec (#10306)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-17 21:52:17 +08:00
21pages
9dd9c45afc fix ci (#10305)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-17 15:01:01 +08:00
21pages
e163b75407 update hwcodec (#10304)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-17 12:07:34 +08:00
princeyogesh
10ff3e6937 Fix for compilation due to minimum Cmake version update and arm based compilation of vcpkg (#10297) 2024-12-17 10:37:57 +08:00
21pages
acae6d6558 try fix FFmpeg amf encode hang (#10283)
* Possible Causes
  * GPU API Call Hangs: This could occur, though it's less likely.
  * Infinite Loop: If `QueryOutput` always fails and `hwsurfaces_in_queue_max` is zero, the loop will continue indefinitely.
* Proposed Solution
  * A query_timeout patch has been added to FFmpeg with a value of 1000ms, which exceeds the time required to encode a single frame. This allows us to remove the loop.
* Test
  * After removing the loop, no frame encoding failures were encountered during testing. A single call to QueryOutput is sufficient, as it typically consumes about 12ms on a 2K screen.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-16 19:40:48 +08:00
fufesou
d025ca1d81 refact: linux, chcon, bin_t (#10293)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-16 19:01:12 +08:00
21pages
e5aa31eb4c Fix auto record outgoing sessions ignore record permission (#10294)
1. Fix auto record outgoing sessions ignore record permission
2. Stop record if record permission changed
3. Update hwcodec
4. Make video thread finish faster when connection closed

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-16 17:13:48 +08:00
Dmitry Beskov
771cc565ab Flathub badge in the README (#10288)
* new flathub badge in readme

* replacing badge with svg
2024-12-16 16:04:53 +08:00
summoner001
db3bdb16a1 Update hu.rs (#10287)
* Update hu.rs

Fixes and corrections

* Update hu.rs

more fixes

* Update hu.rs

Minor fixes

* Update hu.rs

Fixing typo
2024-12-16 15:48:17 +08:00
Dmitry Beskov
c06e1d74b4 changes for flatpak build (#10273) 2024-12-15 16:55:43 +08:00
21pages
b544a2889b update vcpkg to 2024.11.16 (#10272)
1. version changes:
* vcpkg: 2024.07.12 -> 2024.11.16
* aom (except linux sciter): 3.9.1 -> 3.11.0
* libvpx: 1.14.1 -> 1.15.0
* libyuv: not update because compiled failed on arm64, and didn't apply
  different version on different archs
* opus: already the latest version
* ffmpeg: 7.0.2 -> 7.1

2. other changes:
* android 5.0 required, otherwise crash when start, because FFmpeg 7.1 link to mediandk directly

3. Tests:
* Except arm, arm64, linux amf, ios, all the other codecs are tested
* Compile on arm32 linux is not tested, ci is failed before vcpkg
  install
* Tested windows FFmpeg qsv, still no memory leak

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-13 13:28:48 +08:00
Yevhen Popok
9c45636875 Update uk.rs (#10265) 2024-12-12 11:13:50 +08:00
RustDesk
827b5f6a4c Revert "revert linux use cpal (#10260)" (#10262)
This reverts commit b0791ba183.
2024-12-11 13:42:25 +08:00
21pages
b0791ba183 revert linux use cpal (#10260)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-11 13:35:50 +08:00
fufesou
b24b381575 fix: macos, keyboard, translate mode, capslock and deadkeys (#10248)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-10 13:03:00 +08:00
fufesou
0751005073 Fix/windows empty file clipboard on disconn (#10242)
* fix: windows, empty file clipboard on disconn

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

* refact: Don't send files copied before the conn

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

* refact: windows, file clipboard

Empty clipboard if no `Ctrl+C` is pressed, but
`CliprdrDataObject_GetData()` is called.
`CliprdrDataObject_GetData()` is only called in the clipboard object set
by RustDesk.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-10 11:01:34 +08:00
bovirus
fe06cf77da Italian language update (#10245) 2024-12-10 10:48:38 +08:00
Alex Rijckaert
d57cf204c8 Update nl.rs (#10243) 2024-12-10 10:48:29 +08:00
rustdesk
63e22b7685 fix build error with latest xcode 2024-12-09 18:49:02 +08:00
21pages
a02d2bb4ac fix ios audio output (#10235)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-09 15:41:49 +08:00
solokot
0f7d78c263 Update ru.rs (#10233) 2024-12-09 15:11:09 +08:00
XLion
0e321bd845 Update tw.rs (#10231) 2024-12-09 12:30:50 +08:00
Mr-Update
875b738222 Update de.rs (#10228) 2024-12-09 12:30:32 +08:00
21pages
b39e851262 fix typo (#10227)
newer version -> new version

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-08 20:09:10 +08:00
21pages
ec466d459f add version update translation (#10225)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-08 18:27:45 +08:00
21pages
d4a712bb32 always block desktop settings page if video connection exists (#10224)
1. Always block desktop settings page if video connection exists, both mouse event and key event are blocked..
2. Server control page always block key event.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-08 18:26:55 +08:00
fufesou
1c17fddf51 fix: android clipboard permission (#10223)
* fix: android clipboard permission

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

* refact: Android, clipboard, floating ball

Call rust to check if clipboard is enabled.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-07 22:34:54 +08:00
fufesou
3c838e7a92 fix: Android, try sync clipboard on connecting (#10218)
* fix: Android, try sync clipboard on connecting

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

* Android, clipboard, more clear skip check

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

* comments

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

* comment todo: Android clipboard listener, callback twice

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

* Android, clipboard, remove listner

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-07 15:12:15 +08:00
VenusGirl❤
8f44787ba3 Update README-KR.md (#10217) 2024-12-07 10:24:32 +08:00
fufesou
12e15b5a37 fix: linux, weak network, repeated keys (#10211)
Use `press` as the `click` flag on Linux to avoid repeated keys, like
the Legacy mode.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-06 20:01:11 +08:00
rustdesk
588103c6dc 1.3.5 2024-12-05 18:38:39 +08:00
fufesou
2ce9b108ed fix: linux, transparent window (#10192)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-05 17:26:34 +08:00
RustDesk
93e3107881 Revert "Revert "fix: workaround, linux window, transparent rounded corner (#1…" (#10191)
This reverts commit 468bdd6cc6.
2024-12-05 17:07:23 +08:00
RustDesk
468bdd6cc6 Revert "fix: workaround, linux window, transparent rounded corner (#10128)" (#10186)
This reverts commit 8d4c86fe7f.
2024-12-05 11:08:58 +08:00
Dmytro Zozulia
d5c5825ffd Update uk.rs (#10174) 2024-12-05 11:04:10 +08:00
RustDesk
fe4094777f Revert "fix: linux, window, workaround, mint, mate (#10146)" (#10184)
This reverts commit bd0a33e467.
2024-12-05 10:44:37 +08:00
fufesou
f13ef48cec fix: macos, aarch64, try fix running on 12.3 (#10183)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-05 10:38:22 +08:00
338 changed files with 18503 additions and 17311 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "gitsubmodule"
directory: "/"
target-branch: "master"
schedule:
interval: "daily"
commit-message:
prefix: "Git submodule"
labels:
- "dependencies"

View File

@@ -6,6 +6,7 @@ on:
workflow_call:
env:
CARGO_EXPAND_VERSION: "1.0.95"
FLUTTER_VERSION: "3.22.3"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
@@ -25,6 +26,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install prerequisites
run: |
@@ -73,6 +76,7 @@ jobs:
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd

View File

@@ -4,9 +4,9 @@ env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15
# vcpkg version: 2024.11.16
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
on:
workflow_dispatch:
@@ -45,6 +45,8 @@ jobs:
# steps:
# - name: Checkout source code
# uses: actions/checkout@v3
# with:
# submodules: recursive
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
# uses: actions-rs/toolchain@v1
@@ -92,6 +94,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install prerequisites
shell: bash

View File

@@ -31,17 +31,13 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
VERSION: "1.3.4"
# vcpkg version: 2025.01.13
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
VERSION: "1.3.9"
NDK_VERSION: "r27c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
@@ -87,6 +83,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@master
@@ -161,14 +159,44 @@ jobs:
- name: Build rustdesk
run: |
# Windows: build RustDesk
python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack
mv ./flutter/build/windows/x64/runner/Release ./rustdesk
# Download usbmmidd_v2.zip and extract it to ./rustdesk
Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip
Expand-Archive usbmmidd_v2.zip -DestinationPath .
python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack
Remove-Item -Path usbmmidd_v2\Win32 -Recurse
Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat"
mv ./flutter/build/windows/x64/runner/Release ./rustdesk
mv -Force .\usbmmidd_v2 ./rustdesk
# Download printer driver files and extract them to ./rustdesk
try {
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums
# Check and move the files
$checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value
$downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256
$checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value
$downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) {
Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file."
Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath .
mkdir ./rustdesk/drivers
mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver
Expand-Archive printer_driver_adapter.zip -DestinationPath .
mv -Force .\printer_driver_adapter.dll ./rustdesk
} elseif ($checksum_driver -ne $downloadsum_driver.Hash) {
Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file."
} else {
Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file."
}
} catch {
Write-Host "Ingore the printer driver error."
}
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
@@ -276,6 +304,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@master
@@ -404,6 +434,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@master
@@ -413,7 +445,7 @@ jobs:
- name: Build rustdesk
run: |
./build.py --flutter --hwcodec
./build.py --flutter --hwcodec --unix-file-copy-paste
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
@@ -489,6 +521,9 @@ jobs:
brew install nasm yasm
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@v2
with:
@@ -546,6 +581,12 @@ jobs:
run: |
rustup target add ${{ matrix.job.target }}
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
- name: Upload liblibrustdesk.a Artifacts
uses: actions/upload-artifact@master
with:
name: liblibrustdesk.a
path: target/aarch64-apple-ios/release/liblibrustdesk.a
- name: Build rustdesk
shell: bash
@@ -588,6 +629,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
@@ -660,6 +703,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
@@ -719,7 +764,7 @@ jobs:
shell: bash
run: |
cd "$(dirname "$(which flutter)")"
# https://github.com/flutter/flutter/issues/1.3.43
# https://github.com/flutter/flutter/issues/133533
sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
@@ -781,7 +826,7 @@ jobs:
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
fi
./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }}
./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
@@ -952,6 +997,9 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@v2
with:
@@ -1021,16 +1069,6 @@ jobs:
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
key: ${{ matrix.job.target }}
- name: fix android for flutter 3.13
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
cd flutter
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
flutter pub get
cd lib
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -1228,6 +1266,9 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@v2
with:
@@ -1270,16 +1311,6 @@ jobs:
name: librustdesk.so.i686-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86
- name: fix android for flutter 3.13
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
cd flutter
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
flutter pub get
cd lib
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk
shell: bash
env:
@@ -1373,7 +1404,7 @@ jobs:
arch: aarch64,
target: aarch64-unknown-linux-gnu,
distro: ubuntu18.04,
on: [self-hosted, Linux, ARM64],
on: ubuntu-22.04-arm,
deb_arch: arm64,
vcpkg-triplet: arm64-linux,
}
@@ -1386,16 +1417,20 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Maximize build space
if: ${{ matrix.job.arch == 'x86_64' }}
run: |
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo apt-get update -y
sudo apt-get install -y nasm qemu-user-static
sudo apt-get install -y nasm
if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then
sudo apt-get install -y qemu-user-static
fi
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set Swap Space
if: ${{ matrix.job.arch == 'x86_64' }}
@@ -1549,7 +1584,7 @@ jobs:
export JOBS=""
fi
echo $JOBS
cargo build --lib $JOBS --features hwcodec,flutter --release
cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
rm -rf target/release/deps target/release/build
rm -rf ~/.cargo
@@ -1686,7 +1721,6 @@ jobs:
build-rustdesk-linux-sciter:
if: ${{ inputs.upload-artifact }}
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
runs-on: ${{ matrix.job.on }}
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
strategy:
@@ -1702,17 +1736,17 @@ jobs:
deb_arch: amd64,
sciter_arch: x64,
vcpkg-triplet: x64-linux,
extra_features: ",hwcodec",
extra_features: ",hwcodec,unix-file-copy-paste",
}
- {
arch: armv7,
target: armv7-unknown-linux-gnueabihf,
on: [self-hosted, Linux, ARM64],
on: ubuntu-22.04-arm,
distro: ubuntu18.04-rustdesk,
deb_arch: armhf,
sciter_arch: arm32,
vcpkg-triplet: arm-linux,
extra_features: "",
extra_features: ",unix-file-copy-paste",
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -1724,6 +1758,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Free Space
run: |
@@ -1852,6 +1888,8 @@ jobs:
cat ~/.cargo/config
# install dependencies from vcpkg
export VCPKG_ROOT=/opt/artifacts/vcpkg
# remove this when support higher version
export USE_AOM_391=1
if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
@@ -1912,6 +1950,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@master
@@ -1977,13 +2017,15 @@ jobs:
target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04,
on: [self-hosted, Linux, ARM64],
on: ubuntu-22.04-arm,
arch: aarch64,
suffix: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@master
@@ -2009,36 +2051,17 @@ jobs:
shell: /bin/bash
install: |
apt-get update -y
apt-get install -y \
curl \
git \
rpm \
wget
apt-get install -y git flatpak flatpak-builder
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /workspace
# install
apt-get update -y
apt-get install -y \
cmake \
curl \
flatpak \
flatpak-builder \
gcc \
git \
g++ \
libgtk-3-dev \
nasm \
wget
# flatpak deps
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# package
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
@@ -2060,6 +2083,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Prepare env
run: |

View File

@@ -16,9 +16,9 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.3.4"
# vcpkg version: 2024.11.16
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
VERSION: "1.3.9"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -90,7 +90,8 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
@@ -250,6 +251,7 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
submodules: recursive
- name: Install dependencies
run: |

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "libs/hbb_common"]
path = libs/hbb_common
url = https://github.com/rustdesk/hbb_common

666
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.3.4"
version = "1.3.9"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
name = "naming"
path = "src/naming.rs"
[[bin]]
name = "service"
path = "src/service.rs"
[features]
inline = []
cli = []
@@ -78,12 +82,15 @@ fon = "0.6"
zip = "0.6"
shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
[target.'cfg(not(target_os = "linux"))'.dependencies]
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
ringbuf = "0.3"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" }
sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" }
@@ -109,10 +116,12 @@ winapi = { version = "0.3", features = [
"cguid",
"cfgmgr32",
"ioapiset",
"winspool",
] }
winreg = "0.11"
windows-service = "0.6"
virtual_display = { path = "libs/virtual_display" }
remote_printer = { path = "libs/remote_printer" }
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12"
tauri-winrt-notification = "0.1.2"
@@ -149,7 +158,7 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" }
async-process = "1.7"
evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9"
@@ -170,11 +179,11 @@ jni = "0.21"
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
exclude = ["vdi/host", "examples/custom_plugin"]
[package.metadata.winres]
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
ProductName = "RustDesk"
FileDescription = "RustDesk Remote Desktop"
OriginalFilename = "rustdesk.exe"
@@ -190,6 +199,7 @@ os-version = "0.2"
[dev-dependencies]
hound = "3.5"
docopt = "1.1"
[package.metadata.bundle]
name = "RustDesk"

View File

@@ -2,6 +2,7 @@ FROM debian:bullseye-slim
WORKDIR /
ARG DEBIAN_FRONTEND=noninteractive
ENV VCPKG_FORCE_SYSTEM_BINARIES=1
RUN apt update -y && \
apt install --yes --no-install-recommends \
g++ \
@@ -21,7 +22,8 @@ RUN apt update -y && \
libpam0g-dev \
libpulse-dev \
make \
cmake \
wget \
libssl-dev \
unzip \
zip \
sudo \
@@ -31,6 +33,13 @@ RUN apt update -y && \
ninja-build && \
rm -rf /var/lib/apt/lists/*
RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.6/cmake-3.30.6.tar.gz --no-check-certificate && \
tar xzf cmake-3.30.6.tar.gz && \
cd cmake-3.30.6 && \
./configure --prefix=/usr/local && \
make && \
make install
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
/vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
/vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom

View File

@@ -1,14 +1,18 @@
<p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>]<br>
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
</p>
> [!Caution]
> **Misuse Disclaimer:** <br>
> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application.
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
@@ -25,9 +29,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
[<img src="https://flathub.org/api/badge?svg&locale=en"
alt="Get it on Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
## Dependencies
@@ -125,6 +132,7 @@ Begin by cloning the repository and building the Docker container:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
@@ -160,7 +168,7 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[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
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client
## Screenshots
@@ -172,6 +180,3 @@ Please ensure that you are running these commands from the root of the RustDesk
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Public Servers](#public-servers)
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -18,8 +18,8 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.3.4
exec: usr/lib/rustdesk/rustdesk
version: 1.3.9
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
arch:
@@ -77,7 +77,7 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
test:

View File

@@ -18,8 +18,8 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.3.4
exec: usr/lib/rustdesk/rustdesk
version: 1.3.9
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
arch:
@@ -80,7 +80,7 @@ AppDir:
env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
GDK_BACKEND: x11
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
test:

View File

@@ -9,6 +9,7 @@ import shutil
import hashlib
import argparse
import sys
from pathlib import Path
windows = platform.platform().startswith('Windows')
osx = platform.platform().startswith(
@@ -321,7 +322,7 @@ def build_flutter_deb(version, features):
os.chdir('flutter')
system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/')
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/etc/rustdesk/')
system2('mkdir -p tmpdeb/etc/pam.d/')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
@@ -331,7 +332,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/')
f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
@@ -354,7 +355,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
@@ -366,7 +367,7 @@ def build_flutter_deb(version, features):
def build_deb_from_folder(version, binary_folder):
os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/')
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
@@ -374,7 +375,7 @@ def build_deb_from_folder(version, binary_folder):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/')
f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
@@ -391,7 +392,7 @@ def build_deb_from_folder(version, binary_folder):
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
@@ -404,12 +405,13 @@ def build_flutter_dmg(version, features):
if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
# copy dylib
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
system2('flutter build macos --release')
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
'''
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")
@@ -621,21 +623,24 @@ def main():
os.system('mkdir -p tmpdeb/etc/pam.d/')
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2('strip tmpdeb/usr/bin/rustdesk')
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
md5_file('etc/rustdesk/startwm.sh')
md5_file('etc/X11/rustdesk/xorg.conf')
md5_file('etc/pam.d/rustdesk')
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
def md5_file_folder(base_dir):
base_path = Path(base_dir)
for file in base_path.rglob('*'):
if file.is_file() and 'DEBIAN' not in file.parts:
relative_path = file.relative_to(base_path)
md5_file(str(relative_path))
if __name__ == "__main__":

125
docs/CODE_OF_CONDUCT-NO.md Normal file
View File

@@ -0,0 +1,125 @@
# Atferdskodeks for bidragsyterpaktern
## Hva Vi Står For
Vi som medlemer, bidragere, og ledere står for å skape ett hat-fritt felleskap,
uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger,
etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivå, utdanning,
sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual
identitet og orientasjon.
Vi står for åpen, velkommende, mangfold, inklusiv og sunn oppførsel i vårt felleskap.
## Våre Standarer
Eksempler på oppførsel som hjelper ett positivt felleskap inkluderer:
* Vise empati og vennlighet mot andre mennesker
* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer
* Gi og ta konstruktiv kritikk i beste mening
* Akseptere ansvar og unskylde seg for de som er utsatt av våre feil,
og lære av disse
* Fokusere på det som er best ikke bare for individer, men for felleskapet
Eksempler på uakseptabel oppførsel inkluderer:
* Bruk av seksualisert språk eller bilder, og seksual oppmerksomhet.
* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep
* Offentlig eller privat trakassering
* Publisering av andres private informasjon, sånn som bosteds- og epost-addresser,
uten deres godskjenning.
* Andre rettningslinjer som kan bli sett på som upassende i en profesjonell setting.
## Håndhevingsansvar
Felleskapets ledere har ansvar for å klarifisere og håndheve våre standarer av
akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons på
oppførsel de anser som upassende, truende, fornermende eller skadelig.
Felleskapets ledere har retten og ansvaret til å fjerne, redigere, eller avslå
kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke
samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for
moderatorenes valg når passende.
## Omfang
Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og
de gjelder også når ett individ representerer felleskapet på offentlige medier.
Eksempler på representasjon av vårt felleskap inkluderer bruke av offisielle e-mail
addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en
utpekt representant på digitale og fysiske arrangsjemanger.
## Håndheving
Hendelser av misbruk, trakasserende eller på noen måte uakseptert oppførsel kann
bli raportert til felleskapets ledere med ansvar for håndheving på
[info@rustdesk.com](mailto:info@rustdesk.com).
All tilbakemelding vill bli sett gjennom og investigert rettferdig så fort som mulig.
Alle felleskapets ledere er obligert til å respektere privatlivet og sikkerhetet ovenfor
den som raporterer en hendelse.
## Håndhevings Guide
Felleskapets ledere vill følge disse Rettningslinjene for sammfunspåvirkning med
tanke på konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene:
### 1. Korreksjon
**Sammfunspåvirkning**: Bruk av upassende språk eller annen oppførsel ansett som
uprofesjonelt eller uvelkommen i dette felleskapet.
**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som
klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig
unskyldning kan bli forespurt.
### 2. Advarsel
**Sammfunspåvirkning**: Ett brudd på en singulær hendelse eller en serie handlinger.
**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen
interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
de som håndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode.
Dette inkluderer å unngå interaksjoner i felleskapets platformer, samt eksterne
kanaler, som f.eks sosial media. Brudd av disse vilkårene kan føre til midlertidig
eller permanent bannlysning.
### 3. Midlertidig Bannlysning
**Sammfunspåvirkning**: Ett særiøst brudd på felleskapets standarer, inkludert
vedvarende upassende oppførsel.
**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller
offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen
interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
de som håndhever disse etiske rettningslinjene, er tillat for denne perioden.
Brudd på disse vilkårene kan føre til permanent bannlysning.
### 4. Permanent Bannlysning
**Sammfunspåvirkning**: Demonstasjon av mønster i brudd på felleskapets standarer,
inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller
aggresjon mot eller nedsettelse av grupper individer.
**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i
felleskapet
## Attribusjon
Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage],
versjon 2.0, tilgjengelig ved
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Sammfunspåvirknings guid inspirert av
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For svar til vanlige spørsmål angående disse etiske rettningslinjene, se FAQ på
[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig
ved [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

40
docs/CONTRIBUTING-KR.md Normal file
View File

@@ -0,0 +1,40 @@
# RustDesk 기여 가이드라인
RustDesk는 모든 사람의 기여를 환영합니다. 만약 RustDesk에 기여하고 싶다면 아래 가이드를 참고해주세요:
## 기여 방식
RustDesk 또는 종속성에 대한 기여는 GitHub Pull Request 형태로 이루어져야 합니다.
모든 Pull Request는 코어 기여자가 검토하며, 메인 저장소에 반영되거나 필요한 수정 사항에 대한 피드백을 받습니다.
모든 기여는 이 형식을 따라야 합니다.
특정 이슈에 작업하고 싶다면, 먼저 GitHub 이슈에 댓글을 달아 작업하겠다고 알려주세요.
이는 동일한 작업에 대해 중복 기여가 발생하는 것을 방지하기 위함입니다.
## Pull Request Checklist
- master 브랜치에서 새 브랜치를 생성하고 작업하세요.<br/>
필요한 경우 PR 제출 전에 최신 master 브랜치에 리베이스(rebase)하세요.<br/>
충돌이 발생하면 기여자가 직접 해결해야 합니다.
- 커밋은 가능한 한 작고 독립적인 단위로 작성하세요.<br/>
각 커밋은 독립적으로 빌드와 테스트를 통과해야 합니다.
- 커밋에는 반드시 Developer Certificate of Origin (http://developercertificate.org) 서명이 포함되어야 합니다.<br/>
이는 기여자(및 소속된 고용주가 있을 경우) 가 [프로젝트 라이선스](../LICENCE) 에 동의함을 나타냅니다.<br/>
Git에서는 `git commit` 명령어에 `-s` 옵션을 사용해 서명을 추가할 수 있습니다.
- PR이 검토되지 않거나 특정 리뷰어가 필요하면,
<br/> PR이나 댓글에서 리뷰어를 태그하거나 [이메일](mailto:info@rustdesk.com)로 리뷰를 요청할 수 있습니다.
- 수정된 버그나 추가된 기능과 관련된 테스트 코드를 포함해주세요.
Git 사용에 대한 자세한 내용은 [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)을 참조하세요.
## 행동 강령
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## 커뮤니케이션
RustDesk 기여자들은 [Discord](https://discord.gg/nDceKgxnkV)에서 활동하고 있습니다.

46
docs/CONTRIBUTING-NO.md Normal file
View File

@@ -0,0 +1,46 @@
# Bidrag til RustDesk
RustDesk er åpene for bidrag fra alle. Her er reglene for de som har lyst til å
hjelpe oss:
## Bidrag
Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests.
Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til
å godkjenne endringene) og enten bli sendt til main treet eller respondert med
tilbakemelding på endringer som er nødvendig. Alle bidrag burde følge dette formate
også de fra kjerne bidragere.
Om du ønsker å jobbe på en issue må du huske å gjøre krav på den først. Dette
kann gjøres ved å kommentere på den GitHub issue-en du ønsker å jobbe på.
Dette er for å hindre duplikat innsats på samme problem.
## Pull Request Sjekkliste
- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nåværende
master grenen før du sender inn din pull request. Hvis ikke dette gjøres på rent
vis vill du bli spurt om å rebase dine endringer.
- Commits burde være så små som mulig, samtidig som de må være korrekt uavhenging av hverandre
(hver commit burde kompilere og bestå tester).
- Commits burde være akkopaniert med en Developer Certificate of Origin
(http://developercertificate.org), som indikerer att du (og din arbeidsgiver
i det tilfellet) godkjenner å bli knyttet til vilkårene av [prosjekt lisensen](../LICENCE).
Ved bruk av git er dette `-s` opsjonen til `git commit`.
- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til
å se på dem kan du @-svare en med autoritet til å godkjenne dine endringer.
Dette kann gjøres i en pull request, en kommentar eller via epost på [email](mailto:info@rustdesk.com).
- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet.
For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Oppførsel
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## Kommunikasjon
RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV).

14
docs/DEVCONTAINER-NO.md Normal file
View File

@@ -0,0 +1,14 @@
Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget.
Nå tilbyr devcontainer linux og android builds i både debug og release modus.
Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds.
Kommando|Build Type|Modus
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@@ -9,6 +9,11 @@
<b>Wir brauchen Ihre Hilfe, um dieses README, die <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk-Benutzeroberfläche</a> und die <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentation</a> in Ihre Muttersprache zu übersetzen.</b>
</p>
> [!Vorsicht]
> **Haftungsausschluss bei Missbrauch::** <br>
> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung.
Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@@ -147,6 +147,10 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter
> [!Precaución]
> **Descargo de responsabilidad por uso indebido:** <br>
> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El uso indebido, como el acceso no autorizado, el control o la invasión de la privacidad, está estrictamente en contra de nuestras directrices. Los autores no son responsables de ningún uso indebido de la aplicación.
## Capturas de pantalla
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -137,6 +137,10 @@ Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme
> [!Attention]
> **Avertissement contre l'utilisation abusive:** <br>
> Les développeurs de RustDesk ne cautionnent ni ne soutiennent aucune utilisation non éthique ou illégale de ce logiciel. Toute utilisation abusive, telle que l'accès non autorisé, le contrôle ou l'invasion de la vie privée, est strictement contraire à nos directives. Les auteurs ne sont pas responsables de toute utilisation abusive de l'application.
## Images
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -164,6 +164,10 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr
- **[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
> [!Attenzione]
> **Dichiarazione di non responsabilità per uso improprio:** <br>
> Gli sviluppatori di RustDesk non approvano né supportano alcun uso non etico o illegale di questo software. L'uso improprio, come l'accesso non autorizzato, il controllo o l'invasione della privacy, è strettamente contro le nostre linee guida. Gli autori non sono responsabili per qualsiasi uso improprio dell'applicazione.
## Schermate
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -168,6 +168,10 @@ target/release/rustdesk
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
> [!注意]
> **:不正使用に関する免責事項** <br>
> RustDeskの開発者は、このソフトウェアの非倫理的または違法な使用を容認または支持しません。不正アクセス、不正な制御、またはプライバシーの侵害などの不正使用は、当社のガイドラインに厳密に違反します。開発者は、アプリケーションの不正使用に対して一切の責任を負いません。
## スクリーンショット
![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)

View File

@@ -9,12 +9,12 @@
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
</p>
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [스스로 설정](https://rustdesk.com/server)하는 것도, [스스로 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [직접 설정](https://rustdesk.com/server)하거나 [직접 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -43,9 +43,9 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
- run `cargo run`
- 실행 `cargo run`
## [Build](https://rustdesk.com/docs/en/dev/build/)
## [빌드](https://rustdesk.com/docs/en/dev/build/)
## Linux에서 빌드 순서
@@ -67,7 +67,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Install vcpkg
### vcpkg 설치
```sh
git clone https://github.com/microsoft/vcpkg
@@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Fix libvpx (For Fedora)
### libvpx 수정 (For Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -92,7 +92,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Build
### 빌드
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -107,7 +107,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
## Docker에 빌드하는 방법
포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -115,13 +115,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 이하의 커맨드를 실행합니다.
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다.
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 커맨드에 다른 인수를 지정할 필요가 있다면, 커맨드 끝에 있는 `<OPTIONAL-ARGS>` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 커맨드 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 커맨드로 실행할 수 있습니다.
첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 명령에 다른 인수를 지정할 필요가 있다면, 명령 끝에 있는 `<OPTIONAL-ARGS>` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 명령 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다.
```sh
target/debug/rustdesk
@@ -133,9 +133,9 @@ target/debug/rustdesk
target/release/rustdesk
```
커맨드를 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 서브커맨드는 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
명령을 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 하위 명령은 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
## File Structure
## 파일 구조
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, tcp/udp 랩퍼, protobuf, 파일 전송을 위한 fs 함수, 그 외 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처
@@ -143,12 +143,16 @@ target/release/rustdesk
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, 클립보드, 입력, 비디오 서비스 그리고 네트워크 연결
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 접속 시작
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트(TCP hole punching) 혹은 relayed 접속
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트 (TCP hole punching) 혹은 relayed 접속
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트
## Snapshot
> [!주의]
> **오용에 대한 면책 조항:** <br>
> RustDesk의 개발자들은 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용인하거나 지원하지 않습니다. 무단 접근, 제어 또는 개인정보 침해와 같은 오용은 우리의 지침을 엄격히 위반하는 것입니다. 개발자들은 애플리케이션의 오용에 대해 책임을 지지 않습니다.
## 스냅샷
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

177
docs/README-NO.md Normal file
View File

@@ -0,0 +1,177 @@
<p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#public-servers">Servere</a> •
<a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Struktur</a> •
<a href="#snapshot">Snapshot</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a><br>
<b>Vi trenger din hjelp til å oversette denne README-en, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> tid ditt morsmål</b>
</p>
Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](docs/CONTRIBUTING-NO.md) for hjelp med oppstart.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases)
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Få det på F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
[<img src="https://flathub.org/api/badge?svg&locale=en"
alt="Få det på Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
## Avhengigheter
Desktop versjoner bruker Flutter eller Sciter (avviklet) for GUI, denne veiledningen er bare for Sciter, grunnet att det er letter og en mer venlig start. Skjekk ut vår [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for bygging av Flutter versjonen.
Venligst last ned Sciters dynamiske bibliotek selv.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Rå steg for bygging
- Klargjør ditt Rust development env og C++ build env
- Installer [vcpkg](https://github.com/microsoft/vcpkg), og koriger `VCPKG_ROOT` env vaiabelen
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/macOS: vcpkg install libvpx libyuv opus aom
- Kjør `cargo run`
## [Bygg](https://rustdesk.com/docs/en/dev/build/)
## Hvordan Bygge til Linux
### Ubuntu 18 (Debian 10)
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
```sh
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Installer vcpkg
```sh
git clone https://github.com/microsoft/vcpkg
cd vcpkg
git checkout 2023.04.15
cd ..
vcpkg/bootstrap-vcpkg.sh
export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Fiks libvpx (For Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
cd *
./configure
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
make
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Bygg
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Hvordan bygge med Docker
Start med å klone repositoret og bygg Docker konteineren:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
docker build -t "rustdesk-builder" .
```
Deretter, hver gang du trenger å bygge applikasjonen, kjør følgene kommando:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
Det kan ta lengere tid før avhengighetene blir bufret første gang du bygger, senere bygg er raskere. Hvis du trenger å spesifisere forkjellige argumenter til bygge kommandoen, kan du gjøre det på slutten av kommandoen ved `<OPTIONAL-ARGS>` feltet. For eksempel, hvis du ville bygge en optimalisert release versjon, ville du kjørt kommandoen over fulgt `--release`. Den kjørbare filen vill være tilgjengelig i mål direktive på ditt system, og kan bli kjørt med:
```sh
target/debug/rustdesk
```
Eller, hvis du kjører ett release program:
```sh
target/release/rustdesk
```
Venligst pass på att du kjører disse kommandoene fra roten av RustDesk repositoret, eller kan det hende att applikasjon ikke finner de riktige ressursene. Pass også på att andre cargo subkommandoer som for eksempel `install` eller `run` ikke støttes med denne metoden da de vill installere eller kjøre programmet i konteineren istedet for verten.
## Fil Struktur
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodek, configurasjon, tcp/udp innpakning, protobuf, fs funksjon for fil overføring, og noen andre verktøy funksjoner
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: skjermfangst
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform spesefik keyboard/mus kontroll
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: fil kopi og innliming implementasjon for Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: foreldret Sciter UI (avviklet)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/utklippstavle/input/video tjenester, og internett tilkobling
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start en peer tilkobling
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient
## Skjermbilder
![Tilkoblings Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![Koble til Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![Fil Overføring](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

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

View File

@@ -137,6 +137,10 @@ Por favor verifique que está executando estes comandos da raiz do repositório
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma
> [!Cuidadob]
> **Aviso de uso indevido:** <br>
> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação.
## Screenshots
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -148,6 +148,10 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: свяжитесь с [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитесь удаленного прямого (обход TCP NAT) или ретранслируемого соединения
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
> [!Осторожно]
> **Отказ от ответственности за неправомерное использование:** <br>
> Разработчики RustDesk не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование, такое как несанкционированный доступ, контроль или вторжение в частную жизнь, строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения.
## Скриншоты
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -166,6 +166,10 @@ Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan e
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
> [!Dikkat]
> **Yanlış Kullanım Uyarısı:** <br>
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
## Ekran Görüntüleri
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -172,6 +172,3 @@ target/release/rustdesk
![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Публічні сервери](#публічні-сервери)
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -8,6 +8,10 @@
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</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-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
</p>
> [!警告]
> **免责声明:** <br>
> RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

9
docs/SECURITY-NO.md Normal file
View File

@@ -0,0 +1,9 @@
# Sikkerhets Rettningslinjer
## Reportering av en Sårbarhet
Vi verdsetter pris på sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til å rapportere sårbarheter de oppdager til oss.
Om du finner en sikkerhets sårbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved å sende oss en email til info@rustdesk.com.
På dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver å løse ett stort problem. Vi trenger att du raporterer alle sårbarhetene
annsvarfult så vi kan fortsettte å bygge ett en sikker applikasjon for hele felleskapet.

View File

@@ -1,28 +0,0 @@
[package]
name = "custom_plugin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "custom_plugin"
path = "src/lib.rs"
crate-type = ["cdylib"]
[features]
default = ["flutter"]
flutter = []
[dependencies]
lazy_static = "1.4.0"
rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]}
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'
strip = true
#opt-level = 'z' # only have smaller size after strip
rpath = true

View File

@@ -1,30 +0,0 @@
use librustdesk::api::RustDeskApiTable;
/// This file demonstrates how to write a custom plugin for RustDesk.
use std::ffi::{c_char, c_int, CString};
lazy_static::lazy_static! {
pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap();
pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap();
// Do your own logic based on the API provided by RustDesk.
pub static ref API: RustDeskApiTable = RustDeskApiTable::default();
}
#[no_mangle]
fn plugin_name() -> *const c_char {
return PLUGIN_NAME.as_ptr();
}
#[no_mangle]
fn plugin_id() -> *const c_char {
return PLUGIN_ID.as_ptr();
}
#[no_mangle]
fn plugin_init() -> c_int {
return 0 as _;
}
#[no_mangle]
fn plugin_dispose() -> c_int {
return 0 as _;
}

90
examples/ipc.rs Normal file
View File

@@ -0,0 +1,90 @@
use docopt::Docopt;
use hbb_common::{
env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV},
log, tokio,
};
use librustdesk::{ipc::Data, *};
const USAGE: &'static str = "
IPC test program.
Usage:
ipc (-s | --server | -c | --client) [-p <str> | --postfix=<str>]
ipc (-h | --help)
Options:
-h --help Show this screen.
-s --server Run as IPC server.
-c --client Run as IPC client.
-p --postfix=<str> IPC path postfix [default: ].
";
#[derive(Debug, serde::Deserialize)]
struct Args {
flag_server: bool,
flag_client: bool,
flag_postfix: String,
}
#[tokio::main]
async fn main() {
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit());
if args.flag_server {
if args.flag_postfix.is_empty() {
log::info!("Starting IPC server...");
} else {
log::info!(
"Starting IPC server with postfix: '{}'...",
args.flag_postfix
);
}
ipc_server(&args.flag_postfix).await;
} else if args.flag_client {
if args.flag_postfix.is_empty() {
log::info!("Starting IPC client...");
} else {
log::info!(
"Starting IPC client with postfix: '{}'...",
args.flag_postfix
);
}
ipc_client(&args.flag_postfix).await;
}
}
async fn ipc_server(postfix: &str) {
let postfix = postfix.to_string();
let postfix2 = postfix.clone();
std::thread::spawn(move || {
if let Err(err) = crate::ipc::start(&postfix) {
log::error!("Failed to start ipc: {}", err);
std::process::exit(-1);
}
});
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
ipc_client(&postfix2).await;
}
async fn ipc_client(postfix: &str) {
loop {
match crate::ipc::connect(1000, postfix).await {
Ok(mut conn) => match conn.send(&Data::Empty).await {
Ok(_) => {
log::info!("send message to ipc server success");
}
Err(e) => {
log::error!("Failed to send message to ipc server: {}", e);
}
},
Err(e) => {
log::error!("Failed to connect to ipc server: {}", e);
}
}
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
}
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.rustdesk.RustDesk</id>
<developer id="com.rustdesk">
<name>RustDesk</name>
</developer>
<launchable type="desktop-id">com.rustdesk.RustDesk.desktop</launchable>
<metadata_license>CC0-1.0</metadata_license>
<project_license>AGPL-3.0-only</project_license>
<name>RustDesk</name>
<summary>Secure remote desktop access</summary>
<description>
<p>
RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration.
</p>
<ul>
<li> Works on Windows, macOS, Linux, iOS, Android, Web. </li>
<li> Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs. </li>
<li> Own your data, easily set up self-hosting solution on your infrastructure. </li>
<li> P2P connection with end-to-end encryption based on NaCl. </li>
<li> No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand. </li>
<li> We like to keep things simple and will strive to make simpler where possible. </li>
</ul>
<p>
For self-hosting setup instructions please go to our home page.
</p>
</description>
<categories>
<category>Utility</category>
</categories>
<screenshots>
<screenshot type="default">
<caption>Remote desktop session</caption>
<image>https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png</image>
</screenshot>
</screenshots>
<branding>
<color type="primary" scheme_preference="light">#d9eaf8</color>
<color type="primary" scheme_preference="dark">#0160ee</color>
</branding>
<url type="homepage">https://rustdesk.com</url>
<url type="bugtracker">https://github.com/rustdesk/rustdesk/issues</url>
<url type="faq">https://github.com/rustdesk/rustdesk/wiki/FAQ</url>
<url type="help">https://rustdesk.com/docs</url>
<url type="donation">https://ko-fi.com/rustdesk</url>
<url type="vcs-browser">https://github.com/rustdesk/rustdesk</url>
<url type="translate">https://github.com/rustdesk/rustdesk/tree/master/src/lang</url>
<url type="contribute">https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md</url>
<url type="contact">https://rustdesk.com/docs/en/technical-support</url>
<requires>
<display_length compare="ge">600</display_length>
<internet>always</internet>
</requires>
<supports>
<control>keyboard</control>
<control>pointing</control>
</supports>
<content_rating type="oars-1.1"/>
</component>

View File

@@ -1,19 +1,30 @@
{
"id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform",
"runtime-version": "23.08",
"runtime-version": "24.08",
"sdk": "org.freedesktop.Sdk",
"command": "rustdesk",
"icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"rename-desktop-file": "rustdesk.desktop",
"rename-icon": "rustdesk",
"modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
"xdotool.json",
{
"name": "pam",
"buildsystem": "simple",
"build-commands": [
"./configure --disable-selinux --prefix=/app && make -j4 install"
],
"name": "xdotool",
"no-autogen": true,
"make-install-args": ["PREFIX=${FLATPAK_DEST}"],
"sources": [
{
"type": "archive",
"url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
"sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
}
]
},
{
"name": "pam",
"buildsystem": "autotools",
"config-opts": ["--disable-selinux"],
"sources": [
{
"type": "archive",
@@ -26,32 +37,24 @@
"name": "rustdesk",
"buildsystem": "simple",
"build-commands": [
"bsdtar -zxvf rustdesk.deb",
"tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
"mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.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 scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
"bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -",
"cp -r usr/* /app/",
"mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk"
],
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [
{
"type": "file",
"path": "./rustdesk.deb"
"path": "rustdesk.deb"
},
{
"type": "file",
"path": "../res/scalable.svg"
"path": "com.rustdesk.RustDesk.metainfo.xml"
}
]
}
],
"finish-args": [
"--share=ipc",
"--socket=x11",
"--socket=fallback-x11",
"--socket=wayland",
"--share=network",
@@ -60,4 +63,4 @@
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Flatpak"
]
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "xdotool",
"buildsystem": "simple",
"build-commands": [
"make -j4 && PREFIX=./build make install",
"cp -r ./build/* /app/"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
"sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
}
]
}

View File

@@ -306,8 +306,8 @@ class FloatingWindowService : Service(), View.OnTouchListener {
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
// For host side, clipboard sync
val idSyncClipboard = 1
val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false
if (isClipboardListenerEnabled) {
val isServiceSyncEnabled = (MainActivity.rdClipboardManager?.isCaptureStarted ?: false) && FFI.isServiceClipboardEnabled()
if (isServiceSyncEnabled) {
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
}
val idStopService = 2

View File

@@ -19,6 +19,7 @@ import android.view.accessibility.AccessibilityEvent
import android.view.ViewGroup.LayoutParams
import android.view.accessibility.AccessibilityNodeInfo
import android.view.KeyEvent as KeyEventAndroid
import android.view.ViewConfiguration
import android.graphics.Rect
import android.media.AudioManager
import android.accessibilityservice.AccessibilityServiceInfo
@@ -34,10 +35,15 @@ import hbb.MessageOuterClass.KeyEvent
import hbb.MessageOuterClass.KeyboardMode
import hbb.KeyEventConverter
const val LIFT_DOWN = 9
const val LIFT_MOVE = 8
const val LIFT_UP = 10
// const val BUTTON_UP = 2
// const val BUTTON_BACK = 0x08
const val LEFT_DOWN = 9
const val LEFT_MOVE = 8
const val LEFT_UP = 10
const val RIGHT_UP = 18
// (BUTTON_BACK << 3) | BUTTON_UP
const val BACK_UP = 66
const val WHEEL_BUTTON_DOWN = 33
const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331
@@ -64,12 +70,15 @@ class InputService : AccessibilityService() {
private val logTag = "input service"
private var leftIsDown = false
private var touchPath = Path()
private val touchPath = Path()
private var stroke: GestureDescription.StrokeDescription? = null
private var lastTouchGestureStartTime = 0L
private var mouseX = 0
private var mouseY = 0
private var timer = Timer()
private var recentActionTask: TimerTask? = null
// 100(tap timeout) + 400(long press timeout)
private val longPressDuration = ViewConfiguration.getTapTimeout().toLong() + ViewConfiguration.getLongPressTimeout().toLong()
private val wheelActionsQueue = LinkedList<GestureDescription>()
private var isWheelActionsPolling = false
@@ -77,6 +86,9 @@ class InputService : AccessibilityService() {
private var fakeEditTextForTextStateCalculation: EditText? = null
private var lastX = 0
private var lastY = 0
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
@RequiresApi(Build.VERSION_CODES.N)
@@ -84,7 +96,7 @@ class InputService : AccessibilityService() {
val x = max(0, _x)
val y = max(0, _y)
if (mask == 0 || mask == LIFT_MOVE) {
if (mask == 0 || mask == LEFT_MOVE) {
val oldX = mouseX
val oldY = mouseY
mouseX = x * SCREEN_INFO.scale
@@ -98,31 +110,30 @@ class InputService : AccessibilityService() {
}
}
// left button down ,was up
if (mask == LIFT_DOWN) {
// left button down, was up
if (mask == LEFT_DOWN) {
isWaitingLongPress = true
timer.schedule(object : TimerTask() {
override fun run() {
if (isWaitingLongPress) {
isWaitingLongPress = false
leftIsDown = false
endGesture(mouseX, mouseY)
continueGesture(mouseX, mouseY)
}
}
}, LONG_TAP_DELAY * 4)
}, longPressDuration)
leftIsDown = true
startGesture(mouseX, mouseY)
return
}
// left down ,was down
// left down, was down
if (leftIsDown) {
continueGesture(mouseX, mouseY)
}
// left up ,was down
if (mask == LIFT_UP) {
// left up, was down
if (mask == LEFT_UP) {
if (leftIsDown) {
leftIsDown = false
isWaitingLongPress = false
@@ -132,6 +143,11 @@ class InputService : AccessibilityService() {
}
if (mask == RIGHT_UP) {
longPress(mouseX, mouseY)
return
}
if (mask == BACK_UP) {
performGlobalAction(GLOBAL_ACTION_BACK)
return
}
@@ -241,36 +257,78 @@ class InputService : AccessibilityService() {
}
}
private fun startGesture(x: Int, y: Int) {
touchPath = Path()
touchPath.moveTo(x.toFloat(), y.toFloat())
lastTouchGestureStartTime = System.currentTimeMillis()
@RequiresApi(Build.VERSION_CODES.N)
private fun performClick(x: Int, y: Int, duration: Long) {
val path = Path()
path.moveTo(x.toFloat(), y.toFloat())
try {
val longPressStroke = GestureDescription.StrokeDescription(path, 0, duration)
val builder = GestureDescription.Builder()
builder.addStroke(longPressStroke)
Log.d(logTag, "performClick x:$x y:$y time:$duration")
dispatchGesture(builder.build(), null, null)
} catch (e: Exception) {
Log.e(logTag, "performClick, error:$e")
}
}
private fun continueGesture(x: Int, y: Int) {
@RequiresApi(Build.VERSION_CODES.N)
private fun longPress(x: Int, y: Int) {
performClick(x, y, longPressDuration)
}
private fun startGesture(x: Int, y: Int) {
touchPath.reset()
touchPath.moveTo(x.toFloat(), y.toFloat())
lastTouchGestureStartTime = System.currentTimeMillis()
lastX = x
lastY = y
}
@RequiresApi(Build.VERSION_CODES.N)
private fun doDispatchGesture(x: Int, y: Int, willContinue: Boolean) {
touchPath.lineTo(x.toFloat(), y.toFloat())
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
if (duration <= 0) {
duration = 1
}
try {
if (stroke == null) {
stroke = GestureDescription.StrokeDescription(
touchPath,
0,
duration,
willContinue
)
} else {
stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue)
}
stroke?.let {
val builder = GestureDescription.Builder()
builder.addStroke(it)
Log.d(logTag, "doDispatchGesture x:$x y:$y time:$duration")
dispatchGesture(builder.build(), null, null)
}
} catch (e: Exception) {
Log.e(logTag, "doDispatchGesture, willContinue:$willContinue, error:$e")
}
}
@RequiresApi(Build.VERSION_CODES.N)
private fun continueGesture(x: Int, y: Int) {
doDispatchGesture(x, y, true)
touchPath.reset()
touchPath.moveTo(x.toFloat(), y.toFloat())
lastTouchGestureStartTime = System.currentTimeMillis()
lastX = x
lastY = y
}
@RequiresApi(Build.VERSION_CODES.N)
private fun endGesture(x: Int, y: Int) {
try {
touchPath.lineTo(x.toFloat(), y.toFloat())
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
if (duration <= 0) {
duration = 1
}
val stroke = GestureDescription.StrokeDescription(
touchPath,
0,
duration
)
val builder = GestureDescription.Builder()
builder.addStroke(stroke)
Log.d(logTag, "end gesture x:$x y:$y time:$duration")
dispatchGesture(builder.build(), null, null)
} catch (e: Exception) {
Log.e(logTag, "endGesture error:$e")
}
doDispatchGesture(x, y, false)
touchPath.reset()
stroke = null
}
@RequiresApi(Build.VERSION_CODES.N)

View File

@@ -103,7 +103,6 @@ class MainActivity : FlutterActivity() {
mainService?.let {
unbindService(serviceConnection)
}
rdClipboardManager?.rustEnableServiceClipboard(false)
super.onDestroy()
}
@@ -221,6 +220,10 @@ class MainActivity : FlutterActivity() {
result.success(true)
}
"try_sync_clipboard" -> {
rdClipboardManager?.syncClipboard(true)
result.success(true)
}
GET_START_ON_BOOT_OPT -> {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
@@ -402,13 +405,4 @@ class MainActivity : FlutterActivity() {
super.onStart()
stopService(Intent(this, FloatingWindowService::class.java))
}
// For client side
// When swithing from other app to this app, try to sync clipboard.
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
rdClipboardManager?.syncClipboard(true)
}
}
}

View File

@@ -65,8 +65,8 @@ class MainService : Service() {
@Keep
@RequiresApi(Build.VERSION_CODES.N)
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
// turn on screen with LEFT_DOWN when screen off
if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) {
if (wakeLock.isHeld) {
Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release()
@@ -433,6 +433,7 @@ class MainService : Service() {
checkMediaPermission()
_isStart = true
FFI.setFrameRawEnable("video",true)
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
return true
}
@@ -441,6 +442,7 @@ class MainService : Service() {
Log.d(logTag, "Stop Capture")
FFI.setFrameRawEnable("video",false)
_isStart = false
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
// release video
if (reuseVirtualDisplay) {
// The virtual display video projection can be paused by calling `setSurface(null)`.

View File

@@ -36,19 +36,19 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) {
// though the `lastUpdatedClipData` will be set to null once.
private var lastUpdatedClipData: ClipData? = null
private var isClientEnabled = true;
private var _isListening = false;
val isListening: Boolean
get() = _isListening
private var _isCaptureStarted = false;
fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) {
val isCaptureStarted: Boolean
get() = _isCaptureStarted
fun checkPrimaryClip(isClient: Boolean) {
val clipData = clipboardManager.primaryClip
if (clipData != null && clipData.itemCount > 0) {
// Only handle the first item in the clipboard for now.
val clip = clipData.getItemAt(0)
val isHostSync = !isClient && isSync
// Ignore the `isClipboardDataEqual()` check if it's a host sync operation.
// Because it's a action manually triggered by the user.
if (!isHostSync) {
// Ignore the `isClipboardDataEqual()` check if it's a host operation.
// Because it's an action manually triggered by the user.
if (isClient) {
if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) {
Log.d(logTag, "Clipboard data is the same as last update, ignore")
return
@@ -95,13 +95,6 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) {
}
}
private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener {
override fun onPrimaryClipChanged() {
Log.d(logTag, "onPrimaryClipChanged")
checkPrimaryClip(true, false)
}
}
private fun isSupportedMimeType(mimeType: String): Boolean {
return supportedMimeTypes.contains(mimeType)
}
@@ -136,43 +129,23 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) {
return true
}
@Keep
fun rustEnableServiceClipboard(enable: Boolean) {
Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening")
if (enable) {
if (!_isListening) {
clipboardManager.addPrimaryClipChangedListener(clipboardListener)
_isListening = true
}
} else {
if (_isListening) {
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
_isListening = false
lastUpdatedClipData = null
}
}
fun setCaptureStarted(started: Boolean) {
_isCaptureStarted = started
}
@Keep
fun rustEnableClientClipboard(enable: Boolean) {
Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
isClientEnabled = enable
if (enable) {
lastUpdatedClipData = clipboardManager.primaryClip
} else {
lastUpdatedClipData = null
}
lastUpdatedClipData = null
}
fun syncClipboard(isClient: Boolean) {
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening")
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled")
if (isClient && !isClientEnabled) {
return
}
if (!isClient && !_isListening) {
return
}
checkPrimaryClip(isClient, true)
checkPrimaryClip(isClient)
}
@Keep

View File

@@ -24,4 +24,5 @@ object FFI {
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
external fun onClipboardUpdate(clips: ByteBuffer)
external fun isServiceClipboardEnabled(): Boolean
}

View File

@@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx1024M
android.useAndroidX=true
android.enableJetifier=true
org.gradle.daemon=false

Binary file not shown.

BIN
flutter/assets/more.ttf Normal file

Binary file not shown.

View File

@@ -150,6 +150,10 @@ prebuild)
# Flutter used to compile Flutter<->Rust bridge files
CARGO_EXPAND_VERSION="$(yq -r \
.env.CARGO_EXPAND_VERSION \
.github/workflows/bridge.yml)"
FLUTTER_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/bridge.yml)"
@@ -237,7 +241,10 @@ prebuild)
# Install rust bridge generator
cargo install cargo-expand
cargo install \
cargo-expand \
--version "${CARGO_EXPAND_VERSION}" \
--locked
cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
--features "uuid" \

View File

@@ -2,4 +2,6 @@
# https://docs.flutter.dev/deployment/ios
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# no obfuscate, because no easy to check errors
cd $(dirname $(dirname $(which flutter)))
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
flutter build ipa --release

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
cd build/web/
python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
mv jds/dist/index.js ./
mv jds/dist/vendor.js ./
/bin/rm -rf js
python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)'
python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)'
tar czf x *
scp x sg:/tmp/
ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
/bin/rm x
cd -

View File

@@ -133,7 +133,7 @@ SPEC CHECKSUMS:
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47

View File

@@ -1,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@@ -1,2 +1,4 @@
#!/usr/bin/env bash
cd $(dirname $(dirname $(which flutter)))
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:io';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -30,8 +29,10 @@ import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
import 'mobile/pages/view_camera_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'desktop/pages/view_camera_page.dart' as desktop_view_camera;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
@@ -97,6 +98,7 @@ enum DesktopType {
main,
remote,
fileTransfer,
viewCamera,
cm,
portForward,
}
@@ -104,6 +106,10 @@ enum DesktopType {
class IconFont {
static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar';
static const _family3 = 'AddressBook';
static const _family4 = 'DeviceGroup';
static const _family5 = 'More';
IconFont._();
static const IconData max = IconData(0xe606, fontFamily: _family1);
@@ -114,8 +120,12 @@ class IconFont {
static const IconData menu = IconData(0xe628, fontFamily: _family1);
static const IconData search = IconData(0xe6a4, fontFamily: _family2);
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook");
static const IconData addressBook = IconData(0xe602, fontFamily: _family3);
static const IconData deviceGroupOutline =
IconData(0xe623, fontFamily: _family4);
static const IconData deviceGroupFill =
IconData(0xe748, fontFamily: _family4);
static const IconData more = IconData(0xe609, fontFamily: _family5);
}
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
@@ -818,7 +828,11 @@ class OverlayDialogManager {
close([res]) {
_dialogs.remove(dialogTag);
dialog.complete(res);
try {
dialog.complete(res);
} catch (e) {
debugPrint("Dialog complete catch error: $e");
}
BackButtonInterceptor.removeByName(dialogTag);
}
@@ -1742,7 +1756,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
await bind.setLocalFlutterOption(
k: windowFramePrefix + type.name, v: pos.toString());
if (type == WindowType.RemoteDesktop && windowId != null) {
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
windowId != null) {
await _saveSessionWindowPosition(
type, windowId, isMaximized, isFullscreen, pos);
}
@@ -1893,7 +1908,9 @@ Future<bool> restoreWindowPosition(WindowType type,
String? pos;
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
// Though "open in tabs" is true and the new window restore peer position, it's ok.
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
windowId != null &&
peerId != null) {
final peerPos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: windowFramePrefix + type.name);
if (peerPos.isNotEmpty) {
@@ -1908,7 +1925,7 @@ Future<bool> restoreWindowPosition(WindowType type,
debugPrint("no window position saved, ignoring position restoration");
return false;
}
if (type == WindowType.RemoteDesktop) {
if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
if (!isRemotePeerPos && windowId != null) {
if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
@@ -2077,6 +2094,7 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
enum UriLinkType {
remoteDesktop,
fileTransfer,
viewCamera,
portForward,
rdp,
}
@@ -2128,6 +2146,11 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
id = args[i + 1];
i++;
break;
case '--view-camera':
type = UriLinkType.viewCamera;
id = args[i + 1];
i++;
break;
case '--port-forward':
type = UriLinkType.portForward;
id = args[i + 1];
@@ -2169,6 +2192,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
password: password, forceRelay: forceRelay);
});
break;
case UriLinkType.viewCamera:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newViewCamera(id!,
password: password, forceRelay: forceRelay);
});
break;
case UriLinkType.portForward:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newPortForward(id!, false,
@@ -2192,7 +2221,14 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
List<String>? urlLinkToCmdArgs(Uri uri) {
String? command;
String? id;
final options = ["connect", "play", "file-transfer", "port-forward", "rdp"];
final options = [
"connect",
"play",
"file-transfer",
"view-camera",
"port-forward",
"rdp"
];
if (uri.authority.isEmpty &&
uri.path.split('').every((char) => char == '/')) {
return [];
@@ -2230,6 +2266,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
connect(Get.context!, id);
} else if (optionIndex == 2) {
connect(Get.context!, id, isFileTransfer: true);
} else if (optionIndex == 3) {
connect(Get.context!, id, isViewCamera: true);
}
return null;
}
@@ -2282,6 +2320,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isViewCamera,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay,
@@ -2294,6 +2333,12 @@ connectMainDesktop(String id,
isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay);
} else if (isViewCamera) {
await rustDeskWinManager.newViewCamera(id,
password: password,
isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP,
password: password,
@@ -2310,10 +2355,12 @@ connectMainDesktop(String id,
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isViewCamera], starts a session only for view camera.
/// 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 isViewCamera = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool forceRelay = false,
@@ -2345,6 +2392,7 @@ connect(BuildContext context, String id,
await connectMainDesktop(
id,
isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
password: password,
@@ -2355,6 +2403,7 @@ connect(BuildContext context, String id,
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isViewCamera': isViewCamera,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
'password': password,
@@ -2392,6 +2441,31 @@ connect(BuildContext context, String id,
),
);
}
} else if (isViewCamera) {
if (isWeb) {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
desktop_view_camera.ViewCameraPage(
key: ValueKey(id),
id: id,
toolbarState: ToolbarState(),
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ViewCameraPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
} else {
if (isWeb) {
Navigator.push(
@@ -2567,6 +2641,8 @@ bool get kUseCompatibleUiMode =>
isWindows &&
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10;
class ServerConfig {
late String idServer;
late String relayServer;
@@ -2676,6 +2752,8 @@ String getWindowName({WindowType? overrideType}) {
return name;
case WindowType.FileTransfer:
return "File Transfer - $name";
case WindowType.ViewCamera:
return "View Camera - $name";
case WindowType.PortForward:
return "Port Forward - $name";
case WindowType.RemoteDesktop:
@@ -2810,7 +2888,7 @@ Widget buildRemoteBlock(
onExit: (event) => block.value = false,
child: Stack(children: [
// scope block tab
FocusScope(child: child, canRequestFocus: !block.value),
preventMouseKeyBuilder(child: child, block: block.value),
// mask block click, cm not block click and still use check_click_time to avoid block local click
if (mask)
Offstage(
@@ -2822,6 +2900,11 @@ Widget buildRemoteBlock(
));
}
Widget preventMouseKeyBuilder({required Widget child, required bool block}) {
return ExcludeFocus(
excluding: block, child: AbsorbPointer(child: child, absorbing: block));
}
Widget unreadMessageCountBuilder(RxInt? count,
{double? size, double? fontSize}) {
return Obx(() => Offstage(
@@ -3036,6 +3119,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
'peer_id': peerId,
'display': i,
'display_count': pi.displays.length,
'window_type': (kWindowType ?? WindowType.RemoteDesktop).index,
};
if (screenRect != null) {
args['screen_rect'] = {
@@ -3050,12 +3134,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
}
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
int? display, Rect? screenRect) async {
WindowType windowType, int? display, Rect? screenRect) async {
if (screenRect == null) {
// Do not restore window position to new connection if there's a pre-session.
// https://github.com/rustdesk/rustdesk/discussions/8825
if (preSessionCount == 0) {
await restoreWindowPosition(WindowType.RemoteDesktop,
await restoreWindowPosition(windowType,
windowId: windowId, display: display, peerId: peerId);
}
} else {
@@ -3460,35 +3544,6 @@ Widget buildPresetPasswordWarning() {
);
}
bool get isLinuxMateDesktop =>
isLinux &&
(Platform.environment['XDG_CURRENT_DESKTOP']?.toLowerCase() == 'mate' ||
Platform.environment['XDG_SESSION_DESKTOP']?.toLowerCase() == 'mate' ||
Platform.environment['DESKTOP_SESSION']?.toLowerCase() == 'mate');
Map<String, dynamic>? _linuxOsDistro;
String getLinuxOsDistroId() {
if (_linuxOsDistro == null) {
String osInfo = bind.getOsDistroInfo();
if (osInfo.isEmpty) {
_linuxOsDistro = {};
} else {
try {
_linuxOsDistro = jsonDecode(osInfo);
} catch (e) {
debugPrint('Failed to parse os info: $e');
// Don't call `bind.getOsDistroInfo()` again if failed to parse osInfo.
_linuxOsDistro = {};
}
}
}
return (_linuxOsDistro?['id'] ?? '') as String;
}
bool get isLinuxMint =>
getLinuxOsDistroId().toLowerCase().contains('linuxmint');
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
boxShadow() => isMainDesktopWindow
@@ -3635,7 +3690,7 @@ void earlyAssert() {
}
void checkUpdate() {
if (isDesktop || isAndroid) {
if (!isWeb) {
if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
@@ -3650,3 +3705,113 @@ void checkUpdate() {
}
}
}
// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535
// For TextField, TextFormField
extension WorkaroundFreezeLinuxMint on Widget {
Widget workaroundFreezeLinuxMint() {
// No need to check if is Linux Mint, because this workaround is harmless on other platforms.
if (isLinux) {
return ExcludeSemantics(child: this);
} else {
return this;
}
}
}
// Don't use `extension` here, the border looks weird if using `extension` in my test.
Widget workaroundWindowBorder(BuildContext context, Widget child) {
if (!isWin10) {
return child;
}
final isLight = Theme.of(context).brightness == Brightness.light;
final borderColor = isLight ? Colors.black87 : Colors.grey;
final width = isLight ? 0.5 : 0.1;
getBorderWidget(Widget child) {
return Obx(() =>
(stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue)
? Offstage()
: child);
}
final List<Widget> borders = [
getBorderWidget(Container(
color: borderColor,
height: width + 0.1,
))
];
if (kWindowType == WindowType.Main && !isLight) {
borders.addAll([
getBorderWidget(Align(
alignment: Alignment.topLeft,
child: Container(
color: borderColor,
width: width,
),
)),
getBorderWidget(Align(
alignment: Alignment.topRight,
child: Container(
color: borderColor,
width: width,
),
)),
getBorderWidget(Align(
alignment: Alignment.bottomCenter,
child: Container(
color: borderColor,
height: width,
),
)),
]);
}
return Stack(
children: [
child,
...borders,
],
);
}
void updateTextAndPreserveSelection(
TextEditingController controller, String text) {
// Only care about select all for now.
final isSelected = controller.selection.isValid &&
controller.selection.end > controller.selection.start;
// Set text will make the selection invalid.
controller.text = text;
if (isSelected) {
controller.selection = TextSelection(
baseOffset: 0, extentOffset: controller.value.text.length);
}
}
List<String> getPrinterNames() {
final printerNamesJson = bind.mainGetPrinterNames();
if (printerNamesJson.isEmpty) {
return [];
}
try {
final List<dynamic> printerNamesList = jsonDecode(printerNamesJson);
final appPrinterName = '$appName Printer';
return printerNamesList
.map((e) => e.toString())
.where((name) => name != appPrinterName)
.toList();
} catch (e) {
debugPrint('failed to parse printer names, err: $e');
return [];
}
}
String _appName = '';
String get appName {
if (_appName.isEmpty) {
_appName = bind.mainGetAppNameSync();
}
return _appName;
}

View File

@@ -67,6 +67,7 @@ class PeerPayload {
int? status;
String user = '';
String user_name = '';
String? device_group_name;
String note = '';
PeerPayload.fromJson(Map<String, dynamic> json)
@@ -75,6 +76,7 @@ class PeerPayload {
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
device_group_name = json['device_group_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
@@ -84,6 +86,7 @@ class PeerPayload {
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
"device_group_name": p.device_group_name,
});
}
@@ -265,3 +268,19 @@ class AbTag {
: name = json['name'] ?? '',
color = json['color'] ?? '';
}
class DeviceGroupPayload {
String name;
DeviceGroupPayload(this.name);
DeviceGroupPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '';
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
};
return map;
}
}

View File

@@ -286,7 +286,7 @@ class _AddressBookState extends State<AddressBook> {
borderRadius: BorderRadius.circular(8),
),
),
),
).workaroundFreezeLinuxMint(),
),
searchMatchFn: (item, searchValue) {
return item.value
@@ -509,13 +509,13 @@ class _AddressBookState extends State<AddressBook> {
double marginBottom = 4;
row({required Widget lable, required Widget input}) {
row({required Widget label, required Widget input}) {
makeChild(bool isPortrait) => Row(
children: [
!isPortrait
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10))
child: label.marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: ConstrainedBox(
@@ -535,7 +535,7 @@ class _AddressBookState extends State<AddressBook> {
Column(
children: [
row(
lable: Row(
label: Row(
children: [
Text(
'*',
@@ -556,9 +556,9 @@ class _AddressBookState extends State<AddressBook> {
: translate('ID'),
errorText: errorMsg,
errorMaxLines: 5),
))),
).workaroundFreezeLinuxMint())),
row(
lable: Text(
label: Text(
translate('Alias'),
style: style,
),
@@ -569,11 +569,11 @@ class _AddressBookState extends State<AddressBook> {
? null
: translate('Alias'),
),
)),
).workaroundFreezeLinuxMint()),
),
if (isCurrentAbShared)
row(
lable: Text(
label: Text(
translate('Password'),
style: style,
),
@@ -598,7 +598,7 @@ class _AddressBookState extends State<AddressBook> {
},
),
),
),
).workaroundFreezeLinuxMint(),
)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
@@ -704,7 +704,7 @@ class _AddressBookState extends State<AddressBook> {
),
controller: controller,
autofocus: true,
),
).workaroundFreezeLinuxMint(),
),
],
),

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import '../../../models/platform_model.dart';
@@ -6,56 +5,104 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart';
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> combinedPeers = {};
class AllPeersLoader {
List<Peer> peers = [];
void mergePeers(Map<String, dynamic> peers) {
if (peers.containsKey("peers")) {
dynamic peerData = peers["peers"];
bool _isPeersLoading = false;
bool _isPeersLoaded = false;
if (peerData is String) {
try {
peerData = jsonDecode(peerData);
} catch (e) {
print("Error decoding peers: $e");
return;
}
}
final String _listenerKey = 'AllPeersLoader';
if (peerData is List) {
for (var peer in peerData) {
if (peer is Map && peer.containsKey("id")) {
String id = peer["id"];
if (!combinedPeers.containsKey(id)) {
combinedPeers[id] = peer;
}
}
}
late void Function(VoidCallback) setState;
bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
bool get isPeersLoaded => _isPeersLoaded;
AllPeersLoader();
void init(void Function(VoidCallback) setState) {
this.setState = setState;
gFFI.recentPeersModel.addListener(_mergeAllPeers);
gFFI.lanPeersModel.addListener(_mergeAllPeers);
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
}
void clear() {
gFFI.recentPeersModel.removeListener(_mergeAllPeers);
gFFI.lanPeersModel.removeListener(_mergeAllPeers);
gFFI.abModel.removePeerUpdateListener(_listenerKey);
gFFI.groupModel.removePeerUpdateListener(_listenerKey);
}
Future<void> getAllPeers() async {
if (!needLoad) {
return;
}
_isPeersLoading = true;
if (gFFI.recentPeersModel.peers.isEmpty) {
bind.mainLoadRecentPeers();
}
if (gFFI.lanPeersModel.peers.isEmpty) {
bind.mainLoadLanPeers();
}
// No need to care about peers from abModel, and group model.
// Because they will pull data in `refreshCurrentUser()` on startup.
final startTime = DateTime.now();
_mergeAllPeers();
final diffTime = DateTime.now().difference(startTime).inMilliseconds;
if (diffTime < 100) {
await Future.delayed(Duration(milliseconds: diffTime));
}
}
void _mergeAllPeers() {
Map<String, dynamic> combinedPeers = {};
for (var p in gFFI.abModel.allPeers()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
}
mergePeers(recentPeers);
mergePeers(lanPeers);
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();
}
}
}
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 = [];
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
}
}
List<Peer> parsedPeers = [];
Set<String> peerIds = combinedPeers.keys.toSet();
for (final peer in gFFI.lanPeersModel.peers) {
if (!peerIds.contains(peer.id)) {
parsedPeers.add(peer);
peerIds.add(peer.id);
}
}
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
for (final peer in gFFI.recentPeersModel.peers) {
if (!peerIds.contains(peer.id)) {
parsedPeers.add(peer);
peerIds.add(peer.id);
}
}
for (final id in gFFI.recentPeersModel.restPeerIds) {
if (!peerIds.contains(id)) {
parsedPeers.add(Peer.fromJson({'id': id}));
peerIds.add(id);
}
}
peers = parsedPeers;
setState(() {
_isPeersLoading = false;
_isPeersLoaded = true;
});
}
return parsedPeers;
}
class AutocompletePeerTile extends StatefulWidget {

View File

@@ -167,7 +167,7 @@ class ChatPage extends StatelessWidget implements PageShape {
);
},
),
);
).workaroundFreezeLinuxMint();
return SelectionArea(child: chat);
}),
],

View File

@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
@@ -71,7 +70,7 @@ void changeIdDialog() {
final rules = [
RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
LengthRangeValidationRule(6, 16),
RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$'))
];
gFFI.dialogManager.show((setState, close, context) {
@@ -140,7 +139,7 @@ void changeIdDialog() {
msg = '';
});
},
),
).workaroundFreezeLinuxMint(),
const SizedBox(
height: 8.0,
),
@@ -201,13 +200,14 @@ void changeWhiteList({Function()? callback}) async {
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
enabled: !isOptFixed,
autofocus: true),
maxLines: null,
decoration: InputDecoration(
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
enabled: !isOptFixed,
autofocus: true)
.workaroundFreezeLinuxMint(),
),
],
),
@@ -287,22 +287,23 @@ Future<String> changeDirectAccessPort(
children: [
Expanded(
child: TextField(
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '21118',
isCollapsed: true,
prefix: Text('$currentIP : '),
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
autofocus: true),
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '21118',
isCollapsed: true,
prefix: Text('$currentIP : '),
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
autofocus: true)
.workaroundFreezeLinuxMint(),
),
],
),
@@ -335,21 +336,22 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
children: [
Expanded(
child: TextField(
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '10',
isCollapsed: true,
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
autofocus: true),
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '10',
isCollapsed: true,
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
autofocus: true)
.workaroundFreezeLinuxMint(),
),
],
),
@@ -409,25 +411,39 @@ class DialogTextField extends StatelessWidget {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: title,
hintText: hintText,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
helperText: helperText,
helperMaxLines: 8,
errorText: errorText,
errorMaxLines: 8,
),
controller: controller,
focusNode: focusNode,
autofocus: true,
obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
maxLength: maxLength,
),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: title,
hintText: hintText,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
helperText: helperText,
helperMaxLines: 8,
),
controller: controller,
focusNode: focusNode,
autofocus: true,
obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
maxLength: maxLength,
),
if (errorText != null)
Align(
alignment: Alignment.centerLeft,
child: SelectableText(
errorText!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
textAlign: TextAlign.left,
).paddingOnly(top: 8, left: 12),
),
],
).workaroundFreezeLinuxMint(),
),
],
).paddingSymmetric(vertical: 4.0);
@@ -1501,7 +1517,7 @@ showAuditDialog(FFI ffi) async {
maxLength: 256,
controller: controller,
focusNode: focusNode,
)),
).workaroundFreezeLinuxMint()),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit)
@@ -1748,7 +1764,7 @@ void renameDialog(
autofocus: true,
decoration: InputDecoration(labelText: translate('Name')),
validator: validator,
),
).workaroundFreezeLinuxMint(),
),
),
// NOT use Offstage to wrap LinearProgressIndicator
@@ -1808,7 +1824,7 @@ void changeBot({Function()? callback}) async {
decoration: InputDecoration(
hintText: translate('Token'),
),
);
).workaroundFreezeLinuxMint();
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
@@ -2178,7 +2194,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
},
),
),
),
).workaroundFreezeLinuxMint(),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),

View File

@@ -678,7 +678,7 @@ Future<bool?> verificationCodeDialog(
labelText: "Email", prefixIcon: Icon(Icons.email)),
readOnly: true,
controller: TextEditingController(text: user?.email),
)),
).workaroundFreezeLinuxMint()),
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
codeField,
/*

View File

@@ -20,8 +20,11 @@ class MyGroup extends StatefulWidget {
}
class _MyGroupState extends State<MyGroup> {
RxString get selectedUser => gFFI.groupModel.selectedUser;
RxString get searchUserText => gFFI.groupModel.searchUserText;
RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup;
RxString get selectedAccessibleItemName =>
gFFI.groupModel.selectedAccessibleItemName;
RxString get searchAccessibleItemNameText =>
gFFI.groupModel.searchAccessibleItemNameText;
static TextEditingController searchUserController = TextEditingController();
@override
@@ -72,7 +75,7 @@ class _MyGroupState extends State<MyGroup> {
child: Container(
width: double.infinity,
height: double.infinity,
child: _buildUserContacts(),
child: _buildLeftList(),
),
)
],
@@ -105,7 +108,7 @@ class _MyGroupState extends State<MyGroup> {
_buildLeftHeader(),
Container(
width: double.infinity,
child: _buildUserContacts(),
child: _buildLeftList(),
)
],
),
@@ -130,7 +133,8 @@ class _MyGroupState extends State<MyGroup> {
child: TextField(
controller: searchUserController,
onChanged: (value) {
searchUserText.value = value;
searchAccessibleItemNameText.value = value;
selectedAccessibleItemName.value = '';
},
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
@@ -145,25 +149,35 @@ class _MyGroupState extends State<MyGroup> {
border: InputBorder.none,
isDense: true,
),
)),
).workaroundFreezeLinuxMint()),
],
);
}
Widget _buildUserContacts() {
Widget _buildLeftList() {
return Obx(() {
final items = gFFI.groupModel.users.where((p0) {
if (searchUserText.isNotEmpty) {
final userItems = gFFI.groupModel.users.where((p0) {
if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name
.toLowerCase()
.contains(searchUserText.value.toLowerCase());
.contains(searchAccessibleItemNameText.value.toLowerCase());
}
return true;
}).toList();
final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name
.toLowerCase()
.contains(searchAccessibleItemNameText.value.toLowerCase());
}
return true;
}).toList();
listView(bool isPortrait) => ListView.builder(
shrinkWrap: isPortrait,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
itemCount: deviceGroupItems.length + userItems.length,
itemBuilder: (context, index) => index < deviceGroupItems.length
? _buildDeviceGroupItem(deviceGroupItems[index])
: _buildUserItem(userItems[index - deviceGroupItems.length]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return Obx(() => stateGlobal.isPortrait.isFalse
? listView(false)
@@ -174,14 +188,16 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserItem(UserPayload user) {
final username = user.name;
return InkWell(onTap: () {
if (selectedUser.value != username) {
selectedUser.value = username;
isSelectedDeviceGroup.value = false;
if (selectedAccessibleItemName.value != username) {
selectedAccessibleItemName.value = username;
} else {
selectedUser.value = '';
selectedAccessibleItemName.value = '';
}
}, child: Obx(
() {
bool selected = selectedUser.value == username;
bool selected = !isSelectedDeviceGroup.value &&
selectedAccessibleItemName.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container(
@@ -238,4 +254,43 @@ class _MyGroupState extends State<MyGroup> {
},
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
}
Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) {
final name = deviceGroup.name;
return InkWell(onTap: () {
isSelectedDeviceGroup.value = true;
if (selectedAccessibleItemName.value != name) {
selectedAccessibleItemName.value = name;
} else {
selectedAccessibleItemName.value = '';
}
}, child: Obx(
() {
bool selected = isSelectedDeviceGroup.value &&
selectedAccessibleItemName.value == name;
return Container(
decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null,
border: Border(
bottom: BorderSide(
width: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1))),
),
child: Container(
child: Row(
children: [
Container(
width: 20,
height: 20,
child: Icon(IconFont.deviceGroupOutline,
color: MyTheme.accent, size: 19),
).marginOnly(right: 4),
Expanded(child: Text(name)),
],
).paddingSymmetric(vertical: 4),
),
);
},
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
}
}

View File

@@ -488,6 +488,7 @@ abstract class BasePeerCard extends StatelessWidget {
BuildContext context,
String title, {
bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false,
bool isRDP = false,
}) {
@@ -502,6 +503,7 @@ abstract class BasePeerCard extends StatelessWidget {
peer,
tab,
isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
);
@@ -530,6 +532,15 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
@protected
MenuEntryBase<String> _viewCameraAction(BuildContext context) {
return _connectCommonAction(
context,
translate('View camera'),
isViewCamera: true,
);
}
@protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction(
@@ -716,18 +727,18 @@ abstract class BasePeerCard extends StatelessWidget {
switch (tab) {
case PeerTabIndex.recent:
await bind.mainRemovePeer(id: id);
await bind.mainLoadRecentPeers();
bind.mainLoadRecentPeers();
break;
case PeerTabIndex.fav:
final favs = (await bind.mainGetFav()).toList();
if (favs.remove(id)) {
await bind.mainStoreFav(favs: favs);
await bind.mainLoadFavPeers();
bind.mainLoadFavPeers();
}
break;
case PeerTabIndex.lan:
await bind.mainRemoveDiscovered(id: id);
await bind.mainLoadLanPeers();
bind.mainLoadLanPeers();
break;
case PeerTabIndex.ab:
await gFFI.abModel.deletePeers([id]);
@@ -880,6 +891,7 @@ class RecentPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
];
final List favs = (await bind.mainGetFav()).toList();
@@ -939,6 +951,7 @@ class FavoritePeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
@@ -992,6 +1005,7 @@ class DiscoveredPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
];
final List favs = (await bind.mainGetFav()).toList();
@@ -1045,6 +1059,7 @@ class AddressBookPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
@@ -1177,6 +1192,7 @@ class MyGroupPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
@@ -1257,7 +1273,7 @@ void _rdpDialog(String id) async {
hintText: '3389'),
controller: portController,
autofocus: true,
),
).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: isDesktop ? 8 : 0),
@@ -1277,7 +1293,7 @@ void _rdpDialog(String id) async {
labelText:
isDesktop ? null : translate('Username')),
controller: userController,
),
).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
@@ -1305,7 +1321,7 @@ void _rdpDialog(String id) async {
? Icons.visibility_off
: Icons.visibility))),
controller: passwordController,
)),
).workaroundFreezeLinuxMint()),
),
],
))
@@ -1398,6 +1414,7 @@ class TagPainter extends CustomPainter {
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
{bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
var password = '';
@@ -1423,6 +1440,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
password: password,
isSharedPassword: isSharedPassword,
isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP);
}

View File

@@ -33,8 +33,8 @@ class PeerTabPage extends StatefulWidget {
class _TabEntry {
final Widget widget;
final Function({dynamic hint}) load;
_TabEntry(this.widget, this.load);
final Function({dynamic hint})? load;
_TabEntry(this.widget, [this.load]);
}
EdgeInsets? _menuPadding() {
@@ -44,21 +44,15 @@ EdgeInsets? _menuPadding() {
class _PeerTabPageState extends State<PeerTabPage>
with SingleTickerProviderStateMixin {
final List<_TabEntry> entries = [
_TabEntry(
RecentPeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadRecentPeers),
_TabEntry(
FavoritePeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadFavPeers),
_TabEntry(
DiscoveredPeersView(
menuPadding: _menuPadding(),
),
bind.mainDiscover),
_TabEntry(RecentPeersView(
menuPadding: _menuPadding(),
)),
_TabEntry(FavoritePeersView(
menuPadding: _menuPadding(),
)),
_TabEntry(DiscoveredPeersView(
menuPadding: _menuPadding(),
)),
_TabEntry(
AddressBook(
menuPadding: _menuPadding(),
@@ -100,7 +94,7 @@ class _PeerTabPageState extends State<PeerTabPage>
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
}
gFFI.peerTabModel.setCurrentTab(tabIndex);
entries[tabIndex].load(hint: false);
entries[tabIndex].load?.call(hint: false);
}
}
@@ -225,7 +219,7 @@ class _PeerTabPageState extends State<PeerTabPage>
child: RefreshWidget(
onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load();
entries[gFFI.peerTabModel.currentTab].load?.call();
}
},
spinning: loading,
@@ -404,7 +398,7 @@ class _PeerTabPageState extends State<PeerTabPage>
for (var p in peers) {
await bind.mainRemovePeer(id: p.id);
}
await bind.mainLoadRecentPeers();
bind.mainLoadRecentPeers();
break;
case 1:
final favs = (await bind.mainGetFav()).toList();
@@ -412,13 +406,13 @@ class _PeerTabPageState extends State<PeerTabPage>
favs.remove(p.id);
}).toList();
await bind.mainStoreFav(favs: favs);
await bind.mainLoadFavPeers();
bind.mainLoadFavPeers();
break;
case 2:
for (var p in peers) {
await bind.mainRemoveDiscovered(id: p.id);
}
await bind.mainLoadLanPeers();
bind.mainLoadLanPeers();
break;
case 3:
await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
@@ -743,7 +737,7 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
border: InputBorder.none,
isDense: true,
),
),
).workaroundFreezeLinuxMint(),
),
// Icon(Icons.close),
IconButton(

View File

@@ -25,13 +25,13 @@ class PeerSortType {
static const String remoteId = 'Remote ID';
static const String remoteHost = 'Remote Host';
static const String username = 'Username';
// static const String status = 'Status';
static const String status = 'Status';
static List<String> values = [
PeerSortType.remoteId,
PeerSortType.remoteHost,
PeerSortType.username,
// PeerSortType.status
PeerSortType.status
];
}
@@ -384,9 +384,9 @@ class _PeersViewState extends State<_PeersView>
peers.sort((p1, p2) =>
p1.username.toLowerCase().compareTo(p2.username.toLowerCase()));
break;
// case PeerSortType.status:
// peers.sort((p1, p2) => p1.online ? -1 : 1);
// break;
case PeerSortType.status:
peers.sort((p1, p2) => p1.online ? -1 : 1);
break;
}
}
@@ -562,14 +562,26 @@ class MyGroupPeerView extends BasePeersView {
);
static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) {
if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
final model = gFFI.groupModel;
if (model.searchAccessibleItemNameText.isNotEmpty) {
final text = model.searchAccessibleItemNameText.value;
final searchPeersOfUser = peer.loginName.contains(text) &&
model.users.any((user) => user.name == peer.loginName);
final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) &&
model.deviceGroups.any((g) => g.name == peer.device_group_name);
if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
return false;
}
}
if (gFFI.groupModel.selectedUser.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.loginName) {
return false;
if (model.selectedAccessibleItemName.isNotEmpty) {
if (model.isSelectedDeviceGroup.value) {
if (model.selectedAccessibleItemName.value != peer.device_group_name) {
return false;
}
} else {
if (model.selectedAccessibleItemName.value != peer.loginName) {
return false;
}
}
}
return true;

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget {
class RawTouchGestureDetectorRegion extends StatefulWidget {
final Widget child;
final FFI ffi;
final bool isCamera;
late final InputModel inputModel = ffi.inputModel;
late final FfiModel ffiModel = ffi.ffiModel;
RawTouchGestureDetectorRegion({
required this.child,
required this.ffi,
this.isCamera = false,
});
@override
@@ -187,6 +189,11 @@ class _RawTouchGestureDetectorRegionState
return;
}
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
if (ffiModel.isPeerMobile) {
await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
await inputModel.tapDown(MouseButtons.left);
}
}
}
@@ -204,15 +211,31 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!ffi.ffiModel.isPeerMobile) {
if (handleTouch) {
final isMoved = await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
if (!isMoved) {
return;
}
}
await inputModel.tap(MouseButtons.right);
} else {
// It's better to send a message to tell the controlled device that the long press event is triggered.
// We're now using a `TimerTask` in `InputService.kt` to decide whether to trigger the long press event.
// It's not accurate and it's better to use the same detection logic in the controlling side.
}
}
onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async {
if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
final isMoved = await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
if (!isMoved) {
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
return;
}
}
if (!ffi.ffiModel.isPeerMobile) {
await inputModel.tap(MouseButtons.right);
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
}
}
@@ -339,7 +362,9 @@ class _RawTouchGestureDetectorRegionState
if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
await inputModel.sendMouse('up', MouseButtons.left);
if (handleTouch) {
await inputModel.sendMouse('up', MouseButtons.left);
}
}
// scale + pan event
@@ -359,6 +384,7 @@ class _RawTouchGestureDetectorRegionState
_scale = d.scale;
if (scale != 0) {
if (widget.isCamera) return;
await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
@@ -379,6 +405,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if ((isDesktop || isWebDesktop)) {
if (widget.isCamera) return;
await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
@@ -430,7 +457,8 @@ class _RawTouchGestureDetectorRegionState
instance
..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp
..onLongPress = onLongPress;
..onLongPress = onLongPress
..onLongPressMoveUpdate = onLongPressMoveUpdate;
}),
// Customized
HoldTapMoveGestureRecognizer:
@@ -512,3 +540,46 @@ class RawPointerMouseRegion extends StatelessWidget {
);
}
}
class CameraRawPointerMouseRegion extends StatelessWidget {
final InputModel inputModel;
final Widget child;
final PointerEnterEventListener? onEnter;
final PointerExitEventListener? onExit;
final PointerDownEventListener? onPointerDown;
final PointerUpEventListener? onPointerUp;
CameraRawPointerMouseRegion({
this.onEnter,
this.onExit,
this.onPointerDown,
this.onPointerUp,
required this.inputModel,
required this.child,
});
@override
Widget build(BuildContext context) {
return Listener(
onPointerHover: (evt) {
final offset = evt.position;
double x = offset.dx;
double y = max(0.0, offset.dy);
inputModel.handlePointerDevicePos(
kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault);
},
onPointerDown: (evt) {
onPointerDown?.call(evt);
},
onPointerUp: (evt) {
onPointerUp?.call(evt);
},
child: MouseRegion(
cursor: MouseCursor.defer,
onEnter: onEnter,
onExit: onExit,
child: child,
),
);
}
}

View File

@@ -89,10 +89,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
List<TTextMenu> v = [];
// elevation
if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
if (isDefaultConn &&
perms['keyboard'] != false &&
ffi.elevationModel.showRequestMenu) {
v.add(
TTextMenu(
child: Text(translate('Request Elevation')),
@@ -101,7 +104,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// osAccount / osPassword
if (perms['keyboard'] != false) {
if (isDefaultConn && perms['keyboard'] != false) {
v.add(
TTextMenu(
child: Row(children: [
@@ -130,7 +133,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// paste
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
if (isDefaultConn &&
pi.platform != kPeerPlatformAndroid &&
perms['keyboard'] != false) {
v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')),
onPressed: () async {
@@ -142,43 +147,53 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
}));
}
// reset canvas
if (isMobile) {
if (isDefaultConn && isMobile) {
v.add(TTextMenu(
child: Text(translate('Reset canvas')),
onPressed: () => ffi.cursorModel.reset()));
}
connectWithToken(
{required bool isFileTransfer, required bool isTcpTunneling}) {
{bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false}) {
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
connect(context, id,
isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
connToken: connToken);
}
// transferFile
if (isDesktop) {
if (isDefaultConn && isDesktop) {
v.add(
TTextMenu(
child: Text(translate('Transfer file')),
onPressed: () =>
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
onPressed: () => connectWithToken(isFileTransfer: true)),
);
}
// viewCamera
if (isDefaultConn && isDesktop) {
v.add(
TTextMenu(
child: Text(translate('View camera')),
onPressed: () => connectWithToken(isViewCamera: true)),
);
}
// tcpTunneling
if (isDesktop) {
if (isDefaultConn && isDesktop) {
v.add(
TTextMenu(
child: Text(translate('TCP tunneling')),
onPressed: () =>
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
onPressed: () => connectWithToken(isTcpTunneling: true)),
);
}
// note
if (bind
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
.isNotEmpty) {
if (isDefaultConn &&
bind
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
.isNotEmpty) {
v.add(
TTextMenu(
child: Text(translate('Note')),
@@ -186,11 +201,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// divider
if (isDesktop || isWebDesktop) {
if (isDefaultConn && (isDesktop || isWebDesktop)) {
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
}
// ctrlAltDel
if (!ffiModel.viewOnly &&
if (isDefaultConn &&
!ffiModel.viewOnly &&
ffiModel.keyboard &&
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
v.add(
@@ -200,7 +216,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// restart
if (perms['restart'] != false &&
if (isDefaultConn &&
perms['restart'] != false &&
(pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) {
@@ -212,7 +229,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// insertLock
if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) {
if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) {
v.add(
TTextMenu(
child: Text(translate('Insert Lock')),
@@ -220,7 +237,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// blockUserInput
if (ffi.ffiModel.keyboard &&
if (isDefaultConn &&
ffi.ffiModel.keyboard &&
ffi.ffiModel.permissions['block_input'] != false &&
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
{
@@ -236,12 +254,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
}));
}
// switchSides
if (isDesktop &&
if (isDefaultConn &&
isDesktop &&
ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS &&
versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetDefaultSessionsCount(id: id) == 1) {
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
v.add(TTextMenu(
child: Text(translate('Switch Sides')),
onPressed: () =>
@@ -523,6 +542,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
// show quality monitor
final option = 'show-quality-monitor';
@@ -535,7 +555,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
},
child: Text(translate('Show quality monitor'))));
// mute
if (perms['audio'] != false) {
if (isDefaultConn && perms['audio'] != false) {
final option = 'disable-audio';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
@@ -556,7 +576,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
if (ffiModel.keyboard &&
if (isDefaultConn &&
ffiModel.keyboard &&
perms['file'] != false &&
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
final enabled = !ffiModel.viewOnly;
@@ -574,7 +595,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Enable file copy and paste'))));
}
// disable clipboard
if (ffiModel.keyboard && perms['clipboard'] != false) {
if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) {
final enabled = !ffiModel.viewOnly;
final option = 'disable-clipboard';
var value =
@@ -591,7 +612,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Disable clipboard'))));
}
// lock after session end
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) {
final enabled = !ffiModel.viewOnly;
final option = 'lock-after-session-end';
final value =
@@ -656,12 +677,12 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('True color (4:4:4)'))));
}
if (isMobile) {
if (isDefaultConn && isMobile) {
v.addAll(toolbarKeyboardToggles(ffi));
}
// view mode (mobile only, desktop is in keyboard menu)
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu(
value: ffiModel.viewOnly,
onChanged: (value) async {

View File

@@ -27,6 +27,7 @@ const String kPlatformAdditionsAmyuniVirtualDisplays =
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl";
const String kPlatformAdditionsSupportViewCamera = "support_view_camera";
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
@@ -44,6 +45,7 @@ const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopViewCamera = "view camera";
const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top";
@@ -58,6 +60,7 @@ const String kWindowConnect = "connect";
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewViewCamera = "new_view_camera";
const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session";
@@ -75,6 +78,7 @@ const String kOptionScrollStyle = "scroll_style";
const String kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render";
const String kOptionD3DRender = "allow-d3d-render";
const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows";
const String kOptionForceAlwaysRelay = "force-always-relay";
@@ -94,9 +98,11 @@ const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard";
// "Settings -> Security -> Permissions"
const String kOptionEnableRemotePrinter = "enable-remote-printer";
const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio";
const String kOptionEnableCamera = "enable-camera";
const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input";
@@ -214,6 +220,14 @@ const double kDefaultQuality = 50;
const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000;
const String kKeyPrinterIncommingJobAction = 'printer-incomming-job-action';
const String kValuePrinterIncomingJobDismiss = 'dismiss';
const String kValuePrinterIncomingJobDefault = '';
const String kValuePrinterIncomingJobSelected = 'selected';
const String kKeyPrinterSelected = 'printer-selected-name';
const String kKeyPrinterSave = 'allow-printer-dialog-save';
const String kKeyPrinterAllowAutoPrint = 'allow-printer-auto-print';
double kNewWindowOffset = isWindows
? 56.0
: isLinux
@@ -248,7 +262,7 @@ const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
const kWindowResizeEdgeSize = 5.0;
const kWindowBorderWidth = 1.0;
final kWindowBorderWidth = isWindows ? 0.0 : 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0;
@@ -575,4 +589,4 @@ extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this);
}
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';

View File

@@ -2,10 +2,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -17,7 +19,7 @@ import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart';
import '../widgets/button.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
class OnlineStatusWidget extends StatefulWidget {
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
@@ -39,7 +41,7 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
double? get height => bind.isIncomingOnly() ? null : em * 3;
void onUsePublicServerGuide() {
const url = "https://rustdesk.com/pricing.html";
const url = "https://rustdesk.com/pricing";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
@@ -179,6 +181,9 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
stateGlobal.svcStatus.value = SvcStatus.notReady;
}
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
try {
stateGlobal.videoConnCount.value = status['video_conn_count'] as int;
} catch (_) {}
}
}
@@ -197,18 +202,25 @@ class _ConnectionPageState extends State<ConnectionPage>
final _idController = IDTextEditingController();
final RxBool _idInputFocused = false.obs;
final FocusNode _idFocusNode = FocusNode();
final TextEditingController _idEditingController = TextEditingController();
String selectedConnectionType = 'Connect';
bool isWindowMinimized = false;
List<Peer> peers = [];
bool isPeersLoading = false;
bool isPeersLoaded = false;
final AllPeersLoader _allPeersLoader = AllPeersLoader();
// https://github.com/flutter/flutter/issues/157244
Iterable<Peer> _autocompleteOpts = [];
final _menuOpen = false.obs;
@override
void initState() {
super.initState();
_allPeersLoader.init(setState);
_idFocusNode.addListener(onFocusChanged);
if (_idController.text.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@@ -219,6 +231,7 @@ class _ConnectionPageState extends State<ConnectionPage>
}
});
}
Get.put<TextEditingController>(_idEditingController);
Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this);
}
@@ -227,6 +240,10 @@ class _ConnectionPageState extends State<ConnectionPage>
void dispose() {
_idController.dispose();
windowManager.removeListener(this);
_allPeersLoader.clear();
_idFocusNode.removeListener(onFocusChanged);
_idFocusNode.dispose();
_idEditingController.dispose();
if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>();
}
@@ -270,6 +287,20 @@ class _ConnectionPageState extends State<ConnectionPage>
bind.mainOnMainWindowClose();
}
void onFocusChanged() {
_idInputFocused.value = _idFocusNode.hasFocus;
if (_idFocusNode.hasFocus) {
if (_allPeersLoader.needLoad) {
_allPeersLoader.getAllPeers();
}
final textLength = _idEditingController.value.text.length;
// Select all to facilitate removing text, just following the behavior of address input of chrome.
_idEditingController.selection =
TextSelection(baseOffset: 0, extentOffset: textLength);
}
}
@override
Widget build(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
@@ -296,21 +327,10 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Callback for the connect button.
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) {
var id = _idController.id;
connect(context, id, isFileTransfer: isFileTransfer);
}
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
connect(context, id,
isFileTransfer: isFileTransfer, isViewCamera: isViewCamera);
}
/// UI for the remote ID TextField.
@@ -329,11 +349,12 @@ class _ConnectionPageState extends State<ConnectionPage>
Row(
children: [
Expanded(
child: Autocomplete<Peer>(
child: RawAutocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
_autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
} else if (_allPeersLoader.peers.isEmpty &&
!_allPeersLoader.isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
@@ -347,6 +368,7 @@ class _ConnectionPageState extends State<ConnectionPage>
rdpPort: '',
rdpUsername: '',
loginName: '',
device_group_name: '',
);
_autocompleteOpts = [emptyPeer];
} else {
@@ -359,7 +381,7 @@ class _ConnectionPageState extends State<ConnectionPage>
);
}
String textToFind = textEditingValue.text.toLowerCase();
_autocompleteOpts = peers
_autocompleteOpts = _allPeersLoader.peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@@ -373,25 +395,16 @@ class _ConnectionPageState extends State<ConnectionPage>
}
return _autocompleteOpts;
},
focusNode: _idFocusNode,
textEditingController: _idEditingController,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(fieldTextEditingController);
fieldFocusNode.addListener(() async {
_idInputFocused.value = fieldFocusNode.hasFocus;
if (fieldFocusNode.hasFocus && !isPeersLoading) {
_fetchPeers();
}
});
final textLength =
fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection =
TextSelection(baseOffset: 0, extentOffset: textLength);
updateTextAndPreserveSelection(
fieldTextEditingController, _idController.text);
return Obx(() => TextField(
autocorrect: false,
enableSuggestions: false,
@@ -421,7 +434,7 @@ class _ConnectionPageState extends State<ConnectionPage>
onSubmitted: (_) {
onConnect();
},
));
).workaroundFreezeLinuxMint());
},
onSelected: (option) {
setState(() {
@@ -464,7 +477,8 @@ class _ConnectionPageState extends State<ConnectionPage>
maxHeight: maxHeight,
maxWidth: 319,
),
child: peers.isEmpty && isPeersLoading
child: _allPeersLoader.peers.isEmpty &&
!_allPeersLoader.isPeersLoaded
? Container(
height: 80,
child: Center(
@@ -494,21 +508,87 @@ class _ConnectionPageState extends State<ConnectionPage>
),
Padding(
padding: const EdgeInsets.only(top: 13.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Button(
isOutline: true,
onTap: () => onConnect(isFileTransfer: true),
text: "Transfer file",
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
SizedBox(
height: 28.0,
child: ElevatedButton(
onPressed: () {
onConnect();
},
child: Text(translate("Connect")),
),
const SizedBox(
width: 17,
),
const SizedBox(width: 8),
Container(
height: 28.0,
width: 28.0,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8),
),
Button(onTap: onConnect, text: "Connect"),
],
),
)
child: Center(
child: Obx(() {
var offset = Offset(0, 0);
return InkWell(
child: _menuOpen.value
? Transform.rotate(
angle: pi,
child: Icon(IconFont.more, size: 14),
)
: Icon(IconFont.more, size: 14),
onTapDown: (e) {
offset = e.globalPosition;
},
onTap: () async {
_menuOpen.value = true;
final x = offset.dx;
final y = offset.dy;
await mod_menu
.showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: [
(
'Transfer file',
() => onConnect(isFileTransfer: true)
),
(
'View camera',
() => onConnect(isViewCamera: true)
),
]
.map((e) => MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate(e.$1),
style: style,
),
proc: () => e.$2(),
padding: EdgeInsets.symmetric(
horizontal: kDesktopMenuPadding.left),
dismissOnClicked: true,
))
.map((e) => e.build(
context,
const MenuConfig(
commonColor:
CustomPopupMenuTheme.commonColor,
height: CustomPopupMenuTheme.height,
dividerHeight: CustomPopupMenuTheme
.dividerHeight)))
.expand((i) => i)
.toList(),
elevation: 8,
)
.then((_) {
_menuOpen.value = false;
});
},
);
}),
),
),
]),
),
],
),
),

View File

@@ -35,7 +35,7 @@ class DesktopHomePage extends StatefulWidget {
const borderColor = Color(0xFF2F65BA);
class _DesktopHomePageState extends State<DesktopHomePage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _leftPaneScrollController = ScrollController();
@override
@@ -51,6 +51,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bool isCardClosed = false;
final RxBool _editHover = false.obs;
final RxBool _block = false.obs;
final GlobalKey _childKey = GlobalKey();
@@ -58,14 +59,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget build(BuildContext context) {
super.build(context);
final isIncomingOnly = bind.isIncomingOnly();
return Row(
return _buildBlock(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildLeftPane(context),
if (!isIncomingOnly) const VerticalDivider(width: 1),
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
],
);
));
}
Widget _buildBlock({required Widget child}) {
return buildRemoteBlock(
block: _block, mask: true, use: canBeBlocked, child: child);
}
Widget buildLeftPane(BuildContext context) {
@@ -127,12 +134,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
color: Theme.of(context).colorScheme.background,
child: Stack(
children: [
SingleChildScrollView(
controller: _leftPaneScrollController,
child: Column(
key: _childKey,
children: children,
),
Column(
children: [
SingleChildScrollView(
controller: _leftPaneScrollController,
child: Column(
key: _childKey,
children: children,
),
),
Expanded(child: Container())
],
),
if (isOutgoingOnly)
Positioned(
@@ -230,7 +242,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
style: TextStyle(
fontSize: 22,
),
),
).workaroundFreezeLinuxMint(),
),
)
],
@@ -326,7 +338,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
EdgeInsets.only(top: 14, bottom: 10),
),
style: TextStyle(fontSize: 15),
),
).workaroundFreezeLinuxMint(),
),
),
if (showOneTime)
@@ -423,7 +435,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainUriPrefixSync().contains('rustdesk')) {
return buildInstallCard(
"Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
"${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
@@ -763,6 +775,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
await connectMainDesktop(
call.arguments['id'],
isFileTransfer: call.arguments['isFileTransfer'],
isViewCamera: call.arguments['isViewCamera'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
password: call.arguments['password'],
@@ -777,9 +790,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} catch (e) {
debugPrint("Failed to parse window id '${call.arguments}': $e");
}
if (windowId != null) {
WindowType? windowType;
try {
windowType = WindowType.values.byName(args[3]);
} catch (e) {
debugPrint("Failed to parse window type '${call.arguments}': $e");
}
if (windowId != null && windowType != null) {
await rustDeskWinManager.moveTabToNewWindow(
windowId, args[1], args[2]);
windowId, args[1], args[2], windowType);
}
} else if (call.method == kWindowEventOpenMonitorSession) {
final args = jsonDecode(call.arguments);
@@ -787,9 +806,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final peerId = args['peer_id'] as String;
final display = args['display'] as int;
final displayCount = args['display_count'] as int;
final windowType = args['window_type'] as int;
final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect);
windowId, peerId, display, displayCount, screenRect, windowType);
} else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments);
if (windowId != null) {
@@ -805,6 +825,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_updateWindowSize();
});
}
WidgetsBinding.instance.addObserver(this);
}
_updateWindowSize() {
@@ -826,13 +847,18 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_uniLinksSubscription?.cancel();
Get.delete<RxBool>(tag: 'stop-service');
_updateTimer?.cancel();
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
}
}
Widget buildPluginEntry() {
final entries = PluginUiManager.instance.entries.entries;
return Offstage(
@@ -923,7 +949,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
},
maxLength: maxLength,
),
).workaroundFreezeLinuxMint(),
),
],
),
@@ -950,7 +976,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
},
maxLength: maxLength,
),
).workaroundFreezeLinuxMint(),
),
],
),

View File

@@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/printer_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/manager.dart';
@@ -55,6 +56,7 @@ enum SettingsTabKey {
display,
plugin,
account,
printer,
about,
}
@@ -74,6 +76,7 @@ class DesktopSettingPage extends StatefulWidget {
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account,
if (isWindows) SettingsTabKey.printer,
SettingsTabKey.about,
];
@@ -107,13 +110,20 @@ class DesktopSettingPage extends StatefulWidget {
}
class _DesktopSettingPageState extends State<DesktopSettingPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
with
TickerProviderStateMixin,
AutomaticKeepAliveClientMixin,
WidgetsBindingObserver {
late PageController controller;
late Rx<SettingsTabKey> selectedTab;
@override
bool get wantKeepAlive => true;
final RxBool _block = false.obs;
final RxBool _canBeBlocked = false.obs;
Timer? _videoConnTimer;
_DesktopSettingPageState(SettingsTabKey initialTabkey) {
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
if (initialIndex == -1) {
@@ -133,11 +143,34 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_videoConnTimer =
periodic_immediate(Duration(milliseconds: 1000), () async {
if (!mounted) {
return;
}
_canBeBlocked.value = await canBeBlocked();
});
}
@override
void dispose() {
super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
WidgetsBinding.instance.removeObserver(this);
_videoConnTimer?.cancel();
}
List<_TabInfo> _settingTabs() {
@@ -168,6 +201,10 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
break;
case SettingsTabKey.printer:
settingTabs
.add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print));
break;
case SettingsTabKey.about:
settingTabs
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
@@ -199,6 +236,9 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
case SettingsTabKey.account:
children.add(const _Account());
break;
case SettingsTabKey.printer:
children.add(const _Printer());
break;
case SettingsTabKey.about:
children.add(const _About());
break;
@@ -207,12 +247,35 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
return children;
}
Widget _buildBlock({required List<Widget> children}) {
// check both mouseMoveTime and videoConnCount
return Obx(() {
final videoConnBlock =
_canBeBlocked.value && stateGlobal.videoConnCount > 0;
return Stack(children: [
buildRemoteBlock(
block: _block,
mask: false,
use: canBeBlocked,
child: preventMouseKeyBuilder(
child: Row(children: children),
block: videoConnBlock,
),
),
if (videoConnBlock)
Container(
color: Colors.black.withOpacity(0.5),
)
]);
});
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Row(
body: _buildBlock(
children: <Widget>[
SizedBox(
width: _kTabWidth,
@@ -443,6 +506,16 @@ class _GeneralState extends State<_General> {
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
),
),
if (isWindows)
Tooltip(
message: translate('d3d_render_tip'),
child: _OptionCheckBox(
context,
"Use D3D rendering",
kOptionD3DRender,
isServer: false,
),
),
if (!isWeb && !bind.isCustomClient())
_OptionCheckBox(
context,
@@ -554,7 +627,6 @@ class _GeneralState extends State<_General> {
bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false;
// canLaunchUrl blocked on windows portable, user SYSTEM
return {
'user_dir': user_dir,
'root_dir': root_dir,
@@ -706,8 +778,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
locked = false;
setState(() => {});
}),
AbsorbPointer(
absorbing: locked,
preventMouseKeyBuilder(
block: locked,
child: Column(children: [
permissions(context),
password(context),
@@ -901,6 +973,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(
context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
enabled: enabled, fakeValue: fakeValue),
if (isWindows)
_OptionCheckBox(
context, 'Enable remote printer', kOptionEnableRemotePrinter,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
@@ -908,6 +984,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable TCP tunneling', kOptionEnableTunnel,
enabled: enabled, fakeValue: fakeValue),
@@ -1136,7 +1214,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value &&
@@ -1293,7 +1371,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed:
@@ -1374,8 +1452,8 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
locked = false;
setState(() => {});
}),
AbsorbPointer(
absorbing: locked,
preventMouseKeyBuilder(
block: locked,
child: Column(children: [
network(context),
]),
@@ -1817,6 +1895,153 @@ class _PluginState extends State<_Plugin> {
}
}
class _Printer extends StatefulWidget {
const _Printer({super.key});
@override
State<_Printer> createState() => __PrinterState();
}
class __PrinterState extends State<_Printer> {
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
return ListView(controller: scrollController, children: [
outgoing(context),
incomming(context),
]).marginOnly(bottom: _kListViewBottomMargin);
}
Widget outgoing(BuildContext context) {
final isSupportPrinterDriver =
bind.mainGetCommonSync(key: 'is-support-printer-driver') == 'true';
Widget tipOsNotSupported() {
return Align(
alignment: Alignment.topLeft,
child: Text(translate('printer-os-requirement-tip')),
).marginOnly(left: _kCardLeftMargin);
}
Widget tipClientNotInstalled() {
return Align(
alignment: Alignment.topLeft,
child:
Text(translate('printer-requires-installed-{$appName}-client-tip')),
).marginOnly(left: _kCardLeftMargin);
}
Widget tipPrinterNotInstalled() {
final failedMsg = ''.obs;
platformFFI.registerEventHandler(
'install-printer-res', 'install-printer-res', (evt) async {
if (evt['success'] as bool) {
setState(() {});
} else {
failedMsg.value = evt['msg'] as String;
}
}, replace: true);
return Column(children: [
Obx(
() => failedMsg.value.isNotEmpty
? Offstage()
: Align(
alignment: Alignment.topLeft,
child: Text(translate('printer-{$appName}-not-installed-tip'))
.marginOnly(bottom: 10.0),
),
),
Obx(
() => failedMsg.value.isEmpty
? Offstage()
: Align(
alignment: Alignment.topLeft,
child: Text(failedMsg.value,
style: DefaultTextStyle.of(context)
.style
.copyWith(color: Colors.red))
.marginOnly(bottom: 10.0)),
),
_Button('Install {$appName} Printer', () {
failedMsg.value = '';
bind.mainSetCommon(key: 'install-printer', value: '');
})
]).marginOnly(left: _kCardLeftMargin, bottom: 2.0);
}
Widget tipReady() {
return Align(
alignment: Alignment.topLeft,
child: Text(translate('printer-{$appName}-ready-tip')),
).marginOnly(left: _kCardLeftMargin);
}
final installed = bind.mainIsInstalled();
// `is-printer-installed` may fail, but it's rare case.
// Add additional error message here if it's really needed.
final driver_installed =
bind.mainGetCommonSync(key: 'is-printer-installed') == 'true';
final List<Widget> children = [];
if (!isSupportPrinterDriver) {
children.add(tipOsNotSupported());
} else {
children.addAll([
if (!installed) tipClientNotInstalled(),
if (installed && !driver_installed) tipPrinterNotInstalled(),
if (installed && driver_installed) tipReady()
]);
}
return _Card(title: 'Outgoing Print Jobs', children: children);
}
Widget incomming(BuildContext context) {
onRadioChanged(String value) async {
await bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction, value: value);
setState(() {});
}
PrinterOptions printerOptions = PrinterOptions.load();
return _Card(title: 'Incomming Print Jobs', children: [
_Radio(context,
value: kValuePrinterIncomingJobDismiss,
groupValue: printerOptions.action,
label: 'Dismiss',
onChanged: onRadioChanged),
_Radio(context,
value: kValuePrinterIncomingJobDefault,
groupValue: printerOptions.action,
label: 'use-the-default-printer-tip',
onChanged: onRadioChanged),
_Radio(context,
value: kValuePrinterIncomingJobSelected,
groupValue: printerOptions.action,
label: 'use-the-selected-printer-tip',
onChanged: onRadioChanged),
if (printerOptions.printerNames.isNotEmpty)
ComboBox(
initialKey: printerOptions.printerName,
keys: printerOptions.printerNames,
values: printerOptions.printerNames,
enabled: printerOptions.action == kValuePrinterIncomingJobSelected,
onChanged: (value) async {
await bind.mainSetLocalOption(
key: kKeyPrinterSelected, value: value);
setState(() {});
},
).marginOnly(left: 10),
_OptionCheckBox(
context,
'auto-print-tip',
kKeyPrinterAllowAutoPrint,
isServer: false,
enabled: printerOptions.action != kValuePrinterIncomingJobDismiss,
)
]);
}
}
class _About extends StatefulWidget {
const _About({Key? key}) : super(key: key);
@@ -2259,7 +2484,7 @@ _LabeledTextField(
style: TextStyle(
color: disabledTextColor(context, enabled),
),
),
).workaroundFreezeLinuxMint(),
],
),
],
@@ -2438,7 +2663,7 @@ void changeSocks5Proxy() async {
controller: proxyController,
autofocus: true,
enabled: !isOptFixed,
),
).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: 8),
@@ -2458,7 +2683,7 @@ void changeSocks5Proxy() async {
labelText: isMobile ? translate('Username') : null,
),
enabled: !isOptFixed,
),
).workaroundFreezeLinuxMint(),
),
],
).marginOnly(bottom: 8),
@@ -2484,7 +2709,7 @@ void changeSocks5Proxy() async {
controller: pwdController,
enabled: !isOptFixed,
maxLength: bind.mainMaxEncryptLen(),
)),
).workaroundFreezeLinuxMint()),
),
],
),

View File

@@ -37,13 +37,9 @@ class DesktopTabPage extends StatefulWidget {
}
}
class _DesktopTabPageState extends State<DesktopTabPage>
with WidgetsBindingObserver {
class _DesktopTabPageState extends State<DesktopTabPage> {
final tabController = DesktopTabController(tabType: DesktopTabType.main);
final RxBool _block = false.obs;
// bool mouseIn = false;
_DesktopTabPageState() {
RemoteCountState.init();
Get.put<DesktopTabController>(tabController);
@@ -69,19 +65,10 @@ class _DesktopTabPageState extends State<DesktopTabPage>
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
WidgetsBinding.instance.addObserver(this);
}
/*
@@ -97,7 +84,6 @@ class _DesktopTabPageState extends State<DesktopTabPage>
@override
void dispose() {
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>();
super.dispose();
@@ -119,7 +105,6 @@ class _DesktopTabPageState extends State<DesktopTabPage>
isClose: false,
),
),
blockTab: _block,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget

View File

@@ -768,7 +768,7 @@ class _FileManagerViewState extends State<FileManagerView> {
),
controller: name,
autofocus: true,
),
).workaroundFreezeLinuxMint(),
],
),
actions: [
@@ -1657,7 +1657,7 @@ class _FileManagerViewState extends State<FileManagerView> {
onChanged: _locationStatus.value == LocationStatus.fileSearchBar
? (searchText) => onSearchText(searchText, isLocal)
: null,
),
).workaroundFreezeLinuxMint(),
)
],
);

View File

@@ -103,11 +103,13 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
));
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
: workaroundWindowBorder(
context,
Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: SubWindowDragToResizeArea(

View File

@@ -65,6 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
late final TextEditingController controller;
final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs;
final RxBool printer = true.obs;
final RxBool showProgress = false.obs;
final RxBool btnEnabled = true.obs;
@@ -79,6 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
printer.value = installOptions['PRINTER'] != '0';
}
@override
@@ -147,7 +149,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
),
).marginOnly(right: 10),
).workaroundFreezeLinuxMint().marginOnly(right: 10),
),
Obx(
() => OutlinedButton.icon(
@@ -161,7 +163,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
).marginSymmetric(vertical: 2 * em),
Option(startmenu, label: 'Create start menu shortcuts')
.marginOnly(bottom: 7),
Option(desktopicon, label: 'Create desktop icon'),
Option(desktopicon, label: 'Create desktop icon')
.marginOnly(bottom: 7),
Option(printer, label: 'Install {$appName} Printer'),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
@@ -253,6 +257,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
String args = '';
if (startmenu.value) args += ' startmenu';
if (desktopicon.value) args += ' desktopicon';
if (printer.value) args += ' printer';
bind.installInstallMe(options: args, path: controller.text);
}

View File

@@ -238,7 +238,7 @@ class _PortForwardPageState extends State<PortForwardPage>
inputFormatters: inputFormatters,
decoration: InputDecoration(
hintText: hint,
))),
)).workaroundFreezeLinuxMint()),
);
}

View File

@@ -118,11 +118,13 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
backgroundColor: Theme.of(context).colorScheme.background,
body: child),
)
: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
: workaroundWindowBorder(
context,
Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(

View File

@@ -212,14 +212,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
);
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
));
: workaroundWindowBorder(
context,
Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
@@ -267,8 +269,10 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
style: style,
),
proc: () async {
await DesktopMultiWindow.invokeMethod(kMainWindowId,
kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId');
await DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventMoveTabToNewWindow,
'${windowId()},$key,$sessionId,RemoteDesktop');
cancelFunc();
},
padding: padding,
@@ -415,8 +419,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(
windowId(), id!, prePeerCount, display, screenRect);
await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
WindowType.RemoteDesktop, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});

View File

@@ -88,12 +88,14 @@ class _DesktopServerPageState extends State<DesktopServerPage>
);
return isLinux
? buildVirtualWindowFrame(context, body)
: Container(
decoration: BoxDecoration(
border:
Border.all(color: MyTheme.color(context).border!)),
child: body,
);
: workaroundWindowBorder(
context,
Container(
decoration: BoxDecoration(
border:
Border.all(color: MyTheme.color(context).border!)),
child: body,
));
},
),
);
@@ -110,7 +112,8 @@ class ConnectionManager extends StatefulWidget {
class ConnectionManagerState extends State<ConnectionManager>
with WidgetsBindingObserver {
final RxBool _block = false.obs;
final RxBool _controlPageBlock = false.obs;
final RxBool _sidePageBlock = false.obs;
ConnectionManagerState() {
gFFI.serverModel.tabController.onSelected = (client_id_str) {
@@ -139,7 +142,8 @@ class ConnectionManagerState extends State<ConnectionManager>
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (!allowRemoteCMModification()) {
shouldBeBlocked(_block, null);
shouldBeBlocked(_controlPageBlock, null);
shouldBeBlocked(_sidePageBlock, null);
}
}
}
@@ -192,7 +196,6 @@ class ConnectionManagerState extends State<ConnectionManager>
selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100,
tail: null, //buildScrollJumper(),
blockTab: allowRemoteCMModification() ? null : _block,
tabBuilder: (key, icon, label, themeConf) {
final client = serverModel.clients
.firstWhereOrNull((client) => client.id.toString() == key);
@@ -237,13 +240,20 @@ class ConnectionManagerState extends State<ConnectionManager>
? buildSidePage()
: buildRemoteBlock(
child: buildSidePage(),
block: _block,
block: _sidePageBlock,
mask: true),
)),
SizedBox(
width: realClosedWidth,
child:
SizedBox(width: realClosedWidth, child: pageView)),
child: SizedBox(
width: realClosedWidth,
child: allowRemoteCMModification()
? pageView
: buildRemoteBlock(
child: _buildKeyEventBlock(pageView),
block: _controlPageBlock,
mask: false,
))),
]);
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
@@ -268,6 +278,10 @@ class ConnectionManagerState extends State<ConnectionManager>
}
}
Widget _buildKeyEventBlock(Widget child) {
return ExcludeFocus(child: child, excluding: true);
}
Widget buildTitleBar() {
return SizedBox(
height: kDesktopRemoteTabBarHeight,
@@ -339,7 +353,9 @@ Widget buildConnectionCard(Client client) {
key: ValueKey(client.id),
children: [
_CmHeader(client: client),
client.type_() != ClientType.remote || client.disconnected
client.type_() == ClientType.file ||
client.type_() == ClientType.portForward ||
client.disconnected
? Offstage()
: _PrivilegeBoard(client: client),
Expanded(
@@ -512,7 +528,8 @@ class _CmHeaderState extends State<_CmHeader>
Offstage(
offstage: !client.authorized ||
(client.type_() != ClientType.remote &&
client.type_() != ClientType.file),
client.type_() != ClientType.file &&
client.type_() != ClientType.camera),
child: IconButton(
onPressed: () => checkClickTime(client.id, () {
if (client.type_() == ClientType.file) {
@@ -613,96 +630,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
padding: EdgeInsets.symmetric(horizontal: spacing),
mainAxisSpacing: spacing,
crossAxisSpacing: spacing,
children: [
buildPermissionIcon(
client.keyboard,
Icons.keyboard,
(enabled) {
bind.cmSwitchPermission(
connId: client.id, name: "keyboard", enabled: enabled);
setState(() {
client.keyboard = enabled;
});
},
translate('Enable keyboard/mouse'),
),
buildPermissionIcon(
client.clipboard,
Icons.assignment_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id, name: "clipboard", enabled: enabled);
setState(() {
client.clipboard = enabled;
});
},
translate('Enable clipboard'),
),
buildPermissionIcon(
client.audio,
Icons.volume_up_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id, name: "audio", enabled: enabled);
setState(() {
client.audio = enabled;
});
},
translate('Enable audio'),
),
buildPermissionIcon(
client.file,
Icons.upload_file_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id, name: "file", enabled: enabled);
setState(() {
client.file = enabled;
});
},
translate('Enable file copy and paste'),
),
buildPermissionIcon(
client.restart,
Icons.restart_alt_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id, name: "restart", enabled: enabled);
setState(() {
client.restart = enabled;
});
},
translate('Enable remote restart'),
),
buildPermissionIcon(
client.recording,
Icons.videocam_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id, name: "recording", enabled: enabled);
setState(() {
client.recording = enabled;
});
},
translate('Enable recording session'),
),
// only windows support block input
if (isWindows)
buildPermissionIcon(
client.blockInput,
Icons.block,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "block_input",
enabled: enabled);
setState(() {
client.blockInput = enabled;
});
},
translate('Enable blocking user input'),
)
],
children: client.type_() == ClientType.camera
? [
buildPermissionIcon(
client.audio,
Icons.volume_up_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "audio",
enabled: enabled);
setState(() {
client.audio = enabled;
});
},
translate('Enable audio'),
),
buildPermissionIcon(
client.recording,
Icons.videocam_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "recording",
enabled: enabled);
setState(() {
client.recording = enabled;
});
},
translate('Enable recording session'),
),
]
: [
buildPermissionIcon(
client.keyboard,
Icons.keyboard,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "keyboard",
enabled: enabled);
setState(() {
client.keyboard = enabled;
});
},
translate('Enable keyboard/mouse'),
),
buildPermissionIcon(
client.clipboard,
Icons.assignment_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "clipboard",
enabled: enabled);
setState(() {
client.clipboard = enabled;
});
},
translate('Enable clipboard'),
),
buildPermissionIcon(
client.audio,
Icons.volume_up_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "audio",
enabled: enabled);
setState(() {
client.audio = enabled;
});
},
translate('Enable audio'),
),
buildPermissionIcon(
client.file,
Icons.upload_file_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "file",
enabled: enabled);
setState(() {
client.file = enabled;
});
},
translate('Enable file copy and paste'),
),
buildPermissionIcon(
client.restart,
Icons.restart_alt_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "restart",
enabled: enabled);
setState(() {
client.restart = enabled;
});
},
translate('Enable remote restart'),
),
buildPermissionIcon(
client.recording,
Icons.videocam_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "recording",
enabled: enabled);
setState(() {
client.recording = enabled;
});
},
translate('Enable recording session'),
),
// only windows support block input
if (isWindows)
buildPermissionIcon(
client.blockInput,
Icons.block,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "block_input",
enabled: enabled);
setState(() {
client.blockInput = enabled;
});
},
translate('Enable blocking user input'),
)
],
),
),
],

View File

@@ -0,0 +1,730 @@
import 'dart:async';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart';
import '../../common/widgets/overlay.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/toolbar.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../../utils/image.dart';
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.
final Map<String, bool> closeSessionOnDispose = {};
class ViewCameraPage extends StatefulWidget {
ViewCameraPage({
Key? key,
required this.id,
required this.toolbarState,
this.sessionId,
this.tabWindowId,
this.password,
this.display,
this.displays,
this.tabController,
this.connToken,
this.forceRelay,
this.isSharedPassword,
}) : super(key: key) {
initSharedStates(id);
}
final String id;
final SessionID? sessionId;
final int? tabWindowId;
final int? display;
final List<int>? displays;
final String? password;
final ToolbarState toolbarState;
final bool? forceRelay;
final bool? isSharedPassword;
final String? connToken;
final SimpleWrapper<State<ViewCameraPage>?> _lastState = SimpleWrapper(null);
final DesktopTabController? tabController;
FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi;
@override
State<ViewCameraPage> createState() {
final state = _ViewCameraPageState(id);
_lastState.value = state;
return state;
}
}
class _ViewCameraPageState extends State<ViewCameraPage>
with AutomaticKeepAliveClientMixin, MultiWindowListener {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
final _cursorOverImage = false.obs;
var _blockableOverlayState = BlockableOverlayState();
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
// to identify the toolbar instance and its callback function.
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
SessionID get sessionId => _ffi.sessionId;
_ViewCameraPageState(String id) {
_initStates(id);
}
void _initStates(String id) {}
@override
void initState() {
super.initState();
_ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
});
_ffi.start(
widget.id,
isViewCamera: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
display: widget.display,
displays: widget.displays,
connToken: widget.connToken,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
if (!isLinux) {
WakelockPlus.enable();
}
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback(
// (String? lastKey, String? currentKey) async {
// if (_firstEnterImage.value) {
// _firstEnterImage.value = false;
// return true;
// }
// return lastKey == null || lastKey != currentKey;
// });
// _isCustomCursorInited = true;
// }
_blockableOverlayState.applyFfi(_ffi);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController?.onSelected?.call(widget.id);
});
}
@override
void onWindowBlur() {
super.onWindowBlur();
// 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 (isWindows) {
_isWindowBlur = true;
// unfocus the primary-focus when the whole window is lost focus,
// and let OS to handle events instead.
_rawKeyFocusNode.unfocus();
}
stateGlobal.isFocused.value = false;
}
@override
void onWindowFocus() {
super.onWindowFocus();
// See [onWindowBlur].
if (isWindows) {
_isWindowBlur = false;
}
stateGlobal.isFocused.value = true;
}
@override
void onWindowRestore() {
super.onWindowRestore();
// On windows, we use `onWindowRestore` way to handle window restore from
// a minimized state.
if (isWindows) {
_isWindowBlur = false;
}
if (!isLinux) {
WakelockPlus.enable();
}
}
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
@override
void onWindowMaximize() {
super.onWindowMaximize();
if (!isLinux) {
WakelockPlus.enable();
}
}
@override
void onWindowMinimize() {
super.onWindowMinimize();
if (!isLinux) {
WakelockPlus.disable();
}
}
@override
void onWindowEnterFullScreen() {
super.onWindowEnterFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(true);
}
}
@override
void onWindowLeaveFullScreen() {
super.onWindowLeaveFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(false);
}
}
@override
Future<void> dispose() async {
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
// https://github.com/flutter/flutter/issues/64935
super.dispose();
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onViewCameraPageDispose(closeSession);
if (closeSession) {
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();
if (closeSession) {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
}
if (!isLinux) {
await WakelockPlus.disable();
}
await Get.delete<FFI>(tag: widget.id);
removeSharedStates(widget.id);
}
Widget emptyOverlay() => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: Colors.transparent,
),
);
Widget buildBody(BuildContext context) {
remoteToolbar(BuildContext context) => RemoteToolbar(
id: widget.id,
ffi: _ffi,
state: widget.toolbarState,
onEnterOrLeaveImageSetter: (id, func) {
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
_onEnterOrLeaveImage4Toolbar = func;
},
onEnterOrLeaveImageCleaner: (id) {
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
_onEnterOrLeaveImage4Toolbar = null;
}
},
setRemoteState: setState,
);
bodyWidget() {
return Stack(
children: [
Container(
color: kColorCanvas,
child: getBodyForDesktop(context),
),
Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay()
: () {
if (!_ffi.ffiModel.isPeerAndroid) {
return Offstage();
} else {
return Obx(() => Offstage(
offstage: _ffi.dialogManager
.mobileActionsOverlayVisible.isFalse,
child: Overlay(initialEntries: [
makeMobileActionsOverlayEntry(
() => _ffi.dialogManager
.setMobileActionsOverlayVisible(false),
ffi: _ffi,
)
]),
));
}
}(),
// Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue
? Overlay(
initialEntries: [OverlayEntry(builder: remoteToolbar)])
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
),
],
);
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Obx(() {
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isFalse;
if (imageReady) {
// If the privacy mode(disable physical displays) is switched,
// we should not dismiss the dialog immediately.
if (DateTime.now().difference(togglePrivacyModeTime) >
const Duration(milliseconds: 3000)) {
// `dismissAll()` is to ensure that the state is clean.
// It's ok to call dismissAll() here.
_ffi.dialogManager.dismissAll();
// Recreate the block state to refresh the state.
_blockableOverlayState = BlockableOverlayState();
_blockableOverlayState.applyFfi(_ffi);
}
// Block the whole `bodyWidget()` when dialog shows.
return BlockableOverlay(
underlying: bodyWidget(),
state: _blockableOverlayState,
);
} else {
// `_blockableOverlayState` is not recreated here.
// The toolbar's block state won't work properly when reconnecting, but that's okay.
return bodyWidget();
}
}),
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager);
return false;
},
child: MultiProvider(providers: [
ChangeNotifierProvider.value(value: _ffi.ffiModel),
ChangeNotifierProvider.value(value: _ffi.imageModel),
ChangeNotifierProvider.value(value: _ffi.cursorModel),
ChangeNotifierProvider.value(value: _ffi.canvasModel),
ChangeNotifierProvider.value(value: _ffi.recordingModel),
], child: buildBody(context)));
}
void enterView(PointerEnterEvent evt) {
_cursorOverImage.value = true;
_firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Toolbar != null) {
try {
_onEnterOrLeaveImage4Toolbar!(true);
} catch (e) {
//
}
}
// See [onWindowBlur].
if (!isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
_ffi.inputModel.enterOrLeave(true);
}
}
void leaveView(PointerExitEvent evt) {
if (_ffi.ffiModel.keyboard) {
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
}
_cursorOverImage.value = false;
_firstEnterImage.value = false;
if (_onEnterOrLeaveImage4Toolbar != null) {
try {
_onEnterOrLeaveImage4Toolbar!(false);
} catch (e) {
//
}
}
// See [onWindowBlur].
if (!isWindows) {
_ffi.inputModel.enterOrLeave(false);
}
}
Widget _buildRawTouchAndPointerRegion(
Widget child,
PointerEnterEventListener? onEnter,
PointerExitEventListener? onExit,
) {
return RawTouchGestureDetectorRegion(
child: _buildRawPointerMouseRegion(child, onEnter, onExit),
ffi: _ffi,
isCamera: true,
);
}
Widget _buildRawPointerMouseRegion(
Widget child,
PointerEnterEventListener? onEnter,
PointerExitEventListener? onExit,
) {
return CameraRawPointerMouseRegion(
onEnter: onEnter,
onExit: onExit,
onPointerDown: (event) {
// A double check for blur status.
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
// Sometimes the system does not send the necessary focus event to flutter. We should manually
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
// ensure the grab-key thread is running when our users are clicking the remote canvas.
if (_isWindowBlur) {
debugPrint(
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
_isWindowBlur = false;
}
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
},
inputModel: _ffi.inputModel,
child: child,
);
}
Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[
MouseRegion(onEnter: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
}, onExit: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
}, child: LayoutBuilder(builder: (context, constraints) {
final c = Provider.of<CanvasModel>(context, listen: false);
Future.delayed(Duration.zero, () => c.updateViewStyle());
final peerDisplay = CurrentDisplayState.find(widget.id);
return Obx(
() => _ffi.ffiModel.pi.isSet.isFalse
? Container(color: Colors.transparent)
: Obx(() {
widget.toolbarState.initShow(sessionId);
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
return ImagePaint(
id: widget.id,
cursorOverImage: _cursorOverImage,
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
child, enterView, leaveView),
ffi: _ffi,
);
}),
);
}))
];
paints.add(
Positioned(
top: 10,
right: 10,
child: _buildRawTouchAndPointerRegion(
QualityMonitor(_ffi.qualityMonitorModel), null, null),
),
);
return Stack(
children: paints,
);
}
@override
bool get wantKeepAlive => true;
}
class ImagePaint extends StatefulWidget {
final FFI ffi;
final String id;
final RxBool cursorOverImage;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
{Key? key,
required this.ffi,
required this.id,
required this.cursorOverImage,
this.listenerBuilder})
: super(key: key);
@override
State<StatefulWidget> createState() => _ImagePaintState();
}
class _ImagePaintState extends State<ImagePaint> {
bool _lastRemoteCursorMoved = false;
String get id => widget.id;
RxBool get cursorOverImage => widget.cursorOverImage;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);
var c = Provider.of<CanvasModel>(context);
final s = c.scale;
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
c.updateScrollPercent();
return false;
},
child: Container(
child: _buildCrossScrollbarFromLayout(
context,
_buildListener(paintWidget),
c.size,
paintSize,
c.scrollHorizontal,
c.scrollVertical,
)),
);
} else {
if (c.size.width > 0 && c.size.height > 0) {
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c,
s,
Offset(
isLinux ? c.x.toInt().toDouble() : c.x,
isLinux ? c.y.toInt().toDouble() : c.y,
),
c.size,
isViewOriginal())
: _buildScrollAutoNonTextureRender(m, c, s);
return Container(child: _buildListener(paintWidget));
} else {
return Container();
}
}
}
Widget _buildScrollbarNonTextureRender(
ImageModel m, Size imageSize, double s) {
return CustomPaint(
size: imageSize,
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
);
}
Widget _buildScrollAutoNonTextureRender(
ImageModel m, CanvasModel c, double s) {
return CustomPaint(
size: Size(c.size.width, c.size.height),
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
);
}
Widget _BuildPaintTextureRender(
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
final ffiModel = c.parent.target!.ffiModel;
final displays = ffiModel.pi.getCurDisplays();
final children = <Widget>[];
final rect = ffiModel.rect;
if (rect == null) {
return Container();
}
final curDisplay = ffiModel.pi.currentDisplay;
for (var i = 0; i < displays.length; i++) {
final textureId = widget.ffi.textureModel
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
if (true) {
// both "textureId.value != -1" and "true" seems ok
children.add(Positioned(
left: (displays[i].x - rect.left) * s + offset.dx,
top: (displays[i].y - rect.top) * s + offset.dy,
width: displays[i].width * s,
height: displays[i].height * s,
child: Obx(() => Texture(
textureId: textureId.value,
filterQuality:
isViewOriginal ? FilterQuality.none : FilterQuality.low,
)),
));
}
}
return SizedBox(
width: size.width,
height: size.height,
child: Stack(children: children),
);
}
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = cursor.cache ?? preDefaultCursor.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);
}
Widget _buildCrossScrollbarFromLayout(
BuildContext context,
Widget child,
Size layoutSize,
Size size,
ScrollController horizontal,
ScrollController vertical,
) {
var widget = child;
if (layoutSize.width < size.width) {
widget = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: horizontal,
scrollDirection: Axis.horizontal,
physics: cursorOverImage.isTrue
? const NeverScrollableScrollPhysics()
: null,
child: widget,
),
);
} else {
widget = Row(
children: [
Container(
width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
),
widget,
],
);
}
if (layoutSize.height < size.height) {
widget = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: vertical,
physics: cursorOverImage.isTrue
? const NeverScrollableScrollPhysics()
: null,
child: widget,
),
);
} else {
widget = Column(
children: [
Container(
height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
),
widget,
],
);
}
if (layoutSize.width < size.width) {
widget = RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: horizontal,
thumbVisibility: false,
trackVisibility: false,
notificationPredicate: layoutSize.height < size.height
? (notification) => notification.depth == 1
: defaultScrollNotificationPredicate,
child: widget,
);
}
if (layoutSize.height < size.height) {
widget = RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: vertical,
thumbVisibility: false,
trackVisibility: false,
child: widget,
);
}
return Container(
child: widget,
width: layoutSize.width,
height: layoutSize.height,
);
}
Widget _buildListener(Widget child) {
if (listenerBuilder != null) {
return listenerBuilder!(child);
} else {
return child;
}
}
}

View File

@@ -0,0 +1,499 @@
import 'dart:convert';
import 'dart:async';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
static const Color blueColor = MyTheme.button;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
}
class ViewCameraTabPage extends StatefulWidget {
final Map<String, dynamic> params;
const ViewCameraTabPage({Key? key, required this.params}) : super(key: key);
@override
State<ViewCameraTabPage> createState() => _ViewCameraTabPageState(params);
}
class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
final tabController =
Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera));
final contentKey = UniqueKey();
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
String? peerId;
bool _isScreenRectSet = false;
int? _display;
var connectionMap = RxList<Widget>.empty(growable: true);
_ViewCameraTabPageState(Map<String, dynamic> params) {
RemoteCountState.init();
peerId = params['id'];
final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id'];
final display = params['display'];
final displays = params['displays'];
final screenRect = parseParamScreenRect(params);
_isScreenRectSet = screenRect != null;
_display = display as int?;
tryMoveToScreenAndSetFullscreen(screenRect);
if (peerId != null) {
ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
final viewCameraPage = tabController.widget(id);
if (viewCameraPage is ViewCameraPage) {
final ffi = viewCameraPage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId);
}
WindowController.fromWindowId(params['windowId'])
.setTitle(getWindowNameWithId(id));
UnreadChatCountState.find(id).value = 0;
};
tabController.add(TabInfo(
key: peerId!,
label: peerId!,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId),
page: ViewCameraPage(
key: ValueKey(peerId),
id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: params['password'],
toolbarState: ToolbarState(),
tabController: tabController,
connToken: params['connToken'],
forceRelay: params['forceRelay'],
isSharedPassword: params['isSharedPassword'],
),
));
_update_remote_count();
}
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
}
@override
void initState() {
super.initState();
if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.ViewCamera,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
display: _display,
);
});
}
}
@override
Widget build(BuildContext context) {
final child = Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
bool secure =
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {
fingerprint = 'N/A';
}
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
msgFingerprint += '$first\n$second';
} else {
msgFingerprint += fingerprint;
}
final tab = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgConn\n$msgFingerprint',
child: SvgPicture.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
unreadMessageCountBuilder(UnreadChatCountState.find(key))
.marginOnly(left: 4),
],
);
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as ViewCameraPage;
if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue &&
e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
},
target: e.position,
);
}
},
child: tab,
);
}
}),
),
);
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: workaroundWindowBorder(
context,
Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
key: contentKey,
child: tabWidget,
// Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
));
}
// Note: Some dup code to ../widgets/remote_toolbar
Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) {
final List<MenuEntryBase<String>> menu = [];
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as ViewCameraPage;
final ffi = viewCameraPage.ffi;
final sessionId = ffi.sessionId;
final toolbarState = viewCameraPage.toolbarState;
menu.addAll([
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
style: style,
)),
proc: () {
toolbarState.switchShow(sessionId);
cancelFunc();
},
padding: padding,
),
]);
if (tabController.state.value.tabs.length > 1) {
final splitAction = MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Move tab to new window'),
style: style,
),
proc: () async {
await DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventMoveTabToNewWindow,
'${windowId()},$key,$sessionId,ViewCamera');
cancelFunc();
},
padding: padding,
);
menu.insert(1, splitAction);
}
menu.addAll([
MenuEntryDivider<String>(),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Copy Fingerprint'),
style: style,
),
proc: () => onCopyFingerprint(FingerprintState.find(key).value),
padding: padding,
dismissOnClicked: true,
dismissCallback: cancelFunc,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Close'),
style: style,
),
proc: () {
tabController.closeBy(key);
cancelFunc();
},
padding: padding,
)
]);
return mod_menu.PopupMenu<String>(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenuTheme.blueColor,
height: _MenuTheme.height,
dividerHeight: _MenuTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) {
// Keep calling until the window status is hidden.
//
// Workaround for Windows:
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
// `await WindowController.fromWindowId(windowId()).close();`.
Future<void> loopCloseWindow() async {
int c = 0;
final windowController = WindowController.fromWindowId(windowId());
while (c < 20 &&
tabController.state.value.tabs.isEmpty &&
(!await windowController.isHidden())) {
await windowController.close();
await Future.delayed(Duration(milliseconds: 100));
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);
_update_remote_count();
}
int windowId() {
return widget.params["windowId"];
}
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length;
if (connLength <= 1) {
tabController.clear();
return true;
} else {
final bool res;
if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true;
} else {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
}
return res;
}
}
_update_remote_count() =>
RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
debugPrint(
"[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewViewCamera) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
final prePeerCount = tabController.length;
Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
WindowType.ViewCamera, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});
});
ConnectionTypeState.init(id);
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: ViewCameraPage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: ToolbarState(),
tabController: tabController,
connToken: args['connToken'],
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk =
tabController.jumpToByKeyAndDisplay(id, display, isCamera: true);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as ViewCameraPage;
returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
} else if (call.method == kWindowEventRemoteWindowCoords) {
final viewCameraPage =
tabController.state.value.selectedTabInfo.page as ViewCameraPage;
final ffi = viewCameraPage.ffi;
final displayRect = ffi.ffiModel.displaysRect();
if (displayRect != null) {
final wc = WindowController.fromWindowId(windowId());
Rect? frame;
try {
frame = await wc.getFrame();
} catch (e) {
debugPrint(
"Failed to get frame of window $windowId, it may be hidden");
}
if (frame != null) {
ffi.cursorModel.moveLocal(0, 0);
final coords = RemoteWindowCoords(
frame,
CanvasCoords.fromCanvasModel(ffi.canvasModel),
CursorCoords.fromCursorModel(ffi.cursorModel),
displayRect);
returnValue = jsonEncode(coords.toJson());
}
}
} else if (call.method == kWindowEventSetFullscreen) {
stateGlobal.setFullscreen(call.arguments == 'true');
}
_update_remote_count();
return returnValue;
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
class DesktopViewCameraScreen extends StatelessWidget {
final Map<String, dynamic> params;
DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) {
bind.mainInitInputSource();
stateGlobal.getInputSource(force: true);
}
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
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: isLinux ? Colors.transparent : null,
body: ViewCameraTabPage(
params: params,
),
));
}
}

View File

@@ -478,7 +478,10 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
state: widget.state,
setFullscreen: _setFullscreen,
));
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
// Do not show keyboard for camera connection type.
if (widget.ffi.connType == ConnType.defaultConn) {
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
}
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) {
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
@@ -1043,23 +1046,26 @@ class _DisplayMenuState extends State<_DisplayMenu> {
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
if (showVirtualDisplayMenu(ffi))
if (ffi.connType == ConnType.defaultConn)
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn)
_SubmenuButton(
ffi: widget.ffi,
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
child: Text(translate("Virtual display")),
),
cursorToggles(),
if (ffi.connType == ConnType.defaultConn) cursorToggles(),
Divider(),
toggles(),
];
// privacy mode
if (ffiModel.keyboard && pi.features.privacyMode) {
if (ffi.connType == ConnType.defaultConn &&
ffiModel.keyboard &&
pi.features.privacyMode) {
final privacyModeState = PrivacyModeState.find(id);
final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi);
@@ -1085,7 +1091,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
]);
}
}
menuChildren.add(widget.pluginItem);
if (ffi.connType == ConnType.defaultConn) {
menuChildren.add(widget.pluginItem);
}
return menuChildren;
}
@@ -1495,7 +1503,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
);
}
TextField _resolutionInput(TextEditingController controller) {
Widget _resolutionInput(TextEditingController controller) {
return TextField(
decoration: InputDecoration(
border: InputBorder.none,
@@ -1509,7 +1517,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
controller: controller,
);
).workaroundFreezeLinuxMint();
}
List<Widget> _supportedResolutionMenuButtons() => resolutions

View File

@@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -51,6 +52,7 @@ enum DesktopTabType {
cm,
remoteScreen,
fileTransfer,
viewCamera,
portForward,
install,
}
@@ -179,11 +181,13 @@ class DesktopTabController {
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
callOnSelected: callOnSelected);
bool jumpToByKeyAndDisplay(String key, int display) {
bool jumpToByKeyAndDisplay(String key, int display, {bool isCamera = false}) {
for (int i = 0; i < state.value.tabs.length; i++) {
final tab = state.value.tabs[i];
if (tab.key == key) {
final ffi = (tab.page as RemotePage).ffi;
final ffi = isCamera
? (tab.page as ViewCameraPage).ffi
: (tab.page as RemotePage).ffi;
if (ffi.ffiModel.pi.currentDisplay == display) {
return jumpTo(i, callOnSelected: true);
}
@@ -246,7 +250,6 @@ class DesktopTab extends StatefulWidget {
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
final Color? selectedBorderColor;
final RxBool? blockTab;
final DesktopTabController controller;
@@ -272,7 +275,6 @@ class DesktopTab extends StatefulWidget {
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
this.selectedBorderColor,
this.blockTab,
}) : super(key: key);
static RxString tablabelGetter(String peerId) {
@@ -311,7 +313,6 @@ class _DesktopTabState extends State<DesktopTab>
Color? get unSelectedTabBackgroundColor =>
widget.unSelectedTabBackgroundColor;
Color? get selectedBorderColor => widget.selectedBorderColor;
RxBool? get blockTab => widget.blockTab;
DesktopTabController get controller => widget.controller;
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
Debouncer get _scrollDebounce => widget._scrollDebounce;
@@ -533,21 +534,9 @@ class _DesktopTabState extends State<DesktopTab>
]);
}
Widget _buildBlock({required Widget child}) {
if (blockTab != null) {
return buildRemoteBlock(
child: child,
block: blockTab!,
use: canBeBlocked,
mask: tabType == DesktopTabType.main);
} else {
return child;
}
}
List<Widget> _tabWidgets = [];
Widget _buildPageView() {
final child = _buildBlock(
final child = Container(
child: Obx(() => PageView(
controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(),
@@ -740,6 +729,7 @@ class WindowActionPanelState extends State<WindowActionPanel> {
return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
widget.tabController.tabType == DesktopTabType.viewCamera ||
widget.tabController.tabType == DesktopTabType.portForward ||
widget.tabController.tabType == DesktopTabType.cm);
}

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
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';
@@ -76,6 +77,13 @@ Future<void> main(List<String> args) async {
kAppTypeDesktopFileTransfer,
);
break;
case WindowType.ViewCamera:
desktopType = DesktopType.viewCamera;
runMultiWindow(
argument,
kAppTypeDesktopViewCamera,
);
break;
case WindowType.PortForward:
desktopType = DesktopType.portForward;
runMultiWindow(
@@ -133,7 +141,8 @@ void runMainApp(bool startService) async {
runApp(App());
// Set window option.
WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
WindowOptions windowOptions =
getHiddenTitleBarWindowOptions(isMainWindow: true);
windowManager.waitUntilReadyToShow(windowOptions, () async {
// Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main);
@@ -191,6 +200,12 @@ void runMultiWindow(
params: argument,
);
break;
case kAppTypeDesktopViewCamera:
draggablePositions.load();
widget = DesktopViewCameraScreen(
params: argument,
);
break;
case kAppTypeDesktopPortForward:
widget = DesktopPortForwardScreen(
params: argument,
@@ -226,6 +241,19 @@ void runMultiWindow(
await restoreWindowPosition(WindowType.FileTransfer,
windowId: kWindowId!);
break;
case kAppTypeDesktopViewCamera:
// If screen rect is set, the window will be moved to the target screen and then set fullscreen.
if (argument['screen_rect'] == null) {
// display can be used to control the offset of the window.
await restoreWindowPosition(
WindowType.ViewCamera,
windowId: kWindowId!,
peerId: argument['id'] as String?,
// FIXME: fix display index.
display: argument['display'] as int?,
);
}
break;
case kAppTypeDesktopPortForward:
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
break;
@@ -354,7 +382,10 @@ void runInstallPage() async {
}
WindowOptions getHiddenTitleBarWindowOptions(
{Size? size, bool center = false, bool? alwaysOnTop}) {
{bool isMainWindow = false,
Size? size,
bool center = false,
bool? alwaysOnTop}) {
var defaultTitleBarStyle = TitleBarStyle.hidden;
// we do not hide titlebar on win7 because of the frame overflow.
if (kUseCompatibleUiMode) {
@@ -363,7 +394,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
return WindowOptions(
size: size,
center: center,
backgroundColor: Colors.transparent,
backgroundColor: (isMacOS && isMainWindow) ? null : Colors.transparent,
skipTaskbar: false,
titleBarStyle: defaultTitleBarStyle,
alwaysOnTop: alwaysOnTop,
@@ -485,18 +516,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
child = keyListenerBuilder(context, child);
}
if (isLinux) {
// `(!(isLinuxMateDesktop || isLinuxMint))` is not used here for clarity.
// `isLinuxMint` will call ffi function.
if (!isLinuxMateDesktop) {
if (!isLinuxMint) {
debugPrint(
'Linux distro is not linuxmint, and desktop is not mate, '
'so we build virtual window frame.');
child = buildVirtualWindowFrame(context, child);
}
}
return buildVirtualWindowFrame(context, child);
} else {
return workaroundWindowBorder(context, child);
}
return child;
},
),
);

View File

@@ -41,10 +41,11 @@ class _ConnectionPageState extends State<ConnectionPage> {
final _idController = IDTextEditingController();
final RxBool _idEmpty = true.obs;
List<Peer> peers = [];
final FocusNode _idFocusNode = FocusNode();
final TextEditingController _idEditingController = TextEditingController();
final AllPeersLoader _allPeersLoader = AllPeersLoader();
bool isPeersLoading = false;
bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription;
// https://github.com/flutter/flutter/issues/157244
@@ -61,6 +62,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
@override
void initState() {
super.initState();
_allPeersLoader.init(setState);
_idFocusNode.addListener(onFocusChanged);
if (_idController.text.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@@ -71,6 +74,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
});
}
Get.put<TextEditingController>(_idEditingController);
}
@override
@@ -80,7 +84,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
slivers: [
SliverList(
delegate: SliverChildListDelegate([
if (!bind.isCustomClient())
if (!bind.isCustomClient() && !isIOS)
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
_buildRemoteIDTextField(),
])),
@@ -99,6 +103,20 @@ class _ConnectionPageState extends State<ConnectionPage> {
connect(context, id);
}
void onFocusChanged() {
_idEmpty.value = _idEditingController.text.isEmpty;
if (_idFocusNode.hasFocus) {
if (_allPeersLoader.needLoad) {
_allPeersLoader.getAllPeers();
}
final textLength = _idEditingController.value.text.length;
// Select all to facilitate removing text, just following the behavior of address input of chrome.
_idEditingController.selection =
TextSelection(baseOffset: 0, extentOffset: textLength);
}
}
/// UI for software update.
/// If _updateUrl] is not empty, shows a button to update the software.
Widget _buildUpdateUI(String updateUrl) {
@@ -107,7 +125,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
: InkWell(
onTap: () async {
final url = 'https://rustdesk.com/download';
// https://pub.dev/packages/url_launcher#configuration
// https://pub.dev/packages/url_launcher#configuration
// https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs
//
// `await launchUrl(Uri.parse(url))` can also run if skip
@@ -115,9 +133,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
// 2. `<action android:name="android.support.customtabs.action.CustomTabsService" />` in AndroidManifest.xml
//
// But it is better to add the check.
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
}
await launchUrl(Uri.parse(url));
},
child: Container(
alignment: AlignmentDirectional.center,
@@ -129,18 +145,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
color: Colors.white, fontWeight: FontWeight.bold))));
}
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
}
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
Widget _buildRemoteIDTextField() {
@@ -158,11 +162,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Autocomplete<Peer>(
child: RawAutocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
_autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
} else if (_allPeersLoader.peers.isEmpty &&
!_allPeersLoader.isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
@@ -176,6 +181,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
rdpPort: '',
rdpUsername: '',
loginName: '',
device_group_name: '',
);
_autocompleteOpts = [emptyPeer];
} else {
@@ -189,7 +195,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
String textToFind = textEditingValue.text.toLowerCase();
_autocompleteOpts = peers
_autocompleteOpts = _allPeersLoader.peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@@ -203,25 +209,14 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
return _autocompleteOpts;
},
focusNode: _idFocusNode,
textEditingController: _idEditingController,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(
fieldTextEditingController);
fieldFocusNode.addListener(() async {
_idEmpty.value =
fieldTextEditingController.text.isEmpty;
if (fieldFocusNode.hasFocus && !isPeersLoading) {
_fetchPeers();
}
});
final textLength =
fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection = TextSelection(
baseOffset: 0, extentOffset: textLength);
updateTextAndPreserveSelection(
fieldTextEditingController, _idController.text);
return AutoSizeTextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
@@ -301,7 +296,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
maxHeight: maxHeight,
maxWidth: 320,
),
child: peers.isEmpty && isPeersLoading
child: _allPeersLoader
.peers.isEmpty &&
!_allPeersLoader.isPeersLoaded
? Container(
height: 80,
child: Center(
@@ -364,16 +361,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
void dispose() {
_uniLinksSubscription?.cancel();
_idController.dispose();
_idFocusNode.removeListener(onFocusChanged);
_allPeersLoader.clear();
_idFocusNode.dispose();
_idEditingController.dispose();
if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>();
}
if (Get.isRegistered<TextEditingController>()) {
Get.delete<TextEditingController>();
}
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
super.dispose();
}
}

View File

@@ -225,7 +225,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
errorText: errorText,
),
controller: name,
),
).workaroundFreezeLinuxMint(),
],
),
actions: [

View File

@@ -204,6 +204,7 @@ class WebHomePage extends StatelessWidget {
return;
}
bool isFileTransfer = false;
bool isViewCamera = false;
String? id;
String? password;
for (int i = 0; i < args.length; i++) {
@@ -219,6 +220,11 @@ class WebHomePage extends StatelessWidget {
id = args[i + 1];
i++;
break;
case '--view-camera':
isViewCamera = true;
id = args[i + 1];
i++;
break;
case '--password':
password = args[i + 1];
i++;
@@ -228,7 +234,7 @@ class WebHomePage extends StatelessWidget {
}
}
if (id != null) {
connect(context, id, isFileTransfer: isFileTransfer, password: password);
connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password);
}
}
}

View File

@@ -147,6 +147,19 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
gFFI.chatModel.onVoiceCallClosed("End connetion");
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
trySyncClipboard();
}
}
// For client side
// When swithing from other app to this app, try to sync clipboard.
void trySyncClipboard() {
gFFI.invokeMethod("try_sync_clipboard");
}
@override
void didChangeMetrics() {
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
@@ -591,7 +604,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// ko/zh/ja input method: the button will trigger `onKeyEvent`
// and the event will not popup if `KeyEventResult.handled` is returned.
onChanged: handleSoftKeyboardInput,
),
).workaroundFreezeLinuxMint(),
),
];
if (showCursorPaint) {

View File

@@ -782,9 +782,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
tiles: [
SettingsTile(
onPressed: (context) async {
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
}
await launchUrl(Uri.parse(url));
},
title: Text(translate("Version: ") + version),
value: Padding(
@@ -928,9 +926,7 @@ void showAbout(OverlayDialogManager dialogManager) {
InkWell(
onTap: () async {
const url = 'https://rustdesk.com/';
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
}
await launchUrl(Uri.parse(url));
},
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8),

View File

@@ -0,0 +1,721 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../../common.dart';
import '../../common/widgets/overlay.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/remote_input.dart';
import '../../models/input_model.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../utils/image.dart';
final initText = '1' * 1024;
// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
// https://github.com/flutter/flutter/issues/159384
// https://github.com/flutter/flutter/issues/159383
void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
if (isAndroid) {
if (isKeyboardVisible != true) {
// `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
gFFI.invokeMethod("enable_soft_keyboard", false);
}
}
}
class ViewCameraPage extends StatefulWidget {
ViewCameraPage(
{Key? key, required this.id, this.password, this.isSharedPassword})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
@override
State<ViewCameraPage> createState() => _ViewCameraPageState(id);
}
class _ViewCameraPageState extends State<ViewCameraPage>
with WidgetsBindingObserver {
Timer? _timer;
bool _showBar = !isWebDesktop;
bool _showGestureHelp = false;
Orientation? _currentOrientation;
double _viewInsetsBottom = 0;
Timer? _timerDidChangeMetrics;
final _blockableOverlayState = BlockableOverlayState();
final keyboardVisibilityController = KeyboardVisibilityController();
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
final TextEditingController _textController =
TextEditingController(text: initText);
_ViewCameraPageState(String id) {
initSharedStates(id);
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override
void initState() {
super.initState();
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
gFFI.start(
widget.id,
isViewCamera: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
if (!isWeb) {
WakelockPlus.enable();
}
_physicalFocusNode.requestFocus();
gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
_blockableOverlayState.applyFfi(gFFI);
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
gFFI.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
if (gFFI.recordingModel.start) {
showToast(translate('Automatically record outgoing sessions'));
}
_disableAndroidSoftKeyboard(
isKeyboardVisible: keyboardVisibilityController.isVisible);
});
WidgetsBinding.instance.addObserver(this);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
// https://github.com/flutter/flutter/issues/64935
super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
gFFI.inputModel.listenToMouse(false);
gFFI.imageModel.disposeImage();
gFFI.cursorModel.disposeImages();
await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
await gFFI.close();
_timer?.cancel();
_timerDidChangeMetrics?.cancel();
gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
if (!isWeb) {
await WakelockPlus.disable();
}
removeSharedStates(widget.id);
// `on_voice_call_closed` should be called when the connection is ended.
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
// Only one client is considered here for now.
gFFI.chatModel.onVoiceCallClosed("End connetion");
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {}
@override
void didChangeMetrics() {
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
// Don't try reset the view style and focus the cursor.
if (gFFI.cursorModel.lastKeyboardIsVisible &&
gFFI.canvasModel.isMobileCanvasChanged) {
return;
}
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
_timerDidChangeMetrics?.cancel();
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
if (newBottom != _viewInsetsBottom) {
gFFI.canvasModel.mobileFocusCanvasCursor();
_viewInsetsBottom = newBottom;
}
});
}
// to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
// But I don't know why and how to fix it.
Widget emptyOverlay(Color bgColor) => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: bgColor,
),
);
Widget _bottomWidget() => (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar()
: Offstage());
@override
Widget build(BuildContext context) {
final keyboardIsVisible =
keyboardVisibilityController.isVisible && _showEdit;
final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, gFFI.dialogManager);
return false;
},
child: Scaffold(
// workaround for https://github.com/rustdesk/rustdesk/issues/3131
floatingActionButtonLocation: keyboardIsVisible
? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
: null,
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
mini: !keyboardIsVisible,
child: Icon(
(keyboardIsVisible || _showGestureHelp)
? Icons.expand_more
: Icons.expand_less,
color: Colors.white,
),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
if (keyboardIsVisible) {
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
} else if (_showGestureHelp) {
_showGestureHelp = false;
} else {
_showBar = !_showBar;
}
});
}),
bottomNavigationBar: Obx(() => Stack(
alignment: Alignment.bottomCenter,
children: [
gFFI.ffiModel.pi.isSet.isTrue &&
gFFI.ffiModel.waitForFirstImage.isTrue
? emptyOverlay(MyTheme.canvasColor)
: () {
gFFI.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
_bottomWidget(),
gFFI.ffiModel.pi.isSet.isFalse
? emptyOverlay(MyTheme.canvasColor)
: Offstage(),
],
)),
body: Obx(
() => getRawPointerAndKeyBody(Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
return Container(
color: kColorCanvas,
child: 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()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
);
}),
),
);
})
],
)),
)),
);
}
Widget getRawPointerAndKeyBody(Widget child) {
return CameraRawPointerMouseRegion(
inputModel: inputModel,
// Disable RawKeyFocusScope before the connecting is established.
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
child: gFFI.ffiModel.pi.isSet.isTrue
? RawKeyFocusScope(
focusNode: _physicalFocusNode,
inputModel: inputModel,
child: child)
: child,
);
}
Widget getBottomAppBar() {
return BottomAppBar(
elevation: 10,
color: MyTheme.accent,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose(sessionId, gFFI.dialogManager);
},
),
IconButton(
color: Colors.white,
icon: Icon(Icons.tv),
onPressed: () {
setState(() => _showEdit = false);
showOptions(context, widget.id, gFFI.dialogManager);
},
)
] +
(isWeb
? []
: <Widget>[
futureBuilder(
future: gFFI.invokeMethod(
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
hasData: (isSupportVoiceCall) => IconButton(
color: Colors.white,
icon: isAndroid && isSupportVoiceCall
? SvgPicture.asset('assets/chat.svg',
colorFilter: ColorFilter.mode(
Colors.white, BlendMode.srcIn))
: Icon(Icons.message),
onPressed: () =>
isAndroid && isSupportVoiceCall
? showChatOptions(widget.id)
: onPressedTextChat(widget.id),
))
]) +
[
IconButton(
color: Colors.white,
icon: Icon(Icons.more_vert),
onPressed: () {
setState(() => _showEdit = false);
showActions(widget.id);
},
),
]),
Obx(() => IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
? null
: () {
setState(() => _showBar = !_showBar);
},
)),
],
),
);
}
Widget getBodyForMobile() {
return Container(
color: MyTheme.canvasColor,
child: Stack(children: () {
final paints = [
ImagePaint(),
Positioned(
top: 10,
right: 10,
child: QualityMonitor(gFFI.qualityMonitorModel),
),
SizedBox(
width: 0,
height: 0,
child: !_showEdit
? Container()
: TextFormField(
textInputAction: TextInputAction.newline,
autocorrect: false,
// Flutter 3.16.9 Android.
// `enableSuggestions` causes secure keyboard to be shown.
// https://github.com/flutter/flutter/issues/139143
// https://github.com/flutter/flutter/issues/146540
// enableSuggestions: false,
autofocus: true,
focusNode: _mobileFocusNode,
maxLines: null,
controller: _textController,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
// `onChanged` may be called depending on the input method if this widget is wrapped in
// `Focus(onKeyEvent: ..., child: ...)`
// For `Backspace` button in the soft keyboard:
// en/fr input method:
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
// 2. The button will trigger `onKeyEvent` if the text field is empty.
// ko/zh/ja input method: the button will trigger `onKeyEvent`
// and the event will not popup if `KeyEventResult.handled` is returned.
onChanged: null,
).workaroundFreezeLinuxMint(),
),
];
return paints;
}()));
}
Widget getBodyForDesktopWithListener() {
var paints = <Widget>[ImagePaint()];
return Container(
color: MyTheme.canvasColor, child: Stack(children: paints));
}
List<TTextMenu> _getMobileActionMenus() {
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
!gFFI.ffiModel.keyboard) {
return [];
}
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
TTextMenu(
child: Text(translate('Back')),
onPressed: () => gFFI.inputModel.onMobileBack(),
),
TTextMenu(
child: Text(translate('Home')),
onPressed: () => gFFI.inputModel.onMobileHome(),
),
TTextMenu(
child: Text(translate('Apps')),
onPressed: () => gFFI.inputModel.onMobileApps(),
),
TTextMenu(
child: Text(translate('Volume up')),
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
),
TTextMenu(
child: Text(translate('Volume down')),
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
),
TTextMenu(
child: Text(translate('Power')),
onPressed: () => gFFI.inputModel.onMobilePower(),
),
];
}
void showActions(String id) async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
final mobileActionMenus = _getMobileActionMenus();
final menus = toolbarControls(context, id, gFFI);
final List<PopupMenuEntry<int>> more = [
...mobileActionMenus
.asMap()
.entries
.map((e) =>
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList(),
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
...menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(
child: e.value.getChild(),
value: e.key + mobileActionMenus.length))
.toList(),
];
() async {
var index = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: more,
elevation: 8,
);
if (index != null) {
if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
}
}
}();
}
onPressedTextChat(String id) {
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
gFFI.chatModel.toggleChatOverlay();
}
showChatOptions(String id) async {
onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
{TextStyle? labelStyle}) =>
TTextMenu(
child: Text(translate(label), style: labelStyle),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IgnorePointer(
child: IconButton(
onPressed: null,
icon: icon,
),
),
),
onPressed: onPressed,
);
final isInVoice = [
VoiceCallStatus.waitingForResponse,
VoiceCallStatus.connected
].contains(gFFI.chatModel.voiceCallStatus.value);
final menus = [
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
() => onPressedTextChat(widget.id)),
isInVoice
? makeTextMenu(
'End voice call',
SvgPicture.asset(
'assets/call_wait.svg',
colorFilter:
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
),
onPressEndVoiceCall,
labelStyle: TextStyle(color: Colors.redAccent))
: makeTextMenu(
'Voice call',
SvgPicture.asset(
'assets/call_wait.svg',
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
),
onPressVoiceCall),
];
final menuItems = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList();
Future.delayed(Duration.zero, () async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
var index = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: menuItems,
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
}
});
}
}
class ImagePaint extends StatelessWidget {
@override
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);
final c = Provider.of<CanvasModel>(context);
var s = c.scale;
final adjust = c.getAdjustY();
return CustomPaint(
painter: ImagePainter(
image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
);
}
}
void showOptions(
BuildContext context, String id, OverlayDialogManager dialogManager) async {
var displays = <Widget>[];
final pi = gFFI.ffiModel.pi;
final image = gFFI.ffiModel.getConnectionImage();
if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
}
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
final cur = pi.currentDisplay;
final children = <Widget>[];
for (var i = 0; i < pi.displays.length; ++i) {
children.add(InkWell(
onTap: () {
if (i == cur) return;
openMonitorInTheSameTab(i, gFFI, pi);
gFFI.dialogManager.dismissAll();
},
child: Ink(
width: 40,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).hintColor),
borderRadius: BorderRadius.circular(2),
color: i == cur
? Theme.of(context).primaryColor.withOpacity(0.6)
: null),
child: Center(
child: Text((i + 1).toString(),
style: TextStyle(
color: i == cur ? Colors.white : Colors.black87,
fontWeight: FontWeight.bold))))));
}
displays.add(Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
children: children,
)));
}
if (displays.isNotEmpty) {
displays.add(const Divider(color: MyTheme.border));
}
List<TRadioMenu<String>> viewStyleRadios =
await toolbarViewStyle(context, id, gFFI);
List<TRadioMenu<String>> imageQualityRadios =
await toolbarImageQuality(context, id, gFFI);
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI);
dialogManager.show((setState, close, context) {
var viewStyle =
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
var imageQuality =
(imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '')
.obs;
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
final radios = [
for (var e in viewStyleRadios)
Obx(() => getRadio<String>(
e.child,
e.value,
viewStyle.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in imageQualityRadios)
Obx(() => getRadio<String>(
e.child,
e.value,
imageQuality.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in codecRadios)
Obx(() => getRadio<String>(
e.child,
e.value,
codec.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
}
: null)),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
];
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
final displayTogglesList = displayToggles
.asMap()
.entries
.map((e) => Obx(() => CheckboxListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxToggleValues[e.key].value,
onChanged: e.value.onChanged != null
? (v) {
e.value.onChanged?.call(v);
if (v != null) rxToggleValues[e.key].value = v;
}
: null,
title: e.value.child)))
.toList();
final toggles = [
...displayTogglesList,
];
var popupDialogMenus = List<Widget>.empty(growable: true);
if (popupDialogMenus.isNotEmpty) {
popupDialogMenus.add(const Divider(color: MyTheme.border));
}
return CustomAlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays + radios + popupDialogMenus + toggles),
);
}, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
}
class FABLocation extends FloatingActionButtonLocation {
FloatingActionButtonLocation location;
double offsetX;
double offsetY;
FABLocation(this.location, this.offsetX, this.offsetY);
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final offset = location.getOffset(scaffoldGeometry);
return Offset(offset.dx + offsetX, offset.dy + offsetY);
}
}

View File

@@ -66,7 +66,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
? null
: translate('Too short, at least 6 characters.');
},
),
).workaroundFreezeLinuxMint(),
TextFormField(
obscureText: true,
keyboardType: TextInputType.visiblePassword,
@@ -85,7 +85,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
? null
: translate('The confirmation is not identical.');
},
),
).workaroundFreezeLinuxMint(),
])),
onCancel: close,
onSubmit: (validateLength && validateSame) ? submit : null,
@@ -216,7 +216,7 @@ void showServerSettingsWithValue(
),
validator: validator,
autofocus: autofocus,
),
).workaroundFreezeLinuxMint(),
),
],
);
@@ -229,7 +229,7 @@ void showServerSettingsWithValue(
errorText: errorMsg.isEmpty ? null : errorMsg,
),
validator: validator,
);
).workaroundFreezeLinuxMint();
}
return CustomAlertDialog(

View File

@@ -58,6 +58,9 @@ class AbModel {
String? _personalAbGuid;
RxBool legacyMode = false.obs;
// Only handles peers add/remove
final Map<String, VoidCallback> _peerIdUpdateListeners = {};
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
@@ -188,6 +191,7 @@ class AbModel {
debugPrint("pull current Ab error: $e");
}
}
_callbackPeerUpdate();
if (listInitialized && current.initialized) {
_saveCache();
}
@@ -419,6 +423,7 @@ class AbModel {
}
});
}
_callbackPeerUpdate();
return ret;
}
@@ -620,6 +625,9 @@ class AbModel {
}
}
}
if (abEntries.isNotEmpty) {
_callbackPeerUpdate();
}
}
}
@@ -742,6 +750,20 @@ class AbModel {
}
}
void _callbackPeerUpdate() {
for (var listener in _peerIdUpdateListeners.values) {
listener();
}
}
void addPeerUpdateListener(String key, VoidCallback listener) {
_peerIdUpdateListeners[key] = listener;
}
void removePeerUpdateListener(String key) {
_peerIdUpdateListeners.remove(key);
}
// #endregion
}

View File

@@ -235,6 +235,17 @@ class TextureModel {
}
}
onViewCameraPageDispose(bool closeSession) async {
final ffi = parent.target;
if (ffi == null) return;
for (final texture in _pixelbufferRenderTextures.values) {
await texture.destroy(closeSession, ffi);
}
for (final texture in _gpuRenderTextures.values) {
await texture.destroy(closeSession, ffi);
}
}
ensureControl(int display) {
var ctl = _control[display];
if (ctl == null) {

View File

@@ -30,8 +30,15 @@ enum SortBy {
class JobID {
int _count = 0;
int next() {
_count++;
return _count;
String v = bind.mainGetCommonSync(key: 'transfer-job-id');
try {
return int.parse(v);
} catch (e) {
// unreachable. But we still handle it to make it safe.
// If we return -1, we have to check it in the caller.
_count++;
return _count;
}
}
}

View File

@@ -12,16 +12,20 @@ import '../utils/http_service.dart' as http;
class GroupModel {
final RxBool groupLoading = false.obs;
final RxString groupLoadError = "".obs;
final RxList<DeviceGroupPayload> deviceGroups = RxList.empty(growable: true);
final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<Peer> peers = RxList.empty(growable: true);
final RxString selectedUser = ''.obs;
final RxString searchUserText = ''.obs;
final RxBool isSelectedDeviceGroup = false.obs;
final RxString selectedAccessibleItemName = ''.obs;
final RxString searchAccessibleItemNameText = ''.obs;
WeakReference<FFI> parent;
var initialized = false;
var _cacheLoadOnceFlag = false;
var _statusCode = 200;
bool get emtpy => users.isEmpty && peers.isEmpty;
final Map<String, VoidCallback> _peerIdUpdateListeners = {};
bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty;
late final Peers peersModel;
@@ -55,6 +59,12 @@ class GroupModel {
}
Future<void> _pull() async {
List<DeviceGroupPayload> tmpDeviceGroups = List.empty(growable: true);
if (!await _getDeviceGroups(tmpDeviceGroups)) {
// old hbbs doesn't support this api
// return;
}
tmpDeviceGroups.sort((a, b) => a.name.compareTo(b.name));
List<UserPayload> tmpUsers = List.empty(growable: true);
if (!await _getUsers(tmpUsers)) {
return;
@@ -63,6 +73,7 @@ class GroupModel {
if (!await _getPeers(tmpPeers)) {
return;
}
deviceGroups.value = tmpDeviceGroups;
// me first
var index = tmpUsers
.indexWhere((user) => user.name == gFFI.userModel.userName.value);
@@ -71,8 +82,9 @@ class GroupModel {
tmpUsers.insert(0, user);
}
users.value = tmpUsers;
if (!users.any((u) => u.name == selectedUser.value)) {
selectedUser.value = '';
if (!users.any((u) => u.name == selectedAccessibleItemName.value) &&
!deviceGroups.any((d) => d.name == selectedAccessibleItemName.value)) {
selectedAccessibleItemName.value = '';
}
// recover online
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
@@ -82,6 +94,64 @@ class GroupModel {
.map((e) => e.online = true)
.toList();
groupLoadError.value = '';
_callbackPeerUpdate();
}
Future<bool> _getDeviceGroups(
List<DeviceGroupPayload> tmpDeviceGroups) async {
final api = "${await bind.mainGetApiServer()}/api/device-group/accessible";
try {
var uri0 = Uri.parse(api);
final pageSize = 100;
var total = 0;
int current = 0;
do {
current += 1;
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
});
final resp = await http.get(uri, headers: getHttpHeaders());
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final user in data) {
final u = DeviceGroupPayload.fromJson(user);
int index = tmpDeviceGroups.indexWhere((e) => e.name == u.name);
if (index < 0) {
tmpDeviceGroups.add(u);
} else {
tmpDeviceGroups[index] = u;
}
}
}
}
}
} while (current * pageSize < total);
return true;
} catch (err) {
debugPrint('get accessible device groups: $err');
// old hbbs doesn't support this api
// groupLoadError.value =
// '${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
return false;
}
Future<bool> _getUsers(List<UserPayload> tmpUsers) async {
@@ -225,6 +295,7 @@ class GroupModel {
try {
final map = (<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'),
"device_groups": deviceGroups.map((e) => e.toGroupCacheJson()).toList(),
"users": users.map((e) => e.toGroupCacheJson()).toList(),
'peers': peers.map((e) => e.toGroupCacheJson()).toList()
});
@@ -244,8 +315,14 @@ class GroupModel {
if (groupLoading.value) return;
final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return;
deviceGroups.clear();
users.clear();
peers.clear();
if (data['device_groups'] is List) {
for (var u in data['device_groups']) {
deviceGroups.add(DeviceGroupPayload.fromJson(u));
}
}
if (data['users'] is List) {
for (var u in data['users']) {
users.add(UserPayload.fromJson(u));
@@ -255,6 +332,7 @@ class GroupModel {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
_callbackPeerUpdate();
}
} catch (e) {
debugPrint("load group cache: $e");
@@ -263,9 +341,24 @@ class GroupModel {
reset() async {
groupLoadError.value = '';
deviceGroups.clear();
users.clear();
peers.clear();
selectedUser.value = '';
selectedAccessibleItemName.value = '';
await bind.mainClearGroup();
}
void _callbackPeerUpdate() {
for (var listener in _peerIdUpdateListeners.values) {
listener();
}
}
void addPeerUpdateListener(String key, VoidCallback listener) {
_peerIdUpdateListeners[key] = listener;
}
void removePeerUpdateListener(String key) {
_peerIdUpdateListeners.remove(key);
}
}

View File

@@ -18,7 +18,7 @@ import '../common.dart';
import '../consts.dart';
/// Mouse button enum.
enum MouseButtons { left, right, wheel }
enum MouseButtons { left, right, wheel, back }
const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
@@ -155,6 +155,8 @@ extension ToString on MouseButtons {
return 'right';
case MouseButtons.wheel:
return 'wheel';
case MouseButtons.back:
return 'back';
}
}
}
@@ -367,6 +369,7 @@ class InputModel {
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
InputModel(this.parent) {
sessionId = parent.target!.sessionId;
@@ -469,6 +472,7 @@ class InputModel {
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
if (!isInputSourceFlutter) {
if (isDesktop) {
return KeyEventResult.handled;
@@ -523,6 +527,7 @@ class InputModel {
KeyEventResult handleKeyEvent(KeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
if (!isInputSourceFlutter) {
if (isDesktop) {
return KeyEventResult.handled;
@@ -722,6 +727,7 @@ class InputModel {
/// [press] indicates a click event(down and up).
void inputKey(String name, {bool? down, bool? press}) {
if (!keyboardPerm) return;
if (isViewCamera) return;
bind.sessionInputKey(
sessionId: sessionId,
name: name,
@@ -783,6 +789,7 @@ class InputModel {
/// Send scroll event with scroll distance [y].
Future<void> scroll(int y) async {
if (isViewCamera) return;
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json
@@ -806,6 +813,7 @@ class InputModel {
/// Send mouse press event.
Future<void> sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return;
if (isViewCamera) return;
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value})));
@@ -832,6 +840,7 @@ class InputModel {
/// Send mouse movement event with distance in [x] and [y].
Future<void> moveMouse(double x, double y) async {
if (!keyboardPerm) return;
if (isViewCamera) return;
var x2 = x.toInt();
var y2 = y.toInt();
await bind.sessionSendMouse(
@@ -855,6 +864,7 @@ class InputModel {
_lastScale = 1.0;
_stopFling = true;
if (isViewOnly) return;
if (isViewCamera) return;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
}
@@ -863,6 +873,7 @@ class InputModel {
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
if (isViewOnly) return;
if (isViewCamera) return;
if (peerPlatform != kPeerPlatformAndroid) {
final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale;
@@ -902,6 +913,7 @@ class InputModel {
handlePointerEvent('touch', kMouseEventTypePanUpdate,
Offset(x.toDouble(), y.toDouble()));
} else {
if (isViewCamera) return;
bind.sessionSendMouse(
sessionId: sessionId,
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
@@ -910,6 +922,7 @@ class InputModel {
}
void _scheduleFling(double x, double y, int delay) {
if (isViewCamera) return;
if ((x == 0 && y == 0) || _stopFling) {
_fling = false;
return;
@@ -961,6 +974,7 @@ class InputModel {
}
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (isViewCamera) return;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
return;
@@ -992,6 +1006,7 @@ class InputModel {
_remoteWindowCoords = [];
_windowRect = null;
if (isViewOnly) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) {
isPhysicalMouse.value = false;
@@ -1005,6 +1020,7 @@ class InputModel {
void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
@@ -1013,6 +1029,7 @@ class InputModel {
void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) {
Future.delayed(Duration.zero, () async {
@@ -1047,6 +1064,7 @@ class InputModel {
void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return;
if (isViewCamera) return;
if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt();
@@ -1144,6 +1162,7 @@ class InputModel {
}
final evt = PointerEventToRust(kind, type, evtValue).toJson();
if (isViewCamera) return;
bind.sessionSendPointer(
sessionId: sessionId, msg: json.encode(modify(evt)));
}
@@ -1175,6 +1194,7 @@ class InputModel {
Offset offset, {
bool onExit = false,
}) {
if (isViewCamera) return;
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
@@ -1426,7 +1446,18 @@ class InputModel {
}
}
void onMobileBack() => tap(MouseButtons.right);
void onMobileBack() {
final minBackButtonVersion = "1.3.8";
final peerVersion =
parent.target?.ffiModel.pi.version ?? minBackButtonVersion;
var btn = MouseButtons.back;
// For compatibility with old versions
if (versionCmp(peerVersion, minBackButtonVersion) < 0) {
btn = MouseButtons.right;
}
tap(btn);
}
void onMobileHome() => tap(MouseButtons.wheel);
Future<void> onMobileApps() async {
sendMouse('down', MouseButtons.wheel);

View File

@@ -9,7 +9,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
@@ -19,6 +18,7 @@ import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/printer_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -407,15 +407,191 @@ class FfiModel with ChangeNotifier {
parent.target?.fileModel.sendEmptyDirs(evt);
}
} else if (name == "record_status") {
if (desktopType == DesktopType.remote || isMobile) {
if (desktopType == DesktopType.remote ||
desktopType == DesktopType.viewCamera ||
isMobile) {
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
}
} else if (name == "printer_request") {
_handlePrinterRequest(evt, sessionId, peerId);
} else {
debugPrint('Event is not handled in the fixed branch: $name');
}
};
}
_handlePrinterRequest(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
final id = evt['id'];
final path = evt['path'];
final dialogManager = parent.target!.dialogManager;
dialogManager.show((setState, close, context) {
PrinterOptions printerOptions = PrinterOptions.load();
final saveSettings = mainGetLocalBoolOptionSync(kKeyPrinterSave).obs;
final dontShowAgain = false.obs;
final Rx<String> selectedPrinterName = printerOptions.printerName.obs;
final printerNames = printerOptions.printerNames;
final defaultOrSelectedGroupValue =
(printerOptions.action == kValuePrinterIncomingJobDismiss
? kValuePrinterIncomingJobDefault
: printerOptions.action)
.obs;
onRatioChanged(String? value) {
defaultOrSelectedGroupValue.value =
value ?? kValuePrinterIncomingJobDefault;
}
onSubmit() {
final printerName = defaultOrSelectedGroupValue.isEmpty
? ''
: selectedPrinterName.value;
bind.sessionPrinterResponse(
sessionId: sessionId, id: id, path: path, printerName: printerName);
if (saveSettings.value || dontShowAgain.value) {
bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName);
bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction,
value: defaultOrSelectedGroupValue.value);
}
if (dontShowAgain.value) {
mainSetLocalBoolOption(kKeyPrinterAllowAutoPrint, true);
}
close();
}
onCancel() {
if (dontShowAgain.value) {
bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction,
value: kValuePrinterIncomingJobDismiss);
}
close();
}
final printerItemHeight = 30.0;
final selectionAreaHeight =
printerItemHeight * min(8.0, max(printerNames.length, 3.0));
final content = Column(
children: [
Text(translate('print-incoming-job-confirm-tip')),
Row(
children: [
Obx(() => Radio<String>(
value: kValuePrinterIncomingJobDefault,
groupValue: defaultOrSelectedGroupValue.value,
onChanged: onRatioChanged)),
GestureDetector(
child: Text(translate('use-the-default-printer-tip')),
onTap: () => onRatioChanged(kValuePrinterIncomingJobDefault)),
],
),
Column(
children: [
Row(children: [
Obx(() => Radio<String>(
value: kValuePrinterIncomingJobSelected,
groupValue: defaultOrSelectedGroupValue.value,
onChanged: onRatioChanged)),
GestureDetector(
child: Text(translate('use-the-selected-printer-tip')),
onTap: () =>
onRatioChanged(kValuePrinterIncomingJobSelected)),
]),
SizedBox(
height: selectionAreaHeight,
width: 500,
child: ListView.builder(
itemBuilder: (context, index) {
return Obx(() => GestureDetector(
child: Container(
decoration: BoxDecoration(
color: selectedPrinterName.value ==
printerNames[index]
? (defaultOrSelectedGroupValue.value ==
kValuePrinterIncomingJobSelected
? MyTheme.button
: MyTheme.button.withOpacity(0.5))
: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(
Radius.circular(5.0),
),
),
key: ValueKey(printerNames[index]),
height: printerItemHeight,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
printerNames[index],
style: TextStyle(fontSize: 14),
),
),
),
),
onTap: defaultOrSelectedGroupValue.value ==
kValuePrinterIncomingJobSelected
? () {
selectedPrinterName.value =
printerNames[index];
}
: null,
));
},
itemCount: printerNames.length),
),
],
),
Row(
children: [
Obx(() => Checkbox(
value: saveSettings.value,
onChanged: (value) {
if (value != null) {
saveSettings.value = value;
mainSetLocalBoolOption(kKeyPrinterSave, value);
}
})),
GestureDetector(
child: Text(translate('save-settings-tip')),
onTap: () {
saveSettings.value = !saveSettings.value;
mainSetLocalBoolOption(kKeyPrinterSave, saveSettings.value);
}),
],
),
Row(
children: [
Obx(() => Checkbox(
value: dontShowAgain.value,
onChanged: (value) {
if (value != null) {
dontShowAgain.value = value;
}
})),
GestureDetector(
child: Text(translate('dont-show-again-tip')),
onTap: () {
dontShowAgain.value = !dontShowAgain.value;
}),
],
),
],
);
return CustomAlertDialog(
title: Text(translate('Incoming Print Job')),
content: content,
actions: [
dialogButton('OK', onPressed: onSubmit),
dialogButton('Cancel', onPressed: onCancel),
],
onSubmit: onSubmit,
onCancel: onCancel,
);
});
}
_handleUseTextureRender(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
@@ -501,7 +677,9 @@ class FfiModel with ChangeNotifier {
final display = int.parse(evt['display']);
if (_pi.currentDisplay != kAllDisplayValue) {
if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) {
if (bind.peerGetSessionsCount(
id: peerId, connType: parent.target!.connType.index) >
1) {
if (display != _pi.currentDisplay) {
return;
}
@@ -809,7 +987,9 @@ class FfiModel with ChangeNotifier {
_pi.primaryDisplay = currentDisplay;
}
if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) {
if (bind.peerGetSessionsCount(
id: peerId, connType: parent.target!.connType.index) <=
1) {
_pi.currentDisplay = currentDisplay;
}
@@ -827,9 +1007,11 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: kOptionTouchMode) !=
'';
}
// FIXME: handle ViewCamera ConnType independently.
if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady();
} else if (connType == ConnType.defaultConn) {
} else if (connType == ConnType.defaultConn ||
connType == ConnType.viewCamera) {
List<Display> newDisplays = [];
List<dynamic> displays = json.decode(evt['displays']);
for (int i = 0; i < displays.length; ++i) {
@@ -859,7 +1041,7 @@ class FfiModel with ChangeNotifier {
bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionToggleViewOnly));
}
if (connType == ConnType.defaultConn) {
if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) {
final platformAdditions = evt['platform_additions'];
if (platformAdditions != null && platformAdditions != '') {
try {
@@ -1500,13 +1682,15 @@ class CanvasModel with ChangeNotifier {
return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0);
}
updateSize() => _size = getSize();
updateViewStyle({refreshMousePos = true, notify = true}) async {
final style = await bind.sessionGetViewStyle(sessionId: sessionId);
if (style == null) {
return;
}
_size = getSize();
updateSize();
final displayWidth = getDisplayWidth();
final displayHeight = getDisplayHeight();
final viewStyle = ViewStyle(
@@ -1543,7 +1727,7 @@ class CanvasModel with ChangeNotifier {
_resetCanvasOffset(int displayWidth, int displayHeight) {
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) {
if (isMobile) {
_moveToCenterCursor();
}
}
@@ -1736,7 +1920,8 @@ class CanvasModel with ChangeNotifier {
_timerMobileFocusCanvasCursor?.cancel();
_timerMobileFocusCanvasCursor =
Timer(Duration(milliseconds: 100), () async {
await updateViewStyle(refreshMousePos: false, notify: false);
updateSize();
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
notifyListeners();
});
}
@@ -2427,6 +2612,8 @@ class CursorModel with ChangeNotifier {
_x = -10000;
_x = -10000;
_image = null;
_firstUpdateMouseTime = null;
gotMouseControl = true;
disposeImages();
_clearCache();
@@ -2571,7 +2758,8 @@ class ElevationModel with ChangeNotifier {
onPortableServiceRunning(bool running) => _running = running;
}
enum ConnType { defaultConn, fileTransfer, portForward, rdp }
// The index values of `ConnType` are same as rust protobuf.
enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera }
/// Flutter state manager and data communication with the Rust core.
class FFI {
@@ -2646,10 +2834,11 @@ class FFI {
ffiModel.waitForImageTimer = null;
}
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
/// Start with the given [id]. Only transfer file if [isFileTransfer], only view camera if [isViewCamera], only port forward if [isPortForward].
void start(
String id, {
bool isFileTransfer = false,
bool isViewCamera = false,
bool isPortForward = false,
bool isRdp = false,
String? switchUuid,
@@ -2664,9 +2853,15 @@ class FFI {
closed = false;
auditNote = '';
if (isMobile) mobileReset();
assert(!(isFileTransfer && isPortForward), 'more than one connect type');
assert(
(!(isPortForward && isViewCamera)) &&
(!(isViewCamera && isPortForward)) &&
(!(isPortForward && isFileTransfer)),
'more than one connect type');
if (isFileTransfer) {
connType = ConnType.fileTransfer;
} else if (isViewCamera) {
connType = ConnType.viewCamera;
} else if (isPortForward) {
connType = ConnType.portForward;
} else {
@@ -2686,6 +2881,7 @@ class FFI {
sessionId: sessionId,
id: id,
isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isPortForward: isPortForward,
isRdp: isRdp,
switchUuid: switchUuid ?? '',
@@ -2701,7 +2897,10 @@ class FFI {
return;
}
final addRes = bind.sessionAddExistedSync(
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
id: id,
sessionId: sessionId,
displays: Int32List.fromList(displays),
isViewCamera: isViewCamera);
if (addRes != '') {
debugPrint(
'Unreachable, failed to add existed session to $id, $addRes');
@@ -2712,6 +2911,11 @@ class FFI {
if (isDesktop && connType == ConnType.defaultConn) {
textureModel.updateCurrentDisplay(display ?? 0);
}
// FIXME: separate cameras displays or shift all indices.
if (isDesktop && connType == ConnType.viewCamera) {
// FIXME: currently the default 0 is not used.
textureModel.updateCurrentDisplay(display ?? 0);
}
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
// Though the stream is returned immediately, the stream may not be ready.
@@ -2988,6 +3192,9 @@ class PeerInfo with ChangeNotifier {
bool get isAmyuniIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
bool get isSupportViewCamera =>
platformAdditions[kPlatformAdditionsSupportViewCamera] == true;
Display? tryGetDisplay({int? display}) {
if (displays.isEmpty) {
return null;

View File

@@ -60,14 +60,14 @@ class PlatformFFI {
}
bool registerEventHandler(
String eventName, String handlerName, HandleEvent handler) {
String eventName, String handlerName, HandleEvent handler, {bool replace = false}) {
debugPrint('registerEventHandler $eventName $handlerName');
var handlers = _eventHandlers[eventName];
if (handlers == null) {
_eventHandlers[eventName] = {handlerName: handler};
return true;
} else {
if (handlers.containsKey(handlerName)) {
if (!replace && handlers.containsKey(handlerName)) {
return false;
} else {
handlers[handlerName] = handler;

View File

@@ -19,6 +19,7 @@ class Peer {
String rdpUsername;
bool online = false;
String loginName; //login username
String device_group_name;
bool? sameServer;
String getId() {
@@ -41,6 +42,7 @@ class Peer {
rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '',
device_group_name = json['device_group_name'] ?? '',
sameServer = json['same_server'];
Map<String, dynamic> toJson() {
@@ -57,6 +59,7 @@ class Peer {
"rdpPort": rdpPort,
"rdpUsername": rdpUsername,
'loginName': loginName,
'device_group_name': device_group_name,
'same_server': sameServer,
};
}
@@ -83,6 +86,7 @@ class Peer {
"hostname": hostname,
"platform": platform,
"login_name": loginName,
"device_group_name": device_group_name,
};
}
@@ -99,6 +103,7 @@ class Peer {
required this.rdpPort,
required this.rdpUsername,
required this.loginName,
required this.device_group_name,
this.sameServer,
});
@@ -116,6 +121,7 @@ class Peer {
rdpPort: '',
rdpUsername: '',
loginName: '',
device_group_name: '',
);
bool equal(Peer other) {
return id == other.id &&
@@ -129,6 +135,7 @@ class Peer {
forceAlwaysRelay == other.forceAlwaysRelay &&
rdpPort == other.rdpPort &&
rdpUsername == other.rdpUsername &&
device_group_name == other.device_group_name &&
loginName == other.loginName;
}
@@ -146,6 +153,7 @@ class Peer {
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername,
loginName: other.loginName,
device_group_name: other.device_group_name,
sameServer: other.sameServer);
}
@@ -157,6 +165,11 @@ class Peers extends ChangeNotifier {
final String name;
final String loadEvent;
List<Peer> peers = List.empty(growable: true);
// Part of the peers that are not in the rest peers list.
// When there're too many peers, we may want to load the front 100 peers first,
// so we can see peers in UI quickly. `restPeerIds` is the rest peers' ids.
// And then load all peers later.
List<String> restPeerIds = List.empty(growable: true);
final GetInitPeers? getInitPeers;
UpdateEvent event = UpdateEvent.load;
static const _cbQueryOnlines = 'callback_query_onlines';
@@ -230,6 +243,12 @@ class Peers extends ChangeNotifier {
} else {
peers = _decodePeers(evt['peers']);
}
restPeerIds = [];
if (evt['ids'] != null) {
restPeerIds = (evt['ids'] as String).split(',');
}
for (var peer in peers) {
final state = onlineStates[peer.id];
peer.online = state != null && state != false;

View File

@@ -28,14 +28,14 @@ class PeerTabModel with ChangeNotifier {
'Favorites',
'Discovered',
'Address book',
'Group',
'Accessible devices',
];
static const List<IconData> icons = [
Icons.access_time_filled,
Icons.star,
Icons.explore,
IconFont.addressBook,
Icons.group,
IconFont.deviceGroupFill,
];
List<bool> isEnabled = List.from([
true,

View File

@@ -0,0 +1,48 @@
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/platform_model.dart';
class PrinterOptions {
String action;
List<String> printerNames;
String printerName;
PrinterOptions(
{required this.action,
required this.printerNames,
required this.printerName});
static PrinterOptions load() {
var action = bind.mainGetLocalOption(key: kKeyPrinterIncommingJobAction);
if (![
kValuePrinterIncomingJobDismiss,
kValuePrinterIncomingJobDefault,
kValuePrinterIncomingJobSelected
].contains(action)) {
action = kValuePrinterIncomingJobDefault;
}
final printerNames = getPrinterNames();
var selectedPrinterName = bind.mainGetLocalOption(key: kKeyPrinterSelected);
if (!printerNames.contains(selectedPrinterName)) {
if (action == kValuePrinterIncomingJobSelected) {
action = kValuePrinterIncomingJobDefault;
bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction,
value: kValuePrinterIncomingJobDefault);
if (printerNames.isEmpty) {
selectedPrinterName = '';
} else {
selectedPrinterName = printerNames.first;
}
bind.mainSetLocalOption(
key: kKeyPrinterSelected, value: selectedPrinterName);
}
}
return PrinterOptions(
action: action,
printerNames: printerNames,
printerName: selectedPrinterName);
}
}

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