Compare commits

..

406 Commits
1.3.0 ... 1.3.7

Author SHA1 Message Date
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
fufesou
a23822074e feat: Android, opt, check update on startup (#10165)
* feat: Android, opt, check update on startup

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

* refact: check update only on startup

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

* fix: Android, "Download new version"

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-04 17:10:32 +08:00
21pages
3d17bf4990 linux dynamic load libva (#10171)
1. Linux dynamic load libva, which can fix lack of libva dependency for
   appimage or flatpak, also fix libva version mismatch between build
and run.
2. Remove libvdpau, it's not used, and add libva2 explicitly for deb and
   appimage
3. Print FFmpeg configure log to know the actual codecs.

Test
*  ubuntu 22.04 x64
  - [x] deb
  - [x] flatpak
  - [x] appimage
* ubuntu 18.04
  * deb: fcntl64 not found
  - [x]:appimage
  - [ ]: platpak
hwcodec example:
  - [x]: combination of lacking any of libva2, libva-x11-2, libva-drm2,
    intel-media-va-driver
- [ ] federa
- [ ] arch
- [ ] arm64: my ci can't finish arm64 building

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-04 17:10:10 +08:00
Yevhen Popok
fd67be4a16 Update uk.rs (#10162) 2024-12-04 10:31:13 +08:00
fufesou
e6edf39305 fix: support emptry folder transfer for web (#10151)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-03 14:22:20 +08:00
21pages
34d2c62781 set id/relay server with a dialog (#10150)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-03 14:14:29 +08:00
fufesou
bd0a33e467 fix: linux, window, workaround, mint, mate (#10146)
* refact: linux, window, workaround, mint, mate

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

* refact: case insensitive

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-03 01:02:41 +08:00
rustdesk
b8d36b6558 revert multi-window plugin 2024-12-02 22:29:37 +08:00
solokot
f38d89aaee Update ru.rs (#10143) 2024-12-02 18:30:24 +08:00
21pages
773b9d6645 win7 uses soft rendering by default (#10139)
win7 vm got black screen on remote window with texture rendering

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-02 17:10:34 +08:00
rustdesk
dea99ffb3a fix rustdesk exit crash 2024-12-02 16:11:12 +08:00
jkh0kr
3251045e22 Update ko.rs (#10138)
Update ko.rs
2024-12-02 15:47:23 +08:00
21pages
dc58c85e30 try fix mac textedit of server config (#10135)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-12-02 12:35:44 +08:00
fufesou
5a2a94d2cc fix: macos, input (#10133)
1. Workaround sticky `Fn` for more keys.
2. Workaround stikey `Help`.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-12-02 00:09:03 +08:00
rustdesk
f330953f4f bump to 1.3.4 2024-12-01 18:49:24 +08:00
fufesou
8d4c86fe7f fix: workaround, linux window, transparent rounded corner (#10128)
* fix: linux window, rounded corner

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

* Update my_application.cc

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-12-01 17:19:21 +08:00
Kleofass
f8c2713c5b Update lv.rs (#10124) 2024-12-01 00:27:39 +08:00
fufesou
082a66b282 refact: remove flutter_improved_scrolling (#10120)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-30 15:01:44 +08:00
21pages
743b0ce8ce fix mediacodec patch (#10119)
ensure set_parameters_id is not null

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-30 13:29:03 +08:00
21pages
9d9b67aca5 update flutter texture rgba renderer plugin, remove switch rgba (#10070)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-30 12:19:42 +08:00
21pages
d60b5a6ca0 videotoolbox/mediacodec support changing bitrate dynamically (#10117)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-30 11:44:51 +08:00
Vasyl Gello
e0ed6ee986 Fix F-Droid build and bump Android NDK to r27c (#10105)
* Fix fdroid build

* Refactor recent @fufesou edits to reflect the fact that
  .gclient file is needed only on x86 to build jit-release
  version of flutter-engine and `flutter-sdk` directory is
  not affected by flutter version checkouts

* Install cargo-ndk and flutter-rust-codegen with `--locked`
  argument to avoid bumping `cargo-platform` to require
  newer Rust toolchain

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

* Bump Android NDK to r27c

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

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-11-30 09:24:45 +08:00
21pages
d3f0c80e94 "Untagged" tag uses the theme accent color (#10111)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-30 09:24:05 +08:00
fufesou
b32ff87c6e fix: android, pan, canvas, remove toInt() (#10103)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-29 22:39:07 +08:00
rustdesk
b91b49229a enable our engine to fix dart supporting win7, https://github.com/rustdesk/rustdesk/issues/10085#issuecomment-2506485955 2024-11-29 19:10:57 +08:00
fufesou
afc8bb71dc feat: mobile, key help tool, more keys (#10068)
* feat: mobile, key help tool, vk_enter

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

* Mobile, add more function keys

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

* Mobile, more virtual function keys

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

* uinput, menu maps key_compose

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-29 00:56:38 +08:00
XLion
4f86169f7f Update tw.rs (#10076) 2024-11-28 00:10:47 +08:00
Leo Mozoloa
2cf43042e6 Update fr.rs (#10075) 2024-11-27 23:00:41 +08:00
BoyChai
734fb8d6f7 Update README-ZH.md (#10069)
Modify Alibaba Cloud apt source
2024-11-27 10:47:09 +08:00
bovirus
3c7f6d3127 Italian language update (#10067) 2024-11-27 07:37:49 +08:00
21pages
b99c540210 add "Untagged" to filter addressbook peers without tags (#10063)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-26 20:35:17 +08:00
fufesou
84dab0e96f Fix/android keyboard map mode workaround (#10064)
* fix: Android, keyboard, map mode, workaround

The `KeyEvent.physicalKey.usbHidUsage` are wrong if using Microsoft
SwiftKey keyboard.

`window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)` is a workaround for this issue.

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

* fix: clear callback on first image

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

* Android disable soft keyboard in remote page if not editing.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-26 20:33:54 +08:00
fufesou
458a88fb89 fix: mobile autocomplete options (#10060)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-26 16:24:51 +08:00
Yevhen Popok
8a70932cd6 Update Ukrainian UI translation (#10056) 2024-11-26 09:51:27 +08:00
flusheDData
edfae98a01 Update es.rs (#10055)
New term added
2024-11-25 23:46:28 +08:00
fufesou
d61c99b105 fix: Android 29, crash on restart and reconn (#10054)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-25 21:49:09 +08:00
21pages
30a11bfe0a android wait 3s for isStart flag (#10053)
The normal process is that `startCapture` and `VideoService::run` run in parallel,  the `run` function waits for startCapture to complete, then sets the scale, and subsequently calls `stopCapture` and `startCapture`. If the `run` function does not wait long enough, `startCapture` initializes the surface with the original width and height, but the `start` flag is still false, meaning it can't call `stopCapture` and `startCapture`. This results in only capturing the upper-left portion of the virtual display.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-25 21:45:58 +08:00
fufesou
9d2bdfefb1 feat: update build_fdroid.sh (#10040)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-25 11:12:43 +08:00
21pages
34b93c6f83 fix aom pts (#10042)
the old pts/duration is wrong, use timebase 1/1000 like other codecs, not found any differences.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-25 11:09:17 +08:00
21pages
152d0ce74b install vaapi dependencies before vcpkg installs ffmpeg (#10035)
linux vaapi encoding/decoding lost since installing ffmpeg with
vcpkg

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-24 21:07:03 +08:00
rustdesk
32a3bcdc4f no custom engine 2024-11-24 10:05:00 +09:00
zuiyu
314c93b210 Create empty dir on send files in local (#9993)
* feat: Add empty dirs on sendfiles

* Update connection.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-11-23 23:09:11 +08:00
rustdesk
b64f6271e2 typo 2024-11-24 00:08:39 +09:00
RustDesk
ac044c4049 Update config.toml (#10029) 2024-11-23 22:54:43 +08:00
fufesou
0973f51df9 feat: macos, audio, loopback (#10025)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-23 19:41:27 +09:00
21pages
02b046bdbf fix hwcodec ram quality change not work (#10024)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-23 16:36:13 +08:00
Andrzej Rudnik
02c274aeb6 Updated Polish translation (#10019)
* Update pl.rs

* Update README-PL.md
2024-11-23 08:41:25 +08:00
fufesou
ab6a6ca17d fix: ci macos (#10020)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-23 08:38:30 +08:00
21pages
28d38cd71d avoid invalid recording files and fix removing little recording file (#10012)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-22 17:19:22 +08:00
rustdesk
b487f297b8 flutter 3.24.5 2024-11-22 16:58:08 +09:00
21pages
64654ee7cf seperate video decoding thread for each display (#9968)
* seperate video decoding thread for each display

1. Separate Video Decoding Thread for Each Display
2. Fix Decode Errors When Clearing the Queue
Previously, on-flight frames after clearing the queue could not be decoded successfully. This issue can be resolved by setting a discard_queue flag when sending a refresh message. The flag will be reset upon receiving a keyframe.

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

* update video format along with fps to flutter

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

* Fix keyframe interval when auto record outgoing sessions

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-22 00:02:25 +08:00
Alex Rijckaert
1c99eb5500 Update nl.rs (#9997) 2024-11-21 19:12:24 +08:00
zyl
74dd0c8fa0 fix mis-align problem when converting &[u8] to &[f32] (#9986)
* fix: windows, improve audio buffer (#9770)

* .

* fix statics does not record

and avoid channel changing when drio audio when audio is stero

* add some commence

* fix mis-align problem when converting &[u8] to &[f32]

* add safety commence

* revert client.rs

* avoid tmp lifetime extends

* avoid move in loop

* avoid use after drop

* another use after free

* another use after free

* make code more reasonable

---------

Co-authored-by: zylthinking <zhaoyulong@qianxin.com>
2024-11-21 13:36:11 +08:00
rustdesk
d26fea41ee upgrade The-Fat-Controller 2024-11-21 02:26:51 +09:00
21pages
bc211c8031 A=b, A case insensitive (#9976)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-20 19:44:24 +08:00
Mr-Update
d4cb7d68c5 Update de.rs (#9975) 2024-11-20 19:41:43 +08:00
KAYUII
608d7d55d5 add env VCPKG_INSTALLED_ROOT (#9985) 2024-11-20 19:41:30 +08:00
fufesou
4a49fbe4a6 fix: privacy mode 2 (#9972)
Do not change the resolutions when setting the new primary display.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-19 17:29:28 +08:00
fufesou
f760e21ff8 fix: android w&h, refresh when no connection (#9966)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-19 15:03:00 +08:00
solokot
251e1a3487 Update ru.rs (#9962) 2024-11-19 08:15:50 +08:00
bovirus
b990ff3782 Update Italian language (#9961) 2024-11-19 08:15:37 +08:00
21pages
c5426b0fbc Fix hevc decode error "Could not find ref with POC" (#9960)
1. Dropping frames can cause this error, reset encoder when this
   happens.
2. There are some logic error for clear video queue, because video queue
   message is not cleared. This need to be fixed.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-18 23:03:27 +08:00
fufesou
8b710f62c8 feat: android clipboard, multi-formats (#9950)
* feat: android clipboard, multi-formats

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

* Chore

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

* Remove unused code

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-18 15:43:41 +08:00
21pages
0707e791e8 opt av1 test data (#9954)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-18 15:05:23 +08:00
XLion
a07392e6b8 Add URL and Vendor to RPM spec (#9947) 2024-11-17 21:07:00 +08:00
21pages
9125a68f81 remove flutter install cache flag (#9944)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-17 11:35:53 +08:00
21pages
9ee77a9b92 fix last commit (#9939)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-17 09:21:34 +08:00
21pages
304e0e465d if av1 is not slow in test, av1 takes precedence over vp9 (#9938)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-16 22:31:28 +08:00
RustDesk
5d2bb9c995 Update bug_report.yaml (#9935) 2024-11-16 16:27:51 +08:00
RustDesk
ddc172bdfa Update bug_report.yaml (#9934) 2024-11-16 16:26:26 +08:00
21pages
06c7bc137f linux android use cpal (#9914)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-14 21:01:41 +08:00
rustdesk
9e4cc91a14 use linux for bridge because macos runner network problem 2024-11-13 17:00:58 +08:00
zyl
0a28d09ff8 fix: windows, improve audio buffer (#9770) (#9893)
* fix: windows, improve audio buffer (#9770)

* .

* fix statics does not record

and avoid channel changing when drio audio when audio is stero

* add some commence

---------

Co-authored-by: zylthinking <zhaoyulong@qianxin.com>
2024-11-13 15:35:23 +08:00
fufesou
ab89d84a8f refact: ci, bridge (#9899)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-12 22:10:56 +08:00
HanaKuru
0aa98eac6d Use base64Url encoding for server configuration to ensure compatibility with the command line --config option. (#9897) 2024-11-12 20:15:34 +08:00
fufesou
d4aa2b7ce4 fix: virtual display, headless, wait plug in done (#9895)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-12 20:12:05 +08:00
21pages
35b4535ebc fix aarch64 nightly build (#9881)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-11 16:28:25 +08:00
fufesou
f0be80c253 fix: macos, workaround app close (#9880)
* fix: macos, workaround app close

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

* Update common.dart

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-11-11 13:06:23 +08:00
21pages
a79a9f697b fix "Add to addresssBook" dropdown menu (#9878)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-11 08:39:54 +08:00
fufesou
6082bb2754 fix: save load window rect, Windows, ignore dpi (#9875)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-11 00:35:41 +08:00
rustdesk
4e6a43288e https://github.com/rustdesk/rustdesk/issues/9877 https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain 2024-11-10 23:57:25 +08:00
fufesou
72a1f1161e refact: flutter 3.24.4 (#9874)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-10 11:18:08 +08:00
RustDesk
912f5265f1 Revert "Refact/flutter 3.24.4 (#9870)" (#9871)
This reverts commit 5eb2c31207.
2024-11-09 23:32:18 +08:00
fufesou
5eb2c31207 Refact/flutter 3.24.4 (#9870)
* Update pubspec.lock

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

* refact: flutter 3.24.3

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

* fix: workaround Autocomplete options

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

* Replace engine with rustdesk custom flutter engine

* Update flutter-build.yml to use RustDesk flutter engine

* Fix the problem of missing extraction file directory windows-x64-release

* Update pubspec.lock.3.22.3

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

* remove pubspec.lock.3.22.3

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

* upgrade flutter android to 3.24.4

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: vitoway <vitoway@email.com>
Co-authored-by: vitoway <167743630+vitoway@users.noreply.github.com>
2024-11-09 20:51:47 +08:00
Lee Jong Mun
062c8d582c kor translation update (#9866) 2024-11-09 11:37:54 +08:00
XLion
68b07505ab Update tw.rs (#9863) 2024-11-08 22:18:30 +08:00
fufesou
d3efcd4223 fix: mobile, soft keyboard (#9860)
Switching the input method, don't affect the canvas.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-08 15:01:36 +08:00
rustdesk
a277b022ff bump to 1.3.3 2024-11-08 15:00:49 +08:00
21pages
740c5358ab rotate ID3D11Texture2D (#9772)
* Rotate ID3D11Texture2D after duplication with d3d11 video processor.
* If display is not rotated, nothing will be created; If the rotation
  fails, it will use the old fallback logic

TODO:
* If changing from Landscape to Landscape(flipped) during capture, the resolution is
  not changed, video service fallback to gdi directly.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-08 12:12:10 +08:00
fufesou
7978e0301d fix: input mobile -> Android (#9767)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-08 12:11:56 +08:00
fufesou
0f070b0108 revert: 9644, iOS, Korean input (#9857)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 22:54:14 +08:00
fufesou
6f0cb3b8c2 fix: mobile, two fingers cale, no tapdown (#9856)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 22:36:56 +08:00
fufesou
d0ef52e418 fix: touch input, ensure message orders (#9855)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 21:23:41 +08:00
fufesou
69277dd16b fix: mobile, don't adjust canvas on gesture help show up (#9846)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 14:58:10 +08:00
fufesou
faf97c770c fix: mobile, cursor mode, don't reset canvas (#9843)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-06 21:59:23 +08:00
notlin4
78088360ca Fix traditional Chinese localization (#9833) 2024-11-06 07:59:12 +08:00
fufesou
5cfd1701fb fix: mobile input, touch mode, in display (#9827)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-05 17:55:38 +08:00
21pages
a4bd23c9de fix missing window focus service on windows/macos (#9824)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-05 11:28:27 +08:00
fufesou
12c1337b7b fix: mobile mouse mode, cursor range (#9811)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-04 22:37:21 +08:00
fufesou
040253b319 fix: mobile cursor focus (#9803)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-01 15:40:57 +08:00
fufesou
44fa83d080 fix: android input, soft keyboard, mouse mode (#9797)
Cursor movement in the remote screen.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-01 11:25:38 +08:00
21pages
4f7e10bac6 Revert "Change the minimum value of the bitrate slider to 5" (#9795)
* Revert "Change the minimum value of the bitrate slider to 5 (#9480)"

This reverts commit beb1084e87.

* Revert "Change the value of kMinQuality to 5  (#9508)"

This reverts commit d563372a91.
2024-10-31 22:57:39 +08:00
jkh0kr
d1fdcf1b16 Update ko.rs (#9792) 2024-10-31 12:01:37 +08:00
fufesou
4c12b83068 fix: android input do not handle composing (#9790)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-31 10:20:57 +08:00
21pages
f86c88b3d8 refresh icon not visible when not using one-time password (#9791)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-31 10:11:42 +08:00
fufesou
697dd87383 Refact/mobile remove adjust 4 soft keyabord (#9787)
* refact: remove adjust for soft keyboard

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

* mobile, do not set the view style after scale end

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-31 10:11:00 +08:00
fufesou
0b8cccd8be fix: mobile view style, on conn (#9786)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 19:00:07 +08:00
Kleofass
32dbc0c8fb Update lv.rs (#9785) 2024-10-30 17:08:32 +08:00
fufesou
bae4a2c710 Fix/android check normal usbhid usage (#9784)
* fix: android check normal usbhid usage

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

* fix: android input, ignore composing if is deleting

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 15:29:52 +08:00
RustDesk
711ed28846 Revert "revert: #9769 (#9780)" (#9783)
This reverts commit 0f5f9f6524.
2024-10-30 13:12:18 +08:00
fufesou
e6c5064ce5 fix: android input, Backspace and Enter (#9782)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 13:05:25 +08:00
fufesou
0f5f9f6524 revert: #9769 (#9780)
Not sure TextEditingController.addListener() can handle all composing
changes.

https://github.com/rustdesk/rustdesk/issues/7727#issuecomment-2445721499

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 12:17:13 +08:00
21pages
ce7867c1c0 fix wrong display of custom clients when approval mode is not set (#9779)
when approve-mode is not set, the approve mode option shows as
password, it's `both` approve mode in rust, so only ui is wrong.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-30 11:29:39 +08:00
rustdesk
415d2c5c60 OPTION_VERIFICATION_METHOD 2024-10-29 22:52:46 +08:00
fufesou
a289eae07c fix: mobile -> mobile, long press (#9775)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-29 19:57:29 +08:00
RustDesk
1c9b456456 Update bug_report.yaml (#9771) 2024-10-29 15:33:12 +08:00
fufesou
eba19e67ff fix: mobile input (#9769)
1. Map mode. Check if the KeyEvent's usbHidUsage is correct.
2. Korean input, use listener to handle composing state.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-29 14:19:16 +08:00
John Fowler
adc5a7be51 Update hu.rs (#9762)
Making the Hungarian language file complete. Adding tips in Hungarian.
2024-10-29 13:55:13 +08:00
pppanghu77
26d23d588a fix: [translations] Add the translation in cn.rs (#9768)
Add the translation in cn.rs

Log: Add the translation in cn.rs
2024-10-29 13:54:16 +08:00
fufesou
f0450db203 refact: mobile reset canvas (#9766)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-29 09:49:04 +08:00
21pages
3a75947553 add some missing web bridge (#9763)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-28 09:40:16 +08:00
fufesou
c565849062 fix: Function "LockScreen" on macOS since "ignore_flags" in enigo is introduced. (#9757)
1. LockScreen after connection is established.
2. LockScreen after "Map mode" or "Translate mode" keys are sent.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-27 20:36:21 +08:00
21pages
40e8f0d307 revert missing retry and opt keep session (#9755)
* Revert "fix missing retry (#8750)"

If `hasRetry` is true: there is a retry timeout;
If `hasRetry` is false: there is no retry button;

In
https://github.com/rustdesk/rustdesk/discussions/8748#discussioncomment-10081038,when
doesn't want inactive to retry,
cf0e3ec303/flutter/lib/models/model.dart (L444),
1.2.3 always show retry no matter what `hasRetry` is.

This reverts commit c3c99ba107.

* not keep session if there is no remote connection left.

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-26 22:05:54 +08:00
bovirus
129f6c869b Update Italian language (#9752) 2024-10-26 16:06:29 +08:00
Tobias Degen
924aa515c6 fix german translation (#9742) 2024-10-25 08:21:42 +08:00
solokot
c51771c854 Update ru.rs (#9741) 2024-10-25 08:08:02 +08:00
21pages
c8b9031996 refactor session insert, update if already exists (#9739)
* All share the same last_receive_time
* Not second port forward

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-24 23:14:43 +08:00
rustdesk
4da584055d fix ci 2024-10-24 22:52:06 +08:00
rustdesk
bd22b01370 fix "insert ctrl+alt+del" 2024-10-24 22:27:51 +08:00
Tobias Degen
b35b48086a Add translation string for better translationj capability (#9736)
* Add new translation für sending Ctrl+Alt+Del

* Add new translation string for sending Ctrl+Alt+Del
2024-10-24 21:52:39 +08:00
21pages
445e9ac285 no password required for file transfer action in remote control menu (#9731)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-24 17:20:48 +08:00
fufesou
7a3e1fe648 fix: ->macos, mouse events, key flags (#9733)
* fix: win->macos, mouse events,  key flags

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

* comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-24 14:33:37 +08:00
Alex Rijckaert
dfa9519d58 Update nl.rs (#9726) 2024-10-23 18:13:05 +08:00
fufesou
cc6f919080 feat: mobile map mode (#9717)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-23 09:28:39 +08:00
bovirus
2cdaca0fa3 Update Italian language (#9718) 2024-10-22 16:20:35 +08:00
21pages
6159449eba move option video-save-directory and allow-auto-record-outgoing to local (#9715)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-22 08:24:52 +08:00
Andrzej Rudnik
6088920f8d Update pl.rs (#9713)
* Update pl.rs

* Update pl.rs

Updated quota chars.
2024-10-21 19:55:34 +08:00
21pages
e8187588c1 auto record outgoing (#9711)
* Add option auto record outgoing session
* In the same connection, all displays and all windows share the same
  recording state.

todo:

Android check external storage permission

Known issue:

* Sciter old issue, stop the process directly without stop record, the record file can't play.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-21 14:34:06 +08:00
solokot
289076aa70 Update ru.rs (#9712) 2024-10-21 14:12:58 +08:00
fufesou
547da31095 fix: windows, exit fullscreen, remove redraw (#9700)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-19 19:41:55 +08:00
Mr-Update
1bf4ef1f46 Update de.rs (#9699) 2024-10-19 19:41:14 +08:00
21pages
1212d9fa2d web uni link (#9697)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-19 15:32:17 +08:00
fufesou
8c8a643cce fix: workaround physical display rotation (#9696)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-19 10:57:42 +08:00
Kleofass
675ffe0381 Update lv.rs (#9694) 2024-10-18 22:51:17 +08:00
bovirus
844caf8c15 Update Italian language (#9692) 2024-10-18 16:07:49 +08:00
flusheDData
0f6d28def7 Update es.rs (#9687)
New terms added
2024-10-18 08:55:39 +08:00
fufesou
0d3243e6dd fix: android, Korean input (#9667)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-18 08:55:18 +08:00
21pages
53d11e99d7 web only decode the latest image (#9689)
1. web only decode the latest image
2. web/ios remove relay server config when import

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-18 08:45:16 +08:00
21pages
defb3e6c73 fix gtk-sudo in non-English linux (#9680)
change LC_ALL from C.UTF-8 to C

Signed-off-by: 21pages <pages21@163.com>
2024-10-17 20:05:13 +08:00
fufesou
ae8dfe84a0 feat, web toast (#9686)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-17 17:23:09 +08:00
rustdesk
5e920f0fd0 fix ci 2024-10-16 22:37:40 +08:00
XLion
1a0814b201 Update tw.rs (#9672) 2024-10-16 19:33:27 +08:00
shleyZ
ace98d98ad fix: TextFormField onChanged event triggered multiple times when Korean input (#9644)
* fix: TextFormField onChanged event triggered multiple times when Korean input

* logic fix for iOS

* add comments

* move 'onChanged' logic to handleSoftKeyboardInput
2024-10-16 19:25:27 +08:00
Mr-Update
09083b3afa Update de.rs (#9668) 2024-10-16 11:23:12 +08:00
bovirus
36e11c61a9 Update Italian language (#9650) 2024-10-14 21:26:55 +08:00
21pages
55187e9243 fix theme radio update (#9647)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-14 17:46:38 +08:00
21pages
ae1c1a56e6 add missing code of last commit (#9646)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-14 15:51:51 +08:00
fufesou
cdd58e77eb fix: flickers child screen when resizing window (#9645)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-14 15:48:56 +08:00
21pages
ce924cc0d3 combine upload files/folder button (#9643)
* combine upload files/folder button

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

* web compress cache

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-14 15:46:21 +08:00
fufesou
498b8ba3d6 fix: wayland CapsLock (#9641)
* fix: wayland CapsLock

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

* unformat for less changes

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

* refact: input, LockModes, do not check evt.down

Remove `evt.down`(revert the change) to avoid potential bugs.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-14 11:46:28 +08:00
21pages
af610b2408 web (#9640)
resolution, image quality, tab name, last remote id

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-14 11:15:52 +08:00
Yevhen Popok
6cdbcfc082 Update README-UA.md (#9639) 2024-10-14 11:02:57 +08:00
Yevhen Popok
9c7f51bc76 Update uk.rs (#9638) 2024-10-14 11:02:44 +08:00
fufesou
65683cc3e6 refact: remove redundant escape (#9634)
* refact: remove redundant escape

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

* flutter, early assert

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-13 01:07:47 +08:00
21pages
eb1ef0969c web file transfer (#9587)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-12 09:03:13 +08:00
21pages
29b01e9cef remove shift & tap enable multiselect (#9625)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-11 16:47:08 +08:00
21pages
cde7620eda fix web peer card tap (#9622)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-11 16:35:15 +08:00
Ibnul Mutaki
844b853074 update ID trans (#9621) 2024-10-11 10:43:22 +08:00
21pages
97f02ed25e Web password source (#9618)
* ensure window init finish

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

* web password source

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-11 09:52:09 +08:00
Ibnul Mutaki
22c84bbbd1 Update ID translation (#9609)
* update id trans

* update id trans

* update inv

* update ind trans

* update ind trans

* update ind trans
2024-10-10 10:12:24 +08:00
Jernej Simončič
227f154ee7 Add missing Slovenian translations (#9606) 2024-10-10 07:35:46 +08:00
Ibnul Mutaki
59d7bf1e86 Update indonesian translate (#9601)
* update id trans

* update id trans

* update inv

* update ind trans
2024-10-09 10:44:20 +08:00
rustdesk
38fcf4e039 typo 2024-10-09 10:19:24 +08:00
James
4b3b31147e Update eo.rs (#9600) 2024-10-09 10:01:54 +08:00
fufesou
e6d4067f48 refact: remote toolbar style (#9597)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-08 21:16:07 +08:00
fufesou
507de628c9 fix: web, 2fa, trust (#9594)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-08 18:35:25 +08:00
fufesou
2591d4f044 fix: web chat (#9588)
* fix: web chat

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

* add missing svg

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-08 15:23:00 +08:00
XLion
9bcd0d1b03 Add Priority: to Debian control (#9586) 2024-10-07 22:08:17 +08:00
RustDesk
5555ba6b2f Revert "manifest updates from the Flathub's PR (#9581)" (#9585)
This reverts commit e06f456bbd.
2024-10-07 21:50:01 +08:00
XLion
28b6bc186f Add Section to Debian control (#9584) 2024-10-07 21:46:25 +08:00
fufesou
00d38260e1 fix: web send audit note (#9582)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-07 21:37:43 +08:00
Dmitry Beskov
e06f456bbd manifest updates from the Flathub's PR (#9581)
Signed-off-by: dmitry <43372966+besdar@users.noreply.github.com>
2024-10-07 18:55:25 +08:00
rustdesk
cc860b2906 https://github.com/flathub/flathub/pull/5670#issuecomment-2395337173 2024-10-07 15:26:41 +08:00
fufesou
839e8180e0 refact: web, no minimize btn on fullscreen (#9578)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-07 12:00:56 +08:00
fufesou
83aba804d0 fix: web fullscreen (#9577)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-07 00:01:14 +08:00
21pages
560c1effe8 remove mobile web setting, remove web/ios relay server setting (#9575)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-06 16:20:37 +08:00
rustdesk
e7353be0cd fix https://github.com/flathub/flathub/pull/5670#issuecomment-2395067890 2024-10-06 09:25:40 +08:00
fufesou
ba832362a7 fix: installed, copy&paste, special format (#9570)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-06 08:32:04 +08:00
Ibnul Mutaki
9ea09c1515 Update id (Indonesia) translation on some part (#9566)
* update id trans

* update id trans
2024-10-05 10:54:17 +08:00
fufesou
3a97b63e95 fix: windows, multi-window, move between monitors (#9562)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-04 22:05:38 +08:00
fufesou
dec3cde9b3 fix: deb, build, prerm (#9552)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-03 17:56:21 +08:00
rustdesk
c4d0b02478 fix ci 2024-10-03 15:38:55 +08:00
rustdesk
306dd77b81 fix https://github.com/rustdesk/rustdesk/issues/9537 2024-10-03 15:34:40 +08:00
BennyBeat
fd62751cb8 Improved Catalan translation (#9546)
* Improved Catalan translation

Improved Catalan translation

* Update ca.rs

Improved Catalan translation
2024-10-03 15:02:24 +08:00
hms5232
b0edfb8f70 fix wrong terms in tw lang (#9541)
"port" is "通訊埠", "連接埠" or just "埠" in Taiwan, "端口" is used in China.
2024-10-03 15:01:53 +08:00
fufesou
334526026c fix: web/mobile, skip querying onlines, if not in main page (#9535)
* fix: web, skip querying onlines, if not in main page

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

* fix: web/mobile, skip querying onlines

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

* Set isInMainPage to false after router is changed.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-01 15:25:59 +08:00
rustdesk
b5414ec002 fix https://github.com/rustdesk/rustdesk/issues/9527 2024-10-01 07:09:57 +08:00
fufesou
4eca8b9447 fix: web, forget password (#9524)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-01 07:04:43 +08:00
rustdesk
1ebc726acd fix ci 2024-09-30 10:54:05 +08:00
hashiguchi
d563372a91 Change the value of kMinQuality to 5 (#9508)
* Change the minimum value of the bitrate slider to 5

Signed-off-by: hashiguchi <hashiguchi@mtrx.jp>

* Change the value of kMinQuality to 5

Signed-off-by: hashiguchi <hashiguchi@mtrx.jp>

---------

Signed-off-by: hashiguchi <hashiguchi@mtrx.jp>
2024-09-28 21:52:05 +08:00
Alex Rijckaert
4a745d82f6 Update nl.rs (#9507) 2024-09-28 20:07:55 +08:00
fufesou
2f5f701dc7 fix: windows, subwindow, scale (#9506)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-28 19:43:28 +08:00
Iraccib
60a0099ba0 Update it.rs (#9503)
Changed "Scehrmo" to "Schermo"
2024-09-28 18:04:31 +08:00
rustdesk
30a7847100 fix ci 2024-09-28 15:53:39 +08:00
RustDesk
1e822fa135 Update nl.rs (#9501) 2024-09-28 15:49:11 +08:00
Alex Rijckaert
f6261883e8 Update nl.rs (#9500) 2024-09-28 15:33:08 +08:00
bovirus
3365844def Update Italian language (#9493) 2024-09-28 09:26:05 +08:00
solokot
769bbf1e1c Update ru.rs (#9495) 2024-09-28 09:25:52 +08:00
Mr-Update
81b999cfbe Update de.rs (#9496) 2024-09-28 09:25:35 +08:00
fufesou
9959217cc3 chore (#9491)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-27 23:10:51 +08:00
fufesou
3e6938bec6 refact: web desktop, web_id_input_tip (#9490)
* refact: web desktop, web_id_input_tip

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

* Update en.rs

* Update cn.rs

* Update en.rs

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-09-27 21:56:10 +08:00
fufesou
4459406578 fix: windows, window, restore from minimized state (#9482)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-27 14:19:25 +08:00
hashiguchi
beb1084e87 Change the minimum value of the bitrate slider to 5 (#9480)
Signed-off-by: hashiguchi <hashiguchi@mtrx.jp>
2024-09-27 10:14:46 +08:00
rustdesk
d4184fd865 bump to 1.3.2 2024-09-26 23:07:53 +08:00
21pages
ffc73f86a0 fix ab peers view, all peer tab use global peers model (#9475)
Use ChangeNotifierProvider<Peers>.value, and each peer tab has a global unique `Peers` model, then `load peers` and `build
peers` will always be the same one.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-26 22:08:32 +08:00
RustDesk
c74bdcdfdb Revert "fix peers view ChangeNotifierProvider update (#9459)" (#9471)
This reverts commit 7c55e3266b.
2024-09-26 14:20:37 +08:00
Kássio Oliveira
6d8b5b289f Refactor ScanPage for better performance and memory management (#9464)
- Added null checks in `reassemble` method to avoid potential null pointer exceptions when pausing/resuming the camera.
- Refactored image picking and QR code decoding process to use async/await, avoiding UI blocking with synchronous file reads.
- Improved exception handling by making it more specific to QR code reading errors.
- Introduced `StreamSubscription` for the QR scan listener and ensured proper cancellation in `dispose` method to prevent memory leaks.
- Separated button building logic (`_buildImagePickerButton`, `_buildFlashToggleButton`, `_buildCameraSwitchButton`) to enhance code readability and maintainability.
2024-09-26 10:34:12 +08:00
flusheDData
1d6873f622 New translation terms (#9463)
* Update es.rs

New terms added

* Update es.rs

New terms added
2024-09-26 09:29:49 +08:00
21pages
7c55e3266b fix peers view ChangeNotifierProvider update (#9459)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-25 15:11:11 +08:00
Lumiphare
ce5151032e Update README-ZH.md (#9457)
* Update CONTRIBUTING.md links to point to the Chinese version

* translated with AI assistance and manual refinement

* Adapted from the official Chinese translation of the Contributor Covenant

* Improve README-ZH.md

---------

Co-authored-by: sea <api@sea.com>
Co-authored-by: Lumiphare <lumiphare@foxmail.com>
2024-09-25 08:48:45 +08:00
bovirus
ba88bc9e8b Update Italian language (#9452) 2024-09-24 20:33:28 +08:00
fufesou
e0095aebda refact: web elevation (#9445)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-24 13:53:17 +08:00
rustdesk
664a3e186e clean SOFTWARE_UPDATE_URL 2024-09-24 12:00:37 +08:00
21pages
e4f7e126e5 fix check update (#9444)
check_software_update runs in a new thread, won't return directly

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-24 11:37:30 +08:00
Kleofass
49989e34e4 Update lv.rs (#9437) 2024-09-24 09:51:58 +08:00
fufesou
75a14fea23 fix: keyboard, change mode, await, on input source changed (#9434)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-23 17:31:37 +08:00
fufesou
f535406962 refact: web, keyboard, translate mode (#9432)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-23 16:58:21 +08:00
Vasyl Gello
f3f3bb538f Fix F-Droid build and bump Android NDK (#9428)
* Fix F-Droid build

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

* Bump Android NDK to r27b

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

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-09-23 16:43:59 +08:00
21pages
8fefd34c15 update web (#9427)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-23 09:18:22 +08:00
XLion
d98f947824 Update translation (#9426)
add translation for tw.rs and fix typo on cn.rs
2024-09-23 09:17:13 +08:00
solokot
5f52ce2c1b Update ru.rs (#9421) 2024-09-22 19:06:45 +08:00
Alex Rijckaert
1d799483d7 Update nl.rs (#9422) 2024-09-22 19:06:33 +08:00
rustdesk
3db55a718c build 48 2024-09-21 11:22:33 +08:00
Dmytro
a516f01feb Update uk.rs (#9416) 2024-09-20 21:38:16 +08:00
rustdesk
2e314bf032 disable init clipboard sync by default 2024-09-20 17:38:29 +08:00
jkh0kr
b93d4ce3fc Update ko.rs (#9411)
Add Korean language
2024-09-20 15:16:20 +08:00
fufesou
21bcfd173d fix: wayland, rdp input, mouse, scale (#9402)
* fix: wayland, rdp input, mouse, scale

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

* fix: rdp input, mouse, scale, check 0

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-20 11:15:19 +08:00
21pages
3d5262c36f opt gtk sudo ui, fix edit button show (#9399)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-20 11:06:56 +08:00
Mr-Update
cfd801c5d6 Update de.rs (#9401) 2024-09-20 10:54:50 +08:00
bovirus
216a72592d Update Italian language (#9403) 2024-09-20 10:54:37 +08:00
fufesou
ddd3401bd7 fix: keyboard, translate mode (#9406)
hotkey, linux -> win

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-20 10:52:19 +08:00
solokot
47139edd81 Update Russian translation (#9391)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-09-19 18:48:54 +08:00
fufesou
c6e3f60a6b refact: flutter, ChangeNotifier, reduce rebuild (#9392)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-19 18:48:01 +08:00
21pages
88a99211f3 replace pkexec with gtk sudo (#9383)
* Fix https://github.com/rustdesk/rustdesk/issues/9286, replace pkexec
  with gtk sudo. Tested on gnome (ubuntu 22.04, debian 13), xfce (manjaro, suse), kde (kubuntu 23), lxqt (lubuntu 22), Cinnamon (mint 21.3), Mate (mint 21.2)
* Fix incorrect config of the main window opened by the tray, replace
  xdg-open with run_me, replace with dbus + run_me
* Fix `check_if_stop_service`, it causes the problem fixed in
  https://github.com/rustdesk/rustdesk/pull/8414, now revert that fix and fix itself.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-19 18:47:37 +08:00
fufesou
d08c335fdf fix: file transfer, show error, msgbox (#9389)
* fix: file transfer, show error, msgbox

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

* fix: translation

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-18 19:29:35 +08:00
fufesou
e5ec6957fe fix: option OPTION_ONE_WAY_FILE_TRANSFER (#9387)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-18 18:22:12 +08:00
rustdesk
e20f5dd001 fix ci 2024-09-18 13:00:15 +08:00
rustdesk
e1a6ccc100 fix ci 2024-09-18 12:37:26 +08:00
rustdesk
cc288272d3 OPTION_ONE_WAY_CLIPBOARD_REDIRECTION,
OPTION_ENABLE_CLIPBOARD_INIT_SYNC,
        OPTION_ALLOW_LOGON_SCREEN_PASSWORD,
        OPTION_ONE_WAY_FILE_TRANSFER,
2024-09-18 12:18:26 +08:00
Lumiphare
49ce4edb8a Chinese versions of CONTRIBUTING.md and CODE_OF_CONDUCT-ZH.md (#9386)
* Update CONTRIBUTING.md links to point to the Chinese version

* translated with AI assistance and manual refinement

* Adapted from the official Chinese translation of the Contributor Covenant

---------

Co-authored-by: sea <api@sea.com>
2024-09-18 10:39:26 +08:00
お餅のCreeeper
29c3b29bda Fix ja.rs typo (#9378) 2024-09-17 13:26:49 +08:00
お餅のCreeeper
8a8f708c3e update ja.rs (#9376) 2024-09-17 12:32:32 +08:00
fufesou
c5038b1a78 Fix/virtual display do not plug out if not plugged in (#9372)
* fix: win VD, do not plug out if not plugged in

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

* Forcibly virtual display on clicking button "-"

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-16 15:57:40 +08:00
21pages
f4c038ea93 update appindicator and recommends install it (#9364)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-15 14:33:59 +08:00
21pages
d9ea717056 fix last commit (#9355)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-14 10:03:50 +08:00
21pages
40af9dc78b not run window focus service on wayland (#9354)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-14 09:59:14 +08:00
Alex Rijckaert
81fc22a156 Update nl.rs (#9344)
* Update nl.rs

* Update nl.rs

file updated after adjusting (as test) @FastAct to RijckAlex (same person, new account)
2024-09-13 21:04:04 +08:00
rustdesk
2e7bd26e4c fix leak fix 2024-09-13 15:42:51 +08:00
rustdesk
179b562472 another leak 2024-09-13 15:41:42 +08:00
21pages
ab246fdcbf password lowercase check like uppercase (#9343)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-13 09:29:00 +08:00
rustdesk
d65d3b7326 fix ci 2024-09-13 09:21:50 +08:00
rustdesk
9f9a22ec63 uppercase for all 2024-09-13 08:46:21 +08:00
m-hume
a8f1a66043 Trim whitespace from Import server config (#9341) 2024-09-13 08:06:40 +08:00
21pages
0b3e7bf33e update hwcodec, fix linux ci (#9335)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-12 17:34:54 +08:00
fufesou
c358399eca refact: reduce try_get_displays() on login (#9333)
* refact: reduce try_get_displays() on login

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

* Function rename

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-12 14:44:40 +08:00
rustdesk
cacca7295c fix memory leak on mac because of wrong use of objc, by wrapping autoreleasepool 2024-09-12 14:26:29 +08:00
fufesou
d2e98cc620 fix: reduce rebuild for online state (#9331)
* fix: reduce rebuild for online state

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

* Query online, update  on focus changed

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

* Use  to ensure  is right

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

* refact, window events on peer view

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

* chore

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

* Remove unused code

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-12 11:26:01 +08:00
fufesou
2e81bcb447 fix: Query online, remove loop retry (#9326)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-11 17:17:32 +08:00
fufesou
cbca0eb340 fix: keyboard, move tab to new window (#9322)
Do not disable keyboard when moving tab to new window on dispose.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-11 10:01:03 +08:00
fufesou
9380f33d7c Refact/options (#9318)
* refact options

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

* Remove unused msg

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

* web, toggle virtual display

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-10 23:54:59 +08:00
Gheorghi
519539ed0a Update bg.rs with more translations (#9317) 2024-09-10 23:22:14 +08:00
rustdesk
1f2a75fbd8 revert back pub lock because it does not fix leak 2024-09-10 21:28:07 +08:00
fufesou
51055a7e5b fix: tokio, call future in context of runtime (#9310)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-10 17:39:22 +08:00
fufesou
13effe7f14 fix: web, switch display (#9307)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-10 11:29:20 +08:00
21pages
943f96ef8c downgrade url_launcher/uni_links for linux ci (#9306)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-10 10:57:01 +08:00
rustdesk
260a82ee5c upgrade pub for flutter memory leak 2024-09-09 17:25:35 +08:00
fufesou
a2792d1527 comments (#9297)
* comments

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

* comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-08 23:07:42 +08:00
fufesou
2922ebe22a Fix/clipboard retry if is occupied (#9293)
* fix: clipboard, retry if is occupied

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

* fix: clipboard service, hold runtime to cm ipc

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

* update arboard

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

* refact: log

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

* fix: get formats, return only not

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-08 21:13:05 +08:00
rustdesk
1e6944b380 apply --cm-no-ui to non-windows 2024-09-08 12:54:27 +08:00
21pages
993862c103 quit cm process if ipc connection to ipc server closed (#9292)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-08 12:37:41 +08:00
Andrzej Rudnik
c8cd564e69 Update pl.rs (#9285) 2024-09-07 17:58:07 +08:00
21pages
a4cd64f0d5 fix qsv memory leak by updating ffmpeg (#9266)
* fix qsv memory leak by updating ffmpeg

* Memory leaks occur when destroying FFmpeg QSV VRAM encoders. This issue is resolved with FFmpeg version 7.
* FFmpeg requires ffnvcodec version 12.1.14.0 or higher, and an NVIDIA driver version greater than 530. For more details, https://github.com/FFmpeg/nv-codec-headers/tree/n12.1.14.0.
* The code of NVIDIA VRAM encoder is not changed, still use Video Codec SDK version 11, which is unaffected by FFmpeg. Drivers newer than 470 can support this, but we may consider an update later, as the support check by sdk code may not be accurate for FFmpeg RAM encoders.
* The issue is related to FFmpeg, not libmfx. FFmpeg version 7 recommends using libvpl, but vcpkg currently lacks ports for libvpl. We can add these in the future.
* D3D11 Texture Rendering: The "Shared GPU Memory" in the task manager continue increasing when using D3D11 texture render, which can exceed the GPU memory limit (e.g., reaching up to 100GB). I don't know what it is and will try to find it out.
* Roughly tests on Windows, Linux, macOS, and Android for quick fix. Further testing will be performed, and I will share the results in this pr.

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

* update flutter_gpu_texture_render, fix shared gpu memory leak while
rendering

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-07 10:20:52 +08:00
21pages
f0ca4b9fee --no-server parameter to avoid server ipc occupied by ui (#9272)
Signed-off-by: 21pages <pages21@163.com>
2024-09-06 14:43:38 +08:00
Xp96
aa3402b44a Update ptbr.rs (#9271) 2024-09-06 01:15:07 +08:00
fufesou
26ebd0deb9 fix: clipboard, cmd ipc (#9270)
1. Send raw contents if `content_len` > 1024*3.
2. Send raw contents if it is not empty.
3. Try read clipboard again if no data from cm.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-05 23:39:07 +08:00
21pages
4150036589 remove first frame fallback if repeat (#9267)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-05 22:48:20 +08:00
fufesou
7a1157f1b0 refact: quality status event (#9268)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-05 22:37:14 +08:00
21pages
3bd34bf0b9 increase interval for restart linux ui, try fix loop start (#9264)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-05 18:34:48 +08:00
fufesou
5f29016861 fix: build web (#9259)
1. Web, build.
2. Web and mobile, `onSubmitted` for ID text field.
3. Web, remove unused key 'toggle_option'.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-04 22:27:52 +08:00
fufesou
e40243b55d Fix/wf cliprdr c bugs (#9253)
* fix: ResetEvent() after WaitForSingleObject()

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

* fix: check and free mem

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-04 17:04:48 +08:00
fufesou
dbbbd08934 fix: clipboard, support excel xml spreadsheet (#9252)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-04 16:44:36 +08:00
21pages
29e12b84a9 password max length prompt (#9248)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-04 11:31:13 +08:00
fufesou
04c0f66ca9 fix: set to OK if recv flag is TRUE (#9244)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-03 21:15:35 +08:00
fufesou
ec28567362 fix: win, file clipboard (#9243)
1. Return the result of `wait_response_event()` in
   `cliprdr_send_format_list()`
2. Add recv flags to avoid waiting a long time.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-03 20:55:45 +08:00
fufesou
d4377a13c5 refact: peer card, orientation (#9235)
* refact: peer card, orientation

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

* Do not change landscape/portrait on Desktop

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

* comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-03 19:06:11 +08:00
21pages
39e713838f Use fallback codec if first frame fails (#9242)
* Both encoding and decoding use fallback if first frame fails
* More aggresive max fail counter
* Update hwcodec, add judgement when length of the encoded data is zero, https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525997
* Fix serde hwcodec config toml fails when the non-first vec![] is empty, https://github.com/toml-rs/toml-rs/issues/384, the config file is used for cache, when check process is not finished, the cache is used.
* Allow cm not start for pro user

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-03 18:48:17 +08:00
SimonHanel
75a4671bda Update da.rs (#9238)
* Update da.rs

Filled out every empty string and adjusted some for better translation.

Some translations might be janky due to my lack of context for what the string is used for, but it's good enough for now.

* Update da.rs

Minor tweaks
2024-09-03 10:09:25 +08:00
fufesou
827efabbc0 refact: remove fingerprint for web (#9226)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-02 02:00:59 +08:00
fufesou
532fe6aefb refact: web ui, login (#9225)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-01 23:14:57 +08:00
fufesou
ae339f039d refact: web ui (#9217)
* refact: web ui

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

* refact: remove AppBar shadow

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-01 00:30:07 +08:00
fufesou
bf390611ab fix: keyboard, sciter (#9216)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-31 19:02:50 +08:00
fufesou
e3f6829d02 refact: android ios, lan discovery (#9207)
* refact: android ios, lan discovery

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

* fix: build and runtime error

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-30 00:37:38 +08:00
fufesou
832002a10f refact: web, remote toolbar, pin (#9206)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-29 23:38:08 +08:00
RustDesk
d335cdbb0c Update README.md (#9196) 2024-08-28 12:25:00 +08:00
RustDesk
6a5d5875c8 Logo broken (#9195) 2024-08-28 12:23:40 +08:00
fufesou
cf06d1028f fix: web, reset cursor on disconn, back to main page (#9192)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 23:58:04 +08:00
9amm
fd178a7b6c Fix minor typo (#9191)
Co-authored-by: 9amm <>
2024-08-27 23:38:40 +08:00
rustdesk
f3a2733d75 start mac service more later 2024-08-27 20:25:01 +08:00
fufesou
55de573a01 fix: keyboard input, mulit windows (#9189)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 19:30:51 +08:00
fufesou
40239a1c41 feat: macos, mouse button, back&forward (#9185)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 15:20:29 +08:00
fufesou
c68ce7dd84 fix: web v2, keyboard mode (#9180)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 00:00:33 +08:00
rustdesk
690a2c8399 still find delegate failure when my mac restarted automatically sometimes 2024-08-26 17:07:02 +08:00
fufesou
4b4fd94f3e feat: web v2 keyboard (#9175)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-26 12:13:11 +08:00
21pages
5abe42f66c not run get window focus if no multiple displays (#9174)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-26 10:37:35 +08:00
21pages
48aec6484c refresh file transfer table on resume (#9167)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-25 21:29:41 +08:00
21pages
a946d4d0c9 file transfer status text overflow at start (#9166)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-25 20:46:21 +08:00
ELForcer
24f4b94082 Update ru.rs (#9163) 2024-08-25 15:12:08 +08:00
fufesou
aa1e122532 fix: revert key events to raw key events on Linux (#9161)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-25 00:03:31 +08:00
rustdesk
d400999b9c bump to 1.3.1 2024-08-24 19:02:04 +08:00
fufesou
1d416f6626 refact: flutter keyboard, map mode (#9160)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-24 12:10:36 +08:00
Kleofass
9d9741f18e Update lv.rs (#9155) 2024-08-23 21:27:52 +08:00
21pages
50aa8e12ad desktop file transfer, all columns respond to tap, add right click item border (#9153)
When right click selected item, the border is not obvious but can feel
some change.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-23 10:00:36 +08:00
jxdv
5931af460e Update trs (#9144)
* update sk tr

* update cz tr
2024-08-22 09:36:03 +08:00
fufesou
fc607d6789 fix: privacy mode 2, restore (#9141)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-22 09:35:50 +08:00
rustdesk
529e70910d build 47 2024-08-21 18:29:43 +08:00
fufesou
f300d797e2 Fix/ios query onlines (#9134)
* fix: ios query onlines

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

* comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-21 18:21:25 +08:00
Daniel Ehrhardt
e3cce2824d Added Public Server to Readme (#9132) 2024-08-21 09:28:02 +08:00
21pages
f34b8411a7 Fix new cm tab not replace the old persisted tab (#9127)
* This happens when after changing DesktopTab to StatefulWidget, 1.2.7
  and 1.3.0 have this problem.
* When `addConnection` in server_model.dart is called, the old closed
  client is removed, the client parameter of buildConnectionCard is new,
but client id inside Consumer is old.
* The only state in cm page is timer, its value is kept in test.
* There may be a better way to solve the ui update.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-20 15:34:10 +08:00
21pages
8745fcbb6a opt desktop file manager status list (#9117)
* Show delete file/dir log
* Show full path rather than base file name
* Show files count
* Opt status card layout
* Change selected color to accent

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-20 10:53:55 +08:00
302 changed files with 16100 additions and 15493 deletions

View File

@@ -12,3 +12,5 @@ rustflags = [
#rustflags = [ #rustflags = [
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" # "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
#] #]
[net]
git-fetch-with-cli = true

View File

@@ -26,8 +26,8 @@ body:
- type: input - type: input
id: os id: os
attributes: attributes:
label: Operating system(s) on local side and remote side label: Operating system(s) on local (controlling) side and remote (controlled) side
description: What operating system(s) do you see this bug on? local side -> remote side. description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
placeholder: | placeholder: |
Windows 10 -> osx Windows 10 -> osx
validations: validations:
@@ -35,8 +35,8 @@ body:
- type: input - type: input
id: version id: version
attributes: attributes:
label: RustDesk Version(s) on local side and remote side label: RustDesk Version(s) on local (controlling) side and remote (controlled) side
description: What RustDesk version(s) do you see this bug on? local side -> remote side. description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
placeholder: | placeholder: |
1.1.9 -> 1.1.8 1.1.9 -> 1.1.8
validations: validations:

View File

@@ -0,0 +1,42 @@
diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart
index 7e634cd2aa..c1e9acc295 100644
--- a/packages/flutter/lib/src/material/dropdown_menu.dart
+++ b/packages/flutter/lib/src/material/dropdown_menu.dart
@@ -475,7 +475,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _leadingKey = GlobalKey();
late List<GlobalKey> buttonItemKeys;
final MenuController _controller = MenuController();
- late bool _enableFilter;
+ bool _enableFilter = false;
late List<DropdownMenuEntry<T>> filteredEntries;
List<Widget>? _initialMenu;
int? currentHighlight;
@@ -524,6 +524,11 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
}
_localTextEditingController = widget.controller ?? TextEditingController();
}
+ if (oldWidget.enableFilter != widget.enableFilter) {
+ if (!widget.enableFilter) {
+ _enableFilter = false;
+ }
+ }
if (oldWidget.enableSearch != widget.enableSearch) {
if (!widget.enableSearch) {
currentHighlight = null;
@@ -663,6 +668,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
);
currentHighlight = widget.enableSearch ? i : null;
widget.onSelected?.call(entry.value);
+ _enableFilter = false;
}
: null,
requestFocusOnHover: false,
@@ -735,6 +741,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (_enableFilter) {
filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
+ } else {
+ filteredEntries = widget.dropdownMenuEntries;
}
if (widget.enableSearch) {

View File

@@ -6,7 +6,7 @@ on:
workflow_call: workflow_call:
env: env:
FLUTTER_VERSION: "3.16.9" FLUTTER_VERSION: "3.22.3"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
@@ -25,6 +25,8 @@ jobs:
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Install prerequisites - name: Install prerequisites
run: | run: |
@@ -73,12 +75,13 @@ jobs:
- name: Install flutter rust bridge deps - name: Install flutter rust bridge deps
shell: bash shell: bash
run: | run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
pushd flutter && flutter pub get && popd pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
- name: Run flutter rust bridge - name: Run flutter rust bridge
run: | run: |
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@master uses: actions/upload-artifact@master
@@ -89,3 +92,5 @@ jobs:
./src/bridge_generated.io.rs ./src/bridge_generated.io.rs
./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.dart
./flutter/lib/generated_bridge.freezed.dart ./flutter/lib/generated_bridge.freezed.dart
./flutter/macos/Runner/bridge_generated.h
./flutter/ios/Runner/bridge_generated.h

View File

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

View File

@@ -20,21 +20,21 @@ on:
env: env:
SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
CARGO_NDK_VERSION: "3.1.2" CARGO_NDK_VERSION: "3.1.2"
SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
SCITER_NASM_DEBVERSION: "2.14-1" SCITER_NASM_DEBVERSION: "2.14-1"
LLVM_VERSION: "15.0.6" LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.19.6" FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). ANDROID_FLUTTER_VERSION: "3.24.5"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work # for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}" TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.07.12 # vcpkg version: 2024.11.16
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
VERSION: "1.3.0" VERSION: "1.3.7"
NDK_VERSION: "r27" NDK_VERSION: "r27c"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
@@ -46,6 +46,9 @@ env:
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs: jobs:
generate-bridge:
uses: ./.github/workflows/bridge.yml
build-RustDeskTempTopMostWindow: build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
with: with:
@@ -59,7 +62,7 @@ jobs:
build-for-windows-flutter: build-for-windows-flutter:
name: ${{ matrix.job.target }} name: ${{ matrix.job.target }}
needs: [build-RustDeskTempTopMostWindow] needs: [build-RustDeskTempTopMostWindow, generate-bridge]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
strategy: strategy:
fail-fast: false fail-fast: false
@@ -84,6 +87,14 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Install LLVM and Clang - name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1 uses: KyleMayes/install-llvm-action@v1
@@ -95,7 +106,22 @@ jobs:
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
# https://github.com/flutter/flutter/issues/155685
- name: Replace engine with rustdesk custom flutter engine
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Patch flutter
shell: bash
run: |
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1 uses: dtolnay/rust-toolchain@v1
@@ -108,13 +134,6 @@ jobs:
with: with:
prefix-key: ${{ matrix.job.os }} prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
run: |
git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@v11
with: with:
@@ -139,6 +158,7 @@ jobs:
done done
exit 1 exit 1
fi fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash shell: bash
- name: Build rustdesk - name: Build rustdesk
@@ -258,6 +278,8 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Install LLVM and Clang - name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@master uses: rustdesk-org/install-llvm-action-32bit@master
@@ -299,6 +321,7 @@ jobs:
done done
exit 1 exit 1
fi fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash shell: bash
- name: Build rustdesk - name: Build rustdesk
@@ -374,6 +397,7 @@ jobs:
# use build-for-macOS instead # use build-for-macOS instead
if: false if: false
runs-on: [self-hosted, macOS, ARM64] runs-on: [self-hosted, macOS, ARM64]
needs: [generate-bridge]
steps: steps:
- name: Export GitHub Actions cache environment variables - name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6 uses: actions/github-script@v6
@@ -384,13 +408,14 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter rust bridge deps - name: Restore bridge files
shell: bash uses: actions/download-artifact@master
run: | with:
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" name: bridge-artifact
pushd flutter && flutter pub get && popd path: ./
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Build rustdesk - name: Build rustdesk
run: | run: |
@@ -446,6 +471,7 @@ jobs:
if: ${{ inputs.upload-artifact }} if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa name: build rustdesk ios ipa
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
needs: [generate-bridge]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -469,12 +495,20 @@ jobs:
brew install nasm yasm brew install nasm yasm
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@v11
with: with:
@@ -496,6 +530,7 @@ jobs:
done done
exit 1 exit 1
fi fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash shell: bash
- name: Install Rust toolchain - name: Install Rust toolchain
@@ -510,18 +545,23 @@ jobs:
prefix-key: rustdesk-lib-cache-ios prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }} key: ${{ matrix.job.target }}
- name: Install flutter rust bridge deps - name: Restore bridge files
shell: bash uses: actions/download-artifact@master
run: | with:
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" name: bridge-artifact
pushd flutter && flutter pub get && popd path: ./
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib - name: Build rustdesk lib
run: | run: |
rustup target add ${{ matrix.job.target }} rustup target add ${{ matrix.job.target }}
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib 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 - name: Build rustdesk
shell: bash shell: bash
run: | run: |
@@ -550,6 +590,7 @@ jobs:
#if: ${{ inputs.upload-artifact }} #if: ${{ inputs.upload-artifact }}
if: false if: false
runs-on: [self-hosted, macOS, ARM64] runs-on: [self-hosted, macOS, ARM64]
needs: [generate-bridge]
strategy: strategy:
fail-fast: false fail-fast: false
steps: steps:
@@ -562,15 +603,16 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
- name: Install flutter rust bridge deps - name: Restore bridge files
shell: bash uses: actions/download-artifact@master
run: | with:
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" name: bridge-artifact
pushd flutter && flutter pub get && popd path: ./
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib - name: Build rustdesk lib
run: | run: |
@@ -605,6 +647,7 @@ jobs:
build-for-macOS: build-for-macOS:
name: ${{ matrix.job.target }} name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
needs: [generate-bridge]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -614,13 +657,15 @@ jobs:
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "", extra-build-args: "",
arch: x86_64, arch: x86_64,
vcpkg-triplet: x64-osx,
} }
- { - {
target: aarch64-apple-darwin, target: aarch64-apple-darwin,
os: macos-latest, os: macos-latest,
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
extra-build-args: "", extra-build-args: "--screencapturekit",
arch: aarch64, arch: aarch64,
vcpkg-triplet: arm64-osx,
} }
steps: steps:
- name: Export GitHub Actions cache environment variables - name: Export GitHub Actions cache environment variables
@@ -632,6 +677,8 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Import the codesign cert - name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null if: env.MACOS_P12_BASE64 != null
@@ -668,7 +715,13 @@ jobs:
- name: Install build runtime - name: Install build runtime
run: | run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config brew install llvm create-dmg nasm cmake gcc wget ninja
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
if command -v pkg-config &>/dev/null; then
echo "pkg-config is already installed"
else
brew install pkg-config
fi
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
@@ -676,6 +729,11 @@ jobs:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Workaround for flutter issue - name: Workaround for flutter issue
shell: bash shell: bash
run: | run: |
@@ -687,7 +745,7 @@ jobs:
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: ${{ env.RUST_VERSION }} toolchain: ${{ env.MAC_RUST_VERSION }}
targets: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
components: "rustfmt" components: "rustfmt"
@@ -695,12 +753,11 @@ jobs:
with: with:
prefix-key: ${{ matrix.job.os }} prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps - name: Restore bridge files
shell: bash uses: actions/download-artifact@master
run: | with:
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" name: bridge-artifact
pushd flutter && flutter pub get && popd path: ./
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@v11
@@ -722,6 +779,7 @@ jobs:
done done
exit 1 exit 1
fi fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
- name: Show version information (Rust, cargo, Clang) - name: Show version information (Rust, cargo, Clang)
shell: bash shell: bash
@@ -735,6 +793,13 @@ jobs:
- name: Build rustdesk - name: Build rustdesk
run: | run: |
if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then
MIN_MACOS_VERSION="12.3"
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py
sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile
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 ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg - name: create unsigned dmg
@@ -822,11 +887,8 @@ jobs:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.TAG_NAME }}
files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz
generate-bridge-linux:
uses: ./.github/workflows/bridge.yml
build-rustdesk-android: build-rustdesk-android:
needs: [generate-bridge-linux] needs: [generate-bridge]
name: build rustdesk android apk ${{ matrix.job.target }} name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
strategy: strategy:
@@ -884,7 +946,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \
@@ -894,7 +956,6 @@ jobs:
libpam0g-dev \ libpam0g-dev \
libpulse-dev \ libpulse-dev \
libva-dev \ libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
@@ -903,18 +964,27 @@ jobs:
llvm-10-dev \ llvm-10-dev \
nasm \ nasm \
ninja-build \ ninja-build \
openjdk-11-jdk-headless \ openjdk-17-jdk-headless \
pkg-config \ pkg-config \
tree \ tree \
wget wget
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- uses: nttld/setup-ndk@v1 - uses: nttld/setup-ndk@v1
id: setup-ndk id: setup-ndk
with: with:
@@ -974,10 +1044,13 @@ jobs:
key: ${{ matrix.job.target }} key: ${{ matrix.job.target }}
- name: fix android for flutter 3.13 - name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: | run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml cd flutter
cd flutter/lib 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' find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk lib - name: Build rustdesk lib
@@ -986,7 +1059,7 @@ jobs:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: | run: |
rustup target add ${{ matrix.job.target }} rustup target add ${{ matrix.job.target }}
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
case ${{ matrix.job.target }} in case ${{ matrix.job.target }} in
aarch64-linux-android) aarch64-linux-android)
./flutter/ndk_arm64.sh ./flutter/ndk_arm64.sh
@@ -1019,9 +1092,9 @@ jobs:
- name: Build rustdesk - name: Build rustdesk
shell: bash shell: bash
env: env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: | run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# temporary use debug sign config # temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in case ${{ matrix.job.target }} in
@@ -1066,6 +1139,14 @@ jobs:
mkdir -p signed-apk; pushd signed-apk mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk . mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk .
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
- name: Setup sign tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@v1 - uses: r0adkll/sign-android-release@v1
name: Sign app APK name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null if: env.ANDROID_SIGNING_KEY != null
@@ -1077,8 +1158,8 @@ jobs:
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env: env:
# override default build-tools version (29.0.3) -- optional # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
BUILD_TOOLS_VERSION: "30.0.2" BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts - name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -1144,7 +1225,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \
@@ -1154,7 +1235,6 @@ jobs:
libpam0g-dev \ libpam0g-dev \
libpulse-dev \ libpulse-dev \
libva-dev \ libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
@@ -1163,19 +1243,27 @@ jobs:
llvm-10-dev \ llvm-10-dev \
nasm \ nasm \
ninja-build \ ninja-build \
openjdk-11-jdk-headless \ openjdk-17-jdk-headless \
pkg-config \ pkg-config \
tree \ tree \
wget wget
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Restore bridge files - name: Restore bridge files
uses: actions/download-artifact@master uses: actions/download-artifact@master
with: with:
@@ -1208,18 +1296,21 @@ jobs:
path: ./flutter/android/app/src/main/jniLibs/x86 path: ./flutter/android/app/src/main/jniLibs/x86
- name: fix android for flutter 3.13 - name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: | run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml cd flutter
cd flutter/lib 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' find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk - name: Build rustdesk
shell: bash shell: bash
env: env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: | run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# temporary use debug sign config # temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
@@ -1239,6 +1330,14 @@ jobs:
mkdir -p signed-apk mkdir -p signed-apk
mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
- name: Setup sign tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@v1 - uses: r0adkll/sign-android-release@v1
name: Sign app APK name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null if: env.ANDROID_SIGNING_KEY != null
@@ -1250,8 +1349,8 @@ jobs:
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env: env:
# override default build-tools version (29.0.3) -- optional # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
BUILD_TOOLS_VERSION: "30.0.2" BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts - name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -1279,7 +1378,7 @@ jobs:
signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
build-rustdesk-linux: build-rustdesk-linux:
needs: [generate-bridge-linux] needs: [generate-bridge]
name: build rustdesk linux ${{ matrix.job.target }} name: build rustdesk linux ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }} runs-on: ${{ matrix.job.on }}
strategy: strategy:
@@ -1322,6 +1421,8 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Set Swap Space - name: Set Swap Space
if: ${{ matrix.job.arch == 'x86_64' }} if: ${{ matrix.job.arch == 'x86_64' }}
@@ -1370,6 +1471,7 @@ jobs:
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
run: | run: |
sudo apt install -y libva-dev && apt show libva-dev
if ! $VCPKG_ROOT/vcpkg \ if ! $VCPKG_ROOT/vcpkg \
install \ install \
--triplet ${{ matrix.job.vcpkg-triplet }} \ --triplet ${{ matrix.job.vcpkg-triplet }} \
@@ -1383,6 +1485,7 @@ jobs:
done done
exit 1 exit 1
fi fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash shell: bash
- name: Restore bridge files - name: Restore bridge files
@@ -1418,7 +1521,7 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libclang-10-dev \ libclang-10-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
@@ -1427,7 +1530,6 @@ jobs:
libpam0g-dev \ libpam0g-dev \
libpulse-dev \ libpulse-dev \
libva-dev \ libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
@@ -1514,6 +1616,19 @@ jobs:
;; ;;
esac esac
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
case ${{ matrix.job.arch }} in
aarch64)
pushd /opt/flutter-elinux/flutter
;;
x86_64)
pushd /opt/flutter
;;
esac
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
# build flutter # build flutter
pushd /workspace pushd /workspace
export CARGO_INCREMENTAL=0 export CARGO_INCREMENTAL=0
@@ -1636,6 +1751,8 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Free Space - name: Free Space
run: | run: |
@@ -1675,7 +1792,7 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libclang-dev \ libclang-dev \
libdbus-1-dev \ libdbus-1-dev \
@@ -1687,7 +1804,6 @@ jobs:
libpam0g-dev \ libpam0g-dev \
libpulse-dev \ libpulse-dev \
libva-dev \ libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
@@ -1765,6 +1881,8 @@ jobs:
cat ~/.cargo/config cat ~/.cargo/config
# install dependencies from vcpkg # install dependencies from vcpkg
export VCPKG_ROOT=/opt/artifacts/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 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 find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:" echo "$_1:"
@@ -1775,6 +1893,7 @@ jobs:
done done
exit 1 exit 1
fi fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
# build rustdesk # build rustdesk
python3 ./res/inline-sciter.py python3 ./res/inline-sciter.py
export CARGO_INCREMENTAL=0 export CARGO_INCREMENTAL=0
@@ -1824,6 +1943,8 @@ jobs:
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary - name: Download Binary
uses: actions/download-artifact@master uses: actions/download-artifact@master
@@ -1896,6 +2017,8 @@ jobs:
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary - name: Download Binary
uses: actions/download-artifact@master uses: actions/download-artifact@master
@@ -1921,36 +2044,17 @@ jobs:
shell: /bin/bash shell: /bin/bash
install: | install: |
apt-get update -y apt-get update -y
apt-get install -y \ apt-get install -y git flatpak flatpak-builder
curl \
git \
rpm \
wget
run: | run: |
# disable git safe.directory # disable git safe.directory
git config --global --add safe.directory "*" git config --global --add safe.directory "*"
pushd /workspace 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 deps
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# package # package
pushd flatpak pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1 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 flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package - name: Publish flatpak package
@@ -1972,6 +2076,8 @@ jobs:
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Prepare env - name: Prepare env
run: | run: |
@@ -1983,7 +2089,12 @@ jobs:
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Patch flutter
shell: bash
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
# https://rustdesk.com/docs/en/dev/build/web/ # https://rustdesk.com/docs/en/dev/build/web/
- name: Build web - name: Build web

View File

@@ -16,9 +16,9 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly" TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15 # vcpkg version: 2024.11.16
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
VERSION: "1.3.0" VERSION: "1.3.7"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -90,6 +90,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: ${{ matrix.job.ref }} ref: ${{ matrix.job.ref }}
submodules: recursive
- name: Import the codesign cert - name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null if: env.MACOS_P12_BASE64 != null
@@ -149,7 +150,7 @@ jobs:
shell: bash shell: bash
run: | run: |
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml; sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked
# below works for mac to make buildable on 3.13.9 # below works for mac to make buildable on 3.13.9
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd; # pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
pushd flutter && flutter pub get && popd pushd flutter && flutter pub get && popd
@@ -250,6 +251,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: ${{ matrix.job.ref }} ref: ${{ matrix.job.ref }}
submodules: recursive
- name: Install dependencies - name: Install dependencies
run: | run: |
@@ -262,7 +264,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev\
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \
@@ -302,7 +304,7 @@ jobs:
- name: Install flutter rust bridge deps - name: Install flutter rust bridge deps
run: | run: |
git config --global core.longpaths true git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd; pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
pushd flutter ; flutter pub get ; popd pushd flutter ; flutter pub get ; popd
@@ -347,7 +349,7 @@ jobs:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: | run: |
rustup target add ${{ matrix.job.target }} rustup target add ${{ matrix.job.target }}
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
case ${{ matrix.job.target }} in case ${{ matrix.job.target }} in
aarch64-linux-android) aarch64-linux-android)
./flutter/ndk_arm64.sh ./flutter/ndk_arm64.sh

1
.gitignore vendored
View File

@@ -54,3 +54,4 @@ examples/**/target/
vcpkg_installed vcpkg_installed
flutter/lib/generated_plugin_registrant.dart flutter/lib/generated_plugin_registrant.dart
libsciter.dylib libsciter.dylib
flutter/web/

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

119
Cargo.lock generated
View File

@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.4.0" version = "3.4.0"
source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35" source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"core-graphics 0.23.2", "core-graphics 0.23.2",
@@ -723,9 +723,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.16.1" version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -735,9 +735,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
dependencies = [ dependencies = [
"serde 1.0.203", "serde 1.0.203",
] ]
@@ -860,6 +860,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.38" version = "0.4.38"
@@ -887,6 +893,20 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "cidre"
version = "0.4.0"
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
dependencies = [
"cidre-macros",
"parking_lot",
]
[[package]]
name = "cidre-macros"
version = "0.1.0"
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@@ -1270,10 +1290,10 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.15.3" version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
dependencies = [ dependencies = [
"alsa", "alsa",
"cidre",
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
"coreaudio-rs", "coreaudio-rs",
"dasp_sample", "dasp_sample",
@@ -1561,6 +1581,16 @@ dependencies = [
"windows 0.32.0", "windows 0.32.0",
] ]
[[package]]
name = "default_net"
version = "0.1.0"
source = "git+https://github.com/rustdesk-org/default_net#a831d47bcacb4615b394968287697924a8f62be1"
dependencies = [
"anyhow",
"regex",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -1677,7 +1707,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading 0.8.4", "libloading 0.7.4",
] ]
[[package]] [[package]]
@@ -2231,9 +2261,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@@ -2241,9 +2271,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
@@ -2258,9 +2288,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
@@ -2292,9 +2322,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2 1.0.86", "proc-macro2 1.0.86",
"quote 1.0.36", "quote 1.0.36",
@@ -2303,21 +2333,21 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -2881,6 +2911,7 @@ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
"confy", "confy",
"default_net",
"directories-next", "directories-next",
"dirs-next", "dirs-next",
"dlopen", "dlopen",
@@ -2905,6 +2936,7 @@ dependencies = [
"serde 1.0.203", "serde 1.0.203",
"serde_derive", "serde_derive",
"serde_json 1.0.118", "serde_json 1.0.118",
"sha2",
"socket2 0.3.19", "socket2 0.3.19",
"sodiumoxide", "sodiumoxide",
"sysinfo", "sysinfo",
@@ -3044,8 +3076,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hwcodec" name = "hwcodec"
version = "0.7.0" version = "0.7.1"
source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267" source = "git+https://github.com/rustdesk-org/hwcodec#c4d6b1c5c4ddc7548868306004cf5d4eb614a36f"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"cc", "cc",
@@ -3967,11 +3999,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cfg_aliases", "cfg_aliases 0.1.1",
"libc", "libc",
"memoffset 0.9.1", "memoffset 0.9.1",
] ]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"cfg_aliases 0.2.1",
"libc",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@@ -4131,7 +4175,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [ dependencies = [
"proc-macro-crate 2.0.2", "proc-macro-crate 1.3.1",
"proc-macro2 1.0.86", "proc-macro2 1.0.86",
"quote 1.0.36", "quote 1.0.36",
"syn 2.0.68", "syn 2.0.68",
@@ -4350,9 +4394,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.64" version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@@ -4382,9 +4426,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.102" version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -5187,7 +5231,7 @@ dependencies = [
[[package]] [[package]]
name = "rdev" name = "rdev"
version = "0.5.0-2" version = "0.5.0-2"
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488" source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
dependencies = [ dependencies = [
"cocoa 0.24.1", "cocoa 0.24.1",
"core-foundation 0.9.4", "core-foundation 0.9.4",
@@ -5434,7 +5478,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-pulsectl" name = "rust-pulsectl"
version = "0.2.12" version = "0.2.12"
source = "git+https://github.com/open-trade/pulsectl#5e68f4c2b7c644fa321984688602d71e8ad0bba3" source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2"
dependencies = [ dependencies = [
"libpulse-binding", "libpulse-binding",
] ]
@@ -5462,7 +5506,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.3.0" version = "1.3.7"
dependencies = [ dependencies = [
"android-wakelock", "android-wakelock",
"android_logger", "android_logger",
@@ -5494,6 +5538,7 @@ dependencies = [
"flutter_rust_bridge", "flutter_rust_bridge",
"fon", "fon",
"fruitbasket", "fruitbasket",
"gtk",
"hbb_common", "hbb_common",
"hex", "hex",
"hound", "hound",
@@ -5508,6 +5553,7 @@ dependencies = [
"libpulse-simple-binding", "libpulse-simple-binding",
"mac_address", "mac_address",
"magnum-opus", "magnum-opus",
"nix 0.29.0",
"num_cpus", "num_cpus",
"objc", "objc",
"objc_id", "objc_id",
@@ -5539,6 +5585,7 @@ dependencies = [
"system_shutdown", "system_shutdown",
"tao", "tao",
"tauri-winrt-notification", "tauri-winrt-notification",
"termios",
"totp-rs", "totp-rs",
"tray-icon", "tray-icon",
"url", "url",
@@ -5559,7 +5606,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.3.0" version = "1.3.7"
dependencies = [ dependencies = [
"brotli", "brotli",
"dirs 5.0.1", "dirs 5.0.1",
@@ -5778,7 +5825,7 @@ dependencies = [
[[package]] [[package]]
name = "sciter-rs" name = "sciter-rs"
version = "0.5.57" version = "0.5.57"
source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
@@ -6424,7 +6471,7 @@ dependencies = [
[[package]] [[package]]
name = "tfc" name = "tfc"
version = "0.7.0" version = "0.7.0"
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6" source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"core-graphics 0.23.2", "core-graphics 0.23.2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.3.0" version = "1.3.7"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
name = "naming" name = "naming"
path = "src/naming.rs" path = "src/naming.rs"
[[bin]]
name = "service"
path = "src/service.rs"
[features] [features]
inline = [] inline = []
cli = [] cli = []
@@ -36,6 +40,7 @@ unix-file-copy-paste = [
"dep:once_cell", "dep:once_cell",
"clipboard/unix-file-copy-paste", "clipboard/unix-file-copy-paste",
] ]
screencapturekit = ["cpal/screencapturekit"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -78,13 +83,14 @@ zip = "0.6"
shutdown_hooks = "0.1" shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
cpal = "0.15" # 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" ringbuf = "0.3"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1" 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" sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] } enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" } clipboard = { path = "libs/clipboard" }
@@ -150,7 +156,7 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" } psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-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" async-process = "1.7"
evdev = { git="https://github.com/rustdesk-org/evdev" } evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9" dbus = "0.9"
@@ -161,6 +167,9 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/
x11rb = {version = "0.12", features = ["all-extensions"], optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true} percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", optional = true} once_cell = {version = "1.18", optional = true}
nix = { version = "0.29", features = ["term", "process"]}
gtk = "0.18"
termios = "0.3"
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"

View File

@@ -2,6 +2,7 @@ FROM debian:bullseye-slim
WORKDIR / WORKDIR /
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV VCPKG_FORCE_SYSTEM_BINARIES=1
RUN apt update -y && \ RUN apt update -y && \
apt install --yes --no-install-recommends \ apt install --yes --no-install-recommends \
g++ \ g++ \
@@ -21,7 +22,8 @@ RUN apt update -y && \
libpam0g-dev \ libpam0g-dev \
libpulse-dev \ libpulse-dev \
make \ make \
cmake \ wget \
libssl-dev \
unzip \ unzip \
zip \ zip \
sudo \ sudo \
@@ -31,6 +33,13 @@ RUN apt update -y && \
ninja-build && \ ninja-build && \
rm -rf /var/lib/apt/lists/* 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 && \ RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
/vcpkg/bootstrap-vcpkg.sh -disableMetrics && \ /vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
/vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Servers</a> • <a href="#public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
@@ -25,9 +25,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) [**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" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) 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 ## Dependencies
@@ -171,3 +174,4 @@ Please ensure that you are running these commands from the root of the RustDesk
![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) ![File Transfer](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) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -18,8 +18,8 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.3.0 version: 1.3.7
exec: usr/lib/rustdesk/rustdesk exec: usr/share/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
arch: arch:
@@ -47,9 +47,9 @@ AppDir:
- libasound2 - libasound2
- libsystemd0 - libsystemd0
- curl - curl
- libva2
- libva-drm2 - libva-drm2
- libva-x11-2 - libva-x11-2
- libvdpau1
- libgstreamer-plugins-base1.0-0 - libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire - gstreamer1.0-pipewire
- libwayland-client0 - libwayland-client0
@@ -77,7 +77,7 @@ AppDir:
env: env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11 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_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 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: test:

View File

@@ -18,8 +18,8 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.3.0 version: 1.3.7
exec: usr/lib/rustdesk/rustdesk exec: usr/share/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
arch: arch:
@@ -50,9 +50,9 @@ AppDir:
- libasound2 - libasound2
- libsystemd0 - libsystemd0
- curl - curl
- libva2
- libva-drm2 - libva-drm2
- libva-x11-2 - libva-x11-2
- libvdpau1
- libgstreamer-plugins-base1.0-0 - libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire - gstreamer1.0-pipewire
- libwayland-client0 - libwayland-client0
@@ -80,7 +80,7 @@ AppDir:
env: env:
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules 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 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_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 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: test:

View File

@@ -9,6 +9,7 @@ import shutil
import hashlib import hashlib
import argparse import argparse
import sys import sys
from pathlib import Path
windows = platform.platform().startswith('Windows') windows = platform.platform().startswith('Windows')
osx = platform.platform().startswith( osx = platform.platform().startswith(
@@ -111,7 +112,7 @@ def make_parser():
'--hwcodec', '--hwcodec',
action='store_true', action='store_true',
help='Enable feature hwcodec' + ( help='Enable feature hwcodec' + (
'' if windows or osx else ', need libva-dev, libvdpau-dev.') '' if windows or osx else ', need libva-dev.')
) )
parser.add_argument( parser.add_argument(
'--vram', '--vram',
@@ -143,6 +144,12 @@ def make_parser():
"--package", "--package",
type=str type=str
) )
if osx:
parser.add_argument(
'--screencapturekit',
action='store_true',
help='Enable feature screencapturekit'
)
return parser return parser
@@ -274,6 +281,9 @@ def get_features(args):
features.append('flutter') features.append('flutter')
if args.unix_file_copy_paste: if args.unix_file_copy_paste:
features.append('unix-file-copy-paste') features.append('unix-file-copy-paste')
if osx:
if args.screencapturekit:
features.append('screencapturekit')
print("features:", features) print("features:", features)
return features return features
@@ -283,11 +293,14 @@ def generate_control_file(version):
system2('/bin/rm -rf %s' % control_file_path) system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk content = """Package: rustdesk
Section: net
Priority: optional
Version: %s Version: %s
Architecture: %s Architecture: %s
Maintainer: rustdesk <info@rustdesk.com> Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire%s Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software. Description: A remote control software.
""" % (version, get_deb_arch(), get_deb_extra_depends()) """ % (version, get_deb_arch(), get_deb_extra_depends())
@@ -309,7 +322,7 @@ def build_flutter_deb(version, features):
os.chdir('flutter') os.chdir('flutter')
system2('flutter build linux --release') system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/') 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/rustdesk/')
system2('mkdir -p tmpdeb/etc/pam.d/') system2('mkdir -p tmpdeb/etc/pam.d/')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
@@ -319,7 +332,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true') system2('rm tmpdeb/usr/bin/rustdesk || true')
system2( system2(
f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/') f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/')
system2( system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2( system2(
@@ -330,8 +343,6 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2( system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2( system2(
@@ -344,7 +355,7 @@ def build_flutter_deb(version, features):
system2('mkdir -p tmpdeb/DEBIAN') system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version) generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') 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('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf tmpdeb/')
@@ -356,7 +367,7 @@ def build_flutter_deb(version, features):
def build_deb_from_folder(version, binary_folder): def build_deb_from_folder(version, binary_folder):
os.chdir('flutter') os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/') 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/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
@@ -364,7 +375,7 @@ def build_deb_from_folder(version, binary_folder):
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true') system2('rm tmpdeb/usr/bin/rustdesk || true')
system2( system2(
f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/') f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/')
system2( system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2( system2(
@@ -375,15 +386,13 @@ def build_deb_from_folder(version, binary_folder):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2( system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
system2('mkdir -p tmpdeb/DEBIAN') system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version) generate_control_file(version)
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') 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('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf tmpdeb/')
@@ -396,12 +405,13 @@ def build_flutter_dmg(version, features):
if not skip_cargo: if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2( 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 # copy dylib
system2( system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter') os.chdir('flutter')
system2('flutter build macos --release') system2('flutter build macos --release')
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
''' '''
system2( 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") "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")
@@ -613,21 +623,24 @@ def main():
os.system('mkdir -p tmpdeb/etc/pam.d/') os.system('mkdir -p tmpdeb/etc/pam.d/')
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2('strip tmpdeb/usr/bin/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk')
system2('mkdir -p tmpdeb/usr/lib/rustdesk') system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file_folder("tmpdeb/")
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('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
def md5_file(fn): def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() 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__": if __name__ == "__main__":

View File

@@ -1,7 +1,7 @@
#[cfg(windows)] #[cfg(windows)]
fn build_windows() { fn build_windows() {
let file = "src/platform/windows.cc"; let file = "src/platform/windows.cc";
let file2 = "src/platform/windows_delete_test_cert.cc"; let file2 = "src/platform/windows_delete_test_cert.cc";
cc::Build::new().file(file).file(file2).compile("windows"); cc::Build::new().file(file).file(file2).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32"); println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed={}", file); println!("cargo:rerun-if-changed={}", file);
@@ -61,7 +61,11 @@ fn install_android_deps() {
let target = format!("{}-android", target_arch); let target = format!("{}-android", target_arch);
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: std::path::PathBuf = vcpkg_root.into(); let mut path: std::path::PathBuf = vcpkg_root.into();
path.push("installed"); if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
path = vcpkg_root.into();
} else {
path.push("installed");
}
path.push(target); path.push(target);
println!( println!(
"{}", "{}",
@@ -72,7 +76,6 @@ fn install_android_deps() {
); );
println!("cargo:rustc-link-lib=ndk_compat"); println!("cargo:rustc-link-lib=ndk_compat");
println!("cargo:rustc-link-lib=oboe"); println!("cargo:rustc-link-lib=oboe");
println!("cargo:rustc-link-lib=oboe_wrapper");
println!("cargo:rustc-link-lib=c++"); println!("cargo:rustc-link-lib=c++");
println!("cargo:rustc-link-lib=OpenSLES"); println!("cargo:rustc-link-lib=OpenSLES");
} }

View File

@@ -0,0 +1,87 @@
# 贡献者公约行为准则
## 我们的承诺
身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
## 我们的标准
有助于为我们的社区创造积极环境的行为例子包括但不限于:
* 表现出对他人的同情和善意
* 尊重不同的主张、观点和感受
* 提出和大方接受建设性意见
* 承担责任并向受我们错误影响的人道歉
* 注重社区共同诉求,而非个人得失
不当行为例子包括:
* 使用情色化的语言或图像,及性引诱或挑逗
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
* 公开或私下的骚扰行为
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
* 其他有理由认定为违反职业操守的不当行为
## 责任和权力
社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论comment、提交commits、代码、维基wiki编辑、议题issues或其他贡献并在适当时机知采取措施的理由。
## 适用范围
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
## 监督
辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。
所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
## 处理方针
社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
### 1. 纠正
**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
### 2. 警告
**社区影响**: 单个或一系列违规行为。
**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
### 3. 临时封禁
**社区影响**: 严重违反社区准则,包括持续的不当行为。
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
### 4. 永久封禁
**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
**处理意见**: 永久禁止在社区内进行任何形式的公开互动。
## 参见
本行为准则改编自[参与者公约][homepage]2.0 版, 参见
[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0].
指导方针借鉴自[Mozilla纪检分级][Mozilla CoC].
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/zh-cn/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

32
docs/CONTRIBUTING-ZH.md Normal file
View File

@@ -0,0 +1,32 @@
# 为RustDesk做贡献
Rust欢迎每一位贡献者如果您有意向为我们做出贡献请遵循以下指南
## 贡献方式
对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。
如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。
## PR 注意事项
- 从 master 分支创建一个新的分支并在提交PR之前如果需要将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。
- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。
- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名
- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。
- 请为修复的 bug 或新增的功能添加相应的测试用例。
有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## 行为准则
请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。
## 沟通渠道
RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。

View File

@@ -9,12 +9,12 @@
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b> <b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
</p> </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) [![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) ![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 - 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 - 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에서 빌드 순서 ## 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 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 ```sh
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
@@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
``` ```
### Fix libvpx (For Fedora) ### libvpx 수정 (For Fedora)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src
@@ -92,7 +92,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd cd
``` ```
### Build ### 빌드
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | 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 컨테이너 구성하는 것으로 시작합니다. 포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
@@ -115,13 +115,13 @@ cd rustdesk
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 이하의 커맨드를 실행합니다. 이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다.
```sh ```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 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 ```sh
target/debug/rustdesk target/debug/rustdesk
@@ -133,9 +133,9 @@ target/debug/rustdesk
target/release/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/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, tcp/udp 랩퍼, protobuf, 파일 전송을 위한 fs 함수, 그 외 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처
@@ -143,12 +143,12 @@ target/release/rustdesk
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, 클립보드, 입력, 비디오 서비스 그리고 네트워크 연결 - **[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/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)**: 플랫폼 고유의 코드 - **[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](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드
- **[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/js)**: Flutter 웹 클라이언트용 자바스크립트
## Snapshot ## 스냅샷
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@@ -164,3 +164,4 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)

View File

@@ -1,20 +1,18 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br> <img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> • <a href="#публічні-сервери">Сервери</a> •
<a href="#кроки-для-збірки">Збирання</a> • <a href="#кроки-для-збірки">Збирання</a> •
<a href="#як-зібрати-за-допомогою-docker">Docker</a> • <a href="#як-зібрати-за-допомогою-docker">Docker</a> •
<a href="#структура-файлів">Структура</a> • <a href="#структура-файлів">Структура</a> •
<a href="#знімки">Знімки</a><br> <a href="#знімки-екрана">Знімки екрана</a><br>
[<a href="../README.md">English</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="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-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-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B> <b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
</p> </p>
Спілкування з нами: [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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo). Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
```sh ```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ 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 \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
``` ```
### openSUSE Tumbleweed ### openSUSE Tumbleweed
```sh ```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
```sh ```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -158,18 +156,19 @@ target/release/rustdesk
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS. - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень - **[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/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 NAT) або ретрансльованого з'єднання - **[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)**: специфічний для платформи код - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
## Знімки ## Знімки екрана
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)

View File

@@ -8,7 +8,7 @@
[<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> [<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> </p>
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md). RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
## 依赖 ## 依赖
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载 桌面版本使用 Flutter 或 Sciter已弃用作为 GUI本教程仅适用于 Sciter因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本
请自行下载Sciter动态库。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [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) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -133,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器
``` ```
在Dockerfile的RUN apt update之前插入两行 在Dockerfile的RUN apt update之前插入两行
RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \
RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list
``` ```
2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: 2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码:
@@ -207,12 +209,13 @@ target/release/rustdesk
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI已弃用
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现 - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 - **[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)保持UDP通讯, 等待远程连接(通过打洞直连或者中继) - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码 - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
## 截图 ## 截图

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", "id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform", "runtime": "org.freedesktop.Platform",
"runtime-version": "23.08", "runtime-version": "24.08",
"sdk": "org.freedesktop.Sdk", "sdk": "org.freedesktop.Sdk",
"command": "rustdesk", "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": [ "modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json", "shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
"xdotool.json", {
"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", "name": "pam",
"buildsystem": "simple", "buildsystem": "autotools",
"build-commands": [ "config-opts": ["--disable-selinux"],
"./configure --disable-selinux --prefix=/app && make -j4 install"
],
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
@@ -26,32 +37,24 @@
"name": "rustdesk", "name": "rustdesk",
"buildsystem": "simple", "buildsystem": "simple",
"build-commands": [ "build-commands": [
"bsdtar -zxvf rustdesk.deb", "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -",
"tar -xvf ./data.tar.xz", "cp -r usr/* /app/",
"cp -r ./usr/* /app/", "mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk"
"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"
], ],
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"path": "./rustdesk.deb" "path": "rustdesk.deb"
}, },
{ {
"type": "file", "type": "file",
"path": "../res/scalable.svg" "path": "com.rustdesk.RustDesk.metainfo.xml"
} }
] ]
} }
], ],
"finish-args": [ "finish-args": [
"--share=ipc", "--share=ipc",
"--socket=x11",
"--socket=fallback-x11", "--socket=fallback-x11",
"--socket=wayland", "--socket=wayland",
"--share=network", "--share=network",

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

@@ -1,6 +1,9 @@
import com.google.protobuf.gradle.* import com.google.protobuf.gradle.*
plugins { plugins {
id "com.google.protobuf" version "0.9.4" id "com.google.protobuf" version "0.9.4"
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
} }
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
@@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@@ -32,10 +30,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
dependencies { dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.20.1' implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
} }
@@ -57,7 +51,7 @@ protobuf {
} }
android { android {
compileSdkVersion 33 compileSdkVersion 34
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
@@ -105,7 +99,6 @@ flutter {
dependencies { dependencies {
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5' implementation 'com.github.getActivity:XXPermissions:18.5'
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.caverock:androidsvg-aar:1.4'
} }

View File

@@ -15,6 +15,13 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<queries>
<intent>
<!-- https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<application <application
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="RustDesk" android:label="RustDesk"

View File

@@ -304,7 +304,13 @@ class FloatingWindowService : Service(), View.OnTouchListener {
val popupMenu = PopupMenu(this, floatingView) val popupMenu = PopupMenu(this, floatingView)
val idShowRustDesk = 0 val idShowRustDesk = 0
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
val idStopService = 1 // For host side, clipboard sync
val idSyncClipboard = 1
val isServiceSyncEnabled = (MainActivity.rdClipboardManager?.isCaptureStarted ?: false) && FFI.isServiceClipboardEnabled()
if (isServiceSyncEnabled) {
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
}
val idStopService = 2
popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem -> popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) { when (menuItem.itemId) {
@@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
openMainActivity() openMainActivity()
true true
} }
idSyncClipboard -> {
syncClipboard()
true
}
idStopService -> { idStopService -> {
stopMainService() stopMainService()
true true
@@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
} }
} }
private fun syncClipboard() {
MainActivity.rdClipboardManager?.syncClipboard(false)
}
private fun stopMainService() { private fun stopMainService() {
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null) MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
} }

View File

@@ -280,20 +280,20 @@ class InputService : AccessibilityService() {
var textToCommit: String? = null var textToCommit: String? = null
if (keyboardMode == KeyboardMode.Legacy) { // [down] indicates the key's state(down or up).
if (keyEvent.hasChr() && keyEvent.getDown()) { // [press] indicates a click event(down and up).
// https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688
if (keyEvent.hasSeq()) {
textToCommit = keyEvent.getSeq()
} else if (keyboardMode == KeyboardMode.Legacy) {
if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) {
val chr = keyEvent.getChr() val chr = keyEvent.getChr()
if (chr != null) { if (chr != null) {
textToCommit = String(Character.toChars(chr)) textToCommit = String(Character.toChars(chr))
} }
} }
} else if (keyboardMode == KeyboardMode.Translate) { } else if (keyboardMode == KeyboardMode.Translate) {
if (keyEvent.hasSeq() && keyEvent.getDown()) { } else {
val seq = keyEvent.getSeq()
if (seq != null) {
textToCommit = seq
}
}
} }
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit") Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
@@ -320,6 +320,10 @@ class InputService : AccessibilityService() {
} else { } else {
ke?.let { event -> ke?.let { event ->
inputConnection.sendKeyEvent(event) inputConnection.sendKeyEvent(event)
if (keyEvent.getPress()) {
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
inputConnection.sendKeyEvent(actionUpEvent)
}
} }
} }
} }
@@ -333,6 +337,10 @@ class InputService : AccessibilityService() {
for (item in possibleNodes) { for (item in possibleNodes) {
val success = trySendKeyEvent(event, item, textToCommit) val success = trySendKeyEvent(event, item, textToCommit)
if (success) { if (success) {
if (keyEvent.getPress()) {
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
trySendKeyEvent(actionUpEvent, item, textToCommit)
}
break break
} }
} }

View File

@@ -31,14 +31,12 @@ object KeyEventConverter {
} }
var action = 0 var action = 0
if (keyEventProto.getDown()) { if (keyEventProto.getDown() || keyEventProto.getPress()) {
action = KeyEvent.ACTION_DOWN action = KeyEvent.ACTION_DOWN
} else { } else {
action = KeyEvent.ACTION_UP action = KeyEvent.ACTION_UP
} }
// FIXME: The last parameter is the repeat count, not modifiers ?
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
return KeyEvent(0, 0, action, chrValue, 0, modifiers) return KeyEvent(0, 0, action, chrValue, 0, modifiers)
} }

View File

@@ -13,6 +13,8 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.ClipboardManager
import android.os.Bundle
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
@@ -36,6 +38,9 @@ import kotlin.concurrent.thread
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
companion object { companion object {
var flutterMethodChannel: MethodChannel? = null var flutterMethodChannel: MethodChannel? = null
private var _rdClipboardManager: RdClipboardManager? = null
val rdClipboardManager: RdClipboardManager?
get() = _rdClipboardManager;
} }
private val channelTag = "mChannel" private val channelTag = "mChannel"
@@ -85,6 +90,14 @@ class MainActivity : FlutterActivity() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (_rdClipboardManager == null) {
_rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
FFI.setClipboardManager(_rdClipboardManager!!)
}
}
override fun onDestroy() { override fun onDestroy() {
Log.e(logTag, "onDestroy") Log.e(logTag, "onDestroy")
mainService?.let { mainService?.let {
@@ -207,6 +220,10 @@ class MainActivity : FlutterActivity() {
result.success(true) result.success(true)
} }
"try_sync_clipboard" -> {
rdClipboardManager?.syncClipboard(true)
result.success(true)
}
GET_START_ON_BOOT_OPT -> { GET_START_ON_BOOT_OPT -> {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))

View File

@@ -302,6 +302,8 @@ class MainService : Service() {
stopCapture() stopCapture()
FFI.refreshScreen() FFI.refreshScreen()
startCapture() startCapture()
} else {
FFI.refreshScreen()
} }
} }
@@ -431,6 +433,7 @@ class MainService : Service() {
checkMediaPermission() checkMediaPermission()
_isStart = true _isStart = true
FFI.setFrameRawEnable("video",true) FFI.setFrameRawEnable("video",true)
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
return true return true
} }
@@ -439,6 +442,7 @@ class MainService : Service() {
Log.d(logTag, "Stop Capture") Log.d(logTag, "Stop Capture")
FFI.setFrameRawEnable("video",false) FFI.setFrameRawEnable("video",false)
_isStart = false _isStart = false
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
// release video // release video
if (reuseVirtualDisplay) { if (reuseVirtualDisplay) {
// The virtual display video projection can be paused by calling `setSurface(null)`. // The virtual display video projection can be paused by calling `setSurface(null)`.

View File

@@ -0,0 +1,197 @@
package com.carriez.flutter_hbb
import java.nio.ByteBuffer
import java.util.Timer
import java.util.TimerTask
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.util.Log
import androidx.annotation.Keep
import hbb.MessageOuterClass.ClipboardFormat
import hbb.MessageOuterClass.Clipboard
import hbb.MessageOuterClass.MultiClipboards
import ffi.FFI
class RdClipboardManager(private val clipboardManager: ClipboardManager) {
private val logTag = "RdClipboardManager"
private val supportedMimeTypes = arrayOf(
ClipDescription.MIMETYPE_TEXT_PLAIN,
ClipDescription.MIMETYPE_TEXT_HTML
)
// 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`.
// 2. Avoid sending the clipboard data before enabling client clipboard.
// 1) Disable clipboard
// 2) Copy text "a"
// 3) Enable clipboard
// 4) Switch to another app
// 5) Switch back to the app
// 6) "a" should not be sent to the client, because it's copied before enabling clipboard
//
// It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`,
// though the `lastUpdatedClipData` will be set to null once.
private var lastUpdatedClipData: ClipData? = null
private var isClientEnabled = true;
private var _isCaptureStarted = false;
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)
// 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
}
}
val mimeTypeCount = clipData.description.getMimeTypeCount()
val mimeTypes = mutableListOf<String>()
for (i in 0 until mimeTypeCount) {
mimeTypes.add(clipData.description.getMimeType(i))
}
var text: CharSequence? = null;
var html: String? = null;
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
text = clip?.text
}
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
text = clip?.text
html = clip?.htmlText
}
var count = 0
val clips = MultiClipboards.newBuilder()
if (text != null) {
val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString())
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build())
count++
}
if (html != null) {
val content = com.google.protobuf.ByteString.copyFromUtf8(html)
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build())
count++
}
if (count > 0) {
val clipsBytes = clips.build().toByteArray()
val isClientFlag = if (isClient) 1 else 0
val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply {
put(isClientFlag.toByte())
put(clipsBytes)
}
clipsBuf.flip()
lastUpdatedClipData = clipData
Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote")
FFI.onClipboardUpdate(clipsBuf)
}
}
}
private fun isSupportedMimeType(mimeType: String): Boolean {
return supportedMimeTypes.contains(mimeType)
}
private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean {
if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) {
return false
}
val mimeTypeCount = left.description.getMimeTypeCount()
for (i in 0 until mimeTypeCount) {
if (left.description.getMimeType(i) != right.description.getMimeType(i)) {
return false
}
}
if (left.itemCount != right.itemCount) {
return false
}
for (i in 0 until left.itemCount) {
val mimeType = left.description.getMimeType(i)
if (!isSupportedMimeType(mimeType)) {
continue
}
val leftItem = left.getItemAt(i)
val rightItem = right.getItemAt(i)
if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) {
if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) {
return false
}
}
}
return true
}
fun setCaptureStarted(started: Boolean) {
_isCaptureStarted = started
}
@Keep
fun rustEnableClientClipboard(enable: Boolean) {
Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
isClientEnabled = enable
lastUpdatedClipData = null
}
fun syncClipboard(isClient: Boolean) {
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled")
if (isClient && !isClientEnabled) {
return
}
checkPrimaryClip(isClient)
}
@Keep
fun rustUpdateClipboard(clips: ByteArray) {
val clips = MultiClipboards.parseFrom(clips)
var mimeTypes = mutableListOf<String>()
var text: String? = null
var html: String? = null
for (clip in clips.getClipboardsList()) {
when (clip.format) {
ClipboardFormat.Text -> {
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
text = String(clip.content.toByteArray(), Charsets.UTF_8)
}
ClipboardFormat.Html -> {
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML)
html = String(clip.content.toByteArray(), Charsets.UTF_8)
}
ClipboardFormat.ImageRgba -> {
}
ClipboardFormat.ImagePng -> {
}
else -> {
Log.e(logTag, "Unsupported clipboard format: ${clip.format}")
}
}
}
val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray())
var item: ClipData.Item? = null
if (text == null) {
Log.e(logTag, "No text content in clipboard")
return
} else {
if (html == null) {
item = ClipData.Item(text)
} else {
item = ClipData.Item(text, html)
}
}
if (item == null) {
Log.e(logTag, "No item in clipboard")
return
}
val clipData = ClipData(clipDescription, item)
lastUpdatedClipData = clipData
clipboardManager.setPrimaryClip(clipData)
}
}

View File

@@ -5,12 +5,15 @@ package ffi
import android.content.Context import android.content.Context
import java.nio.ByteBuffer import java.nio.ByteBuffer
import com.carriez.flutter_hbb.RdClipboardManager
object FFI { object FFI {
init { init {
System.loadLibrary("rustdesk") System.loadLibrary("rustdesk")
} }
external fun init(ctx: Context) external fun init(ctx: Context)
external fun setClipboardManager(clipboardManager: RdClipboardManager)
external fun startServer(app_dir: String, custom_client_config: String) external fun startServer(app_dir: String, custom_client_config: String)
external fun startService() external fun startService()
external fun onVideoFrameUpdate(buf: ByteBuffer) external fun onVideoFrameUpdate(buf: ByteBuffer)
@@ -20,4 +23,6 @@ object FFI {
external fun setFrameRawEnable(name: String, value: Boolean) external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String) external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String external fun getLocalOption(key: String): String
external fun onClipboardUpdate(clips: ByteBuffer)
external fun isServiceClipboardEnabled(): Boolean
} }

View File

@@ -1,18 +1,3 @@
buildscript {
ext.kotlin_version = '1.9.10'
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.14'
}
}
allprojects { allprojects {
repositories { repositories {
google() google()
@@ -24,6 +9,8 @@ allprojects {
rootProject.buildDir = '../build' rootProject.buildDir = '../build'
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip

View File

@@ -1,11 +1,25 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
def properties = new Properties()
assert localPropertiesFile.exists() repositories {
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk") plugins {
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
}
include ":app"

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -68,6 +68,7 @@ function build {
pushd "$SCRIPTDIR/.." pushd "$SCRIPTDIR/.."
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed" $VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
popd popd
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies" echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then

View File

@@ -1,7 +1,5 @@
#!/bin/bash #!/bin/bash
set -x
# #
# Script to build F-Droid release of RustDesk # Script to build F-Droid release of RustDesk
# #
@@ -23,6 +21,43 @@ set -x
# + build: perform actual build of APK file # + build: perform actual build of APK file
# #
# Start of functions
# Install Flutter of version `VERSION` from Github repository
# into directory `FLUTTER_DIR` and apply patches if needed
prepare_flutter() {
VERSION="${1}"
FLUTTER_DIR="${2}"
if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then
git clone https://github.com/flutter/flutter "${FLUTTER_DIR}"
fi
pushd "${FLUTTER_DIR}"
git restore .
git checkout "${VERSION}"
# Patch flutter
if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then
git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
fi
flutter config --no-analytics
popd # ${FLUTTER_DIR}
}
# Start of script
set -x
# Note current working directory as root dir for patches
ROOTDIR="${PWD}"
# Parse command-line arguments # Parse command-line arguments
VERNAME="${1}" VERNAME="${1}"
@@ -101,18 +136,27 @@ prebuild)
.env.CARGO_NDK_VERSION \ .env.CARGO_NDK_VERSION \
.github/workflows/flutter-build.yml)" .github/workflows/flutter-build.yml)"
# Flutter used to compile main Rustdesk library
FLUTTER_VERSION="$(yq -r \ FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \ .env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)" .github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \ FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \ .env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)" .github/workflows/flutter-build.yml)"
fi fi
# Flutter used to compile Flutter<->Rust bridge files
FLUTTER_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/bridge.yml)"
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \ FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_RUST_BRIDGE_VERSION \ .env.FLUTTER_RUST_BRIDGE_VERSION \
.github/workflows/flutter-build.yml)" .github/workflows/bridge.yml)"
NDK_VERSION="$(yq -r \ NDK_VERSION="$(yq -r \
.env.NDK_VERSION \ .env.NDK_VERSION \
@@ -127,6 +171,7 @@ prebuild)
.github/workflows/flutter-build.yml)" .github/workflows/flutter-build.yml)"
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] || if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
[ -z "${FLUTTER_BRIDGE_VERSION}" ] ||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] || [ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] || [ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
[ -z "${VCPKG_COMMIT_ID}" ]; then [ -z "${VCPKG_COMMIT_ID}" ]; then
@@ -163,24 +208,6 @@ prebuild)
sdkmanager --install "ndk;${NDK_VERSION}" sdkmanager --install "ndk;${NDK_VERSION}"
fi fi
# Install Flutter
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
pushd "${HOME}"
git clone https://github.com/flutter/flutter
pushd flutter
git reset --hard "${FLUTTER_VERSION}"
flutter config --no-analytics
popd # flutter
popd # ${HOME}
fi
# Install Rust # Install Rust
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
@@ -205,14 +232,18 @@ prebuild)
cargo install \ cargo install \
cargo-ndk \ cargo-ndk \
--version "${CARGO_NDK_VERSION}" --version "${CARGO_NDK_VERSION}" \
--locked
# Install rust bridge generator # Install rust bridge generator
cargo install cargo-expand cargo install \
cargo-expand \
--locked
cargo install flutter_rust_bridge_codegen \ cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \ --version "${FLUTTER_RUST_BRIDGE_VERSION}" \
--features "uuid" --features "uuid" \
--locked
# Populate native vcpkg dependencies # Populate native vcpkg dependencies
@@ -275,12 +306,66 @@ prebuild)
git apply res/fdroid/patches/*.patch git apply res/fdroid/patches/*.patch
# If Flutter version used to generate bridge files differs from Flutter
# version used to compile Rustdesk library, generate bridge using the
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
# Install Flutter bridge version
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
# Save changes
git add .
# Edit pubspec to make flutter bridge version work
sed \
-i \
-e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \
flutter/pubspec.yaml
# Download Flutter dependencies
pushd flutter
flutter clean
flutter packages pub get
popd # flutter
# Generate FFI bindings
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart
# Add bridge files to save-list
git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.*
# Restore everything
git checkout '*'
git clean -dffx
git reset
fi
# Install Flutter version for RustDesk library build
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
sed \ sed \
-i \ -i \
-e '/gms/d' \ -e '/gms/d' \
flutter/android/build.gradle \ flutter/android/build.gradle \
flutter/android/app/build.gradle flutter/android/app/build.gradle
# `firebase_analytics` is not in these files now, but we still keep the following lines.
sed \ sed \
-i \ -i \
-e '/firebase_analytics/d' \ -e '/firebase_analytics/d' \
@@ -296,33 +381,6 @@ prebuild)
-e '/firebase/Id' \ -e '/firebase/Id' \
flutter/lib/main.dart flutter/lib/main.dart
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
# Fix for android 3.13.9
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
sed \
-i \
-e 's/uni_links_desktop/#uni_links_desktop/g' \
flutter/pubspec.yaml
set --
while read -r _1; do
set -- "$@" "${_1}"
done 0<<.a
$(find flutter/lib/ -type f -name "*dart*")
.a
sed \
-i \
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
"$@"
set --
fi
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
;; ;;
build) build)
# build: perform actual build of APK file # build: perform actual build of APK file
@@ -334,9 +392,12 @@ build)
# '.github/workflows/flutter-build.yml' # '.github/workflows/flutter-build.yml'
# #
# Flutter used to compile main Rustdesk library
FLUTTER_VERSION="$(yq -r \ FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \ .env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)" .github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \ FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \ .env.FLUTTER_VERSION \
@@ -372,16 +433,11 @@ build)
pushd flutter pushd flutter
flutter clean
flutter packages pub get flutter packages pub get
popd # flutter popd # flutter
# Generate FFI bindings
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart
# Build host android deps # Build host android deps
bash flutter/build_android_deps.sh "${ANDROID_ABI}" bash flutter/build_android_deps.sh "${ANDROID_ABI}"

View File

@@ -2,4 +2,6 @@
# https://docs.flutter.dev/deployment/ios # https://docs.flutter.dev/deployment/ios
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# no obfuscate, because no easy to check errors # 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 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

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>12.0</string> <string>13.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,10 +1,7 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
platform :ios, '12.0' platform :ios, '13.0'
project 'Runner', { project 'Runner', {
'Debug' => :debug, 'Debug' => :debug,

View File

@@ -133,10 +133,10 @@ SPEC CHECKSUMS:
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c
COCOAPODS: 1.15.2 COCOAPODS: 1.15.2

View File

@@ -347,7 +347,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -491,7 +491,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -541,7 +541,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;

View File

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

View File

@@ -1,2 +1,4 @@
#!/usr/bin/env bash #!/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 cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

View File

@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart'; import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/model.dart'; import 'models/model.dart';
import 'models/platform_model.dart'; import 'models/platform_model.dart';
@@ -50,6 +51,9 @@ final isLinux = isLinux_;
final isDesktop = isDesktop_; final isDesktop = isDesktop_;
final isWeb = isWeb_; final isWeb = isWeb_;
final isWebDesktop = isWebDesktop_; final isWebDesktop = isWebDesktop_;
final isWebOnWindows = isWebOnWindows_;
final isWebOnLinux = isWebOnLinux_;
final isWebOnMacOs = isWebOnMacOS_;
var isMobile = isAndroid || isIOS; var isMobile = isAndroid || isIOS;
var version = ''; var version = '';
int androidVersion = 0; int androidVersion = 0;
@@ -60,6 +64,9 @@ int androidVersion = 0;
// So we need to use this flag to enable/disable resizable. // So we need to use this flag to enable/disable resizable.
bool _linuxWindowResizable = true; bool _linuxWindowResizable = true;
// Only used on Windows(window manager).
bool _ignoreDevicePixelRatio = true;
/// only available for Windows target /// only available for Windows target
int windowsBuildNumber = 0; int windowsBuildNumber = 0;
DesktopType? desktopType; DesktopType? desktopType;
@@ -347,6 +354,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 224, 224, 224), hoverColor: Color.fromARGB(255, 224, 224, 224),
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
dialogBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
elevation: 15, elevation: 15,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -442,6 +452,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 45, 46, 53), hoverColor: Color.fromARGB(255, 45, 46, 53),
scaffoldBackgroundColor: Color(0xFF18191E), scaffoldBackgroundColor: Color(0xFF18191E),
dialogBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E),
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
elevation: 15, elevation: 15,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -545,9 +558,9 @@ class MyTheme {
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
} }
static void changeDarkMode(ThemeMode mode) async { static Future<void> changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode); Get.changeThemeMode(mode);
if (desktopType == DesktopType.main || isAndroid || isIOS) { if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
if (mode == ThemeMode.system) { if (mode == ThemeMode.system) {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: defaultOptionTheme); key: kCommConfKeyTheme, value: defaultOptionTheme);
@@ -555,7 +568,7 @@ class MyTheme {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString()); key: kCommConfKeyTheme, value: mode.toShortString());
} }
await bind.mainChangeTheme(dark: mode.toShortString()); if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
// Synchronize the window theme of the system. // Synchronize the window theme of the system.
updateSystemWindowTheme(); updateSystemWindowTheme();
} }
@@ -671,10 +684,12 @@ closeConnection({String? id}) {
overlays: SystemUiOverlay.values); overlays: SystemUiOverlay.values);
gFFI.chatModel.hideChatOverlay(); gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
stateGlobal.isInMainPage = true;
}(); }();
} else { } else {
if (isWeb) { if (isWeb) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
stateGlobal.isInMainPage = true;
} else { } else {
final controller = Get.find<DesktopTabController>(); final controller = Get.find<DesktopTabController>();
controller.closeBy(id); controller.closeBy(id);
@@ -1162,33 +1177,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
dialogManager.dismissAll(); dialogManager.dismissAll();
})); }));
} }
if (reconnect != null && title == "Connection Error") { if (reconnect != null &&
title == "Connection Error" &&
reconnectTimeout != null) {
// `enabled` is used to disable the dialog button once the button is clicked. // `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs; final enabled = true.obs;
final button = reconnectTimeout != null final button = Obx(() => _ReconnectCountDownButton(
? Obx(() => _ReconnectCountDownButton( second: reconnectTimeout,
second: reconnectTimeout, onPressed: enabled.isTrue
onPressed: enabled.isTrue ? () {
? () { // Disable the button
// Disable the button enabled.value = false;
enabled.value = false; reconnect(dialogManager, sessionId, false);
reconnect(dialogManager, sessionId, false); }
} : null,
: null, ));
))
: Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button); buttons.insert(0, button);
} }
if (link.isNotEmpty) { if (link.isNotEmpty) {
@@ -1611,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) {
return SvgPicture.asset('assets/$platform.svg', height: size, width: size); return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
} }
class OffsetDevicePixelRatio {
Offset offset;
final double devicePixelRatio;
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
}
class LastWindowPosition { class LastWindowPosition {
double? width; double? width;
double? height; double? height;
@@ -1699,8 +1696,10 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
if (isFullscreen || isMaximized) { if (isFullscreen || isMaximized) {
setPreFrame(); setPreFrame();
} else { } else {
position = await windowManager.getPosition(); position = await windowManager.getPosition(
sz = await windowManager.getSize(); ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
sz = await windowManager.getSize(
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
} }
break; break;
default: default:
@@ -1818,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) {
} }
/// return null means center /// return null means center
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset( Future<Offset?> _adjustRestoreMainWindowOffset(
double? left, double? left,
double? top, double? top,
double? width, double? width,
@@ -1832,13 +1831,9 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
double? frameTop; double? frameTop;
double? frameRight; double? frameRight;
double? frameBottom; double? frameBottom;
double devicePixelRatio = 1.0;
if (isDesktop || isWebDesktop) { if (isDesktop || isWebDesktop) {
for (final screen in await window_size.getScreenList()) { for (final screen in await window_size.getScreenList()) {
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
devicePixelRatio = screen.scaleFactor;
}
frameLeft = frameLeft == null frameLeft = frameLeft == null
? screen.visibleFrame.left ? screen.visibleFrame.left
: min(screen.visibleFrame.left, frameLeft); : min(screen.visibleFrame.left, frameLeft);
@@ -1872,7 +1867,7 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
top < frameTop!) { top < frameTop!) {
return null; return null;
} else { } else {
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio); return Offset(left, top);
} }
} }
@@ -1932,47 +1927,23 @@ Future<bool> restoreWindowPosition(WindowType type,
} }
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height); final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset( final offsetLeftTop = await _adjustRestoreMainWindowOffset(
lpos.offsetWidth, lpos.offsetWidth,
lpos.offsetHeight, lpos.offsetHeight,
size.width, size.width,
size.height, size.height,
); );
debugPrint( debugPrint(
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}"); "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
switch (type) { switch (type) {
case WindowType.Main: case WindowType.Main:
// https://github.com/rustdesk/rustdesk/issues/8038
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
// So we need to adjust the offset by the scale factor.
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
if (isWindows) {
double? curDevicePixelRatio;
Offset curPos = await windowManager.getPosition();
for (final screen in await window_size.getScreenList()) {
if (isPointInRect(curPos, screen.visibleFrame)) {
curDevicePixelRatio = screen.scaleFactor;
}
}
if (curDevicePixelRatio != null &&
curDevicePixelRatio != 0 &&
offsetDevicePixelRatio != null) {
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
final scale =
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
offsetDevicePixelRatio.offset =
offsetDevicePixelRatio.offset.scale(scale, scale);
debugPrint(
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
}
}
}
restorePos() async { restorePos() async {
if (offsetDevicePixelRatio == null) { if (offsetLeftTop == null) {
await windowManager.center(); await windowManager.center();
} else { } else {
await windowManager.setPosition(offsetDevicePixelRatio.offset); await windowManager.setPosition(offsetLeftTop,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
} }
} }
if (lpos.isMaximized == true) { if (lpos.isMaximized == true) {
@@ -1981,20 +1952,39 @@ Future<bool> restoreWindowPosition(WindowType type,
await windowManager.maximize(); await windowManager.maximize();
} }
} else { } else {
if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) { final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
await windowManager.setSize(size); if (isWindows) {
if (storeSize) {
// We need to set the window size first to avoid the incorrect size in some special cases.
// E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
// The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
// After restoring, the size will be incorrect.
// See known issue in https://github.com/rustdesk/rustdesk/pull/9840
await windowManager.setSize(size,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
await restorePos();
if (storeSize) {
await windowManager.setSize(size,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
} else {
if (storeSize) {
await windowManager.setSize(size,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
await restorePos();
} }
await restorePos();
} }
return true; return true;
default: default:
final wc = WindowController.fromWindowId(windowId!); final wc = WindowController.fromWindowId(windowId!);
restoreFrame() async { restoreFrame() async {
if (offsetDevicePixelRatio == null) { if (offsetLeftTop == null) {
await wc.center(); await wc.center();
} else { } else {
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx, final frame = Rect.fromLTWH(
offsetDevicePixelRatio.offset.dy, size.width, size.height); offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
await wc.setFrame(frame); await wc.setFrame(frame);
} }
} }
@@ -2026,6 +2016,8 @@ Future<bool> restoreWindowPosition(WindowType type,
return false; return false;
} }
var webInitialLink = "";
/// Initialize uni links for macos/windows /// Initialize uni links for macos/windows
/// ///
/// [Availability] /// [Availability]
@@ -2042,7 +2034,12 @@ Future<bool> initUniLinks() async {
if (initialLink == null || initialLink.isEmpty) { if (initialLink == null || initialLink.isEmpty) {
return false; return false;
} }
return handleUriLink(uriString: initialLink); if (isWeb) {
webInitialLink = initialLink;
return false;
} else {
return handleUriLink(uriString: initialLink);
}
} catch (err) { } catch (err) {
debugPrintStack(label: "$err"); debugPrintStack(label: "$err");
return false; return false;
@@ -2055,7 +2052,7 @@ Future<bool> initUniLinks() async {
/// ///
/// Returns a [StreamSubscription] which can listen the uni links. /// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) { StreamSubscription? listenUniLinks({handleByFlutter = true}) {
if (isLinux) { if (isLinux || isWeb) {
return null; return null;
} }
@@ -2248,7 +2245,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
} }
} }
var key = uri.queryParameters["key"]; var queryParameters =
uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
var key = queryParameters["key"];
if (id != null) { if (id != null) {
if (key != null) { if (key != null) {
id = "$id?key=$key"; id = "$id?key=$key";
@@ -2257,7 +2257,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
if (isMobile) { if (isMobile) {
if (id != null) { if (id != null) {
final forceRelay = uri.queryParameters["relay"] != null; final forceRelay = queryParameters["relay"] != null;
connect(Get.context!, id, forceRelay: forceRelay); connect(Get.context!, id, forceRelay: forceRelay);
return null; return null;
} }
@@ -2267,7 +2267,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
if (command != null && id != null) { if (command != null && id != null) {
args.add(command); args.add(command);
args.add(id); args.add(id);
var param = uri.queryParameters; var param = queryParameters;
String? password = param["password"]; String? password = param["password"];
if (password != null) args.addAll(['--password', password]); if (password != null) args.addAll(['--password', password]);
String? switch_uuid = param["switch_uuid"]; String? switch_uuid = param["switch_uuid"];
@@ -2285,16 +2285,19 @@ connectMainDesktop(String id,
required bool isRDP, required bool isRDP,
bool? forceRelay, bool? forceRelay,
String? password, String? password,
String? connToken,
bool? isSharedPassword}) async { bool? isSharedPassword}) async {
if (isFileTransfer) { if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id, await rustDeskWinManager.newFileTransfer(id,
password: password, password: password,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay); forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) { } else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP, await rustDeskWinManager.newPortForward(id, isRDP,
password: password, password: password,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay); forceRelay: forceRelay);
} else { } else {
await rustDeskWinManager.newRemoteDesktop(id, await rustDeskWinManager.newRemoteDesktop(id,
@@ -2314,6 +2317,7 @@ connect(BuildContext context, String id,
bool isRDP = false, bool isRDP = false,
bool forceRelay = false, bool forceRelay = false,
String? password, String? password,
String? connToken,
bool? isSharedPassword}) async { bool? isSharedPassword}) async {
if (id == '') return; if (id == '') return;
if (!isDesktop || desktopType == DesktopType.main) { if (!isDesktop || desktopType == DesktopType.main) {
@@ -2355,24 +2359,40 @@ connect(BuildContext context, String id,
'password': password, 'password': password,
'isSharedPassword': isSharedPassword, 'isSharedPassword': isSharedPassword,
'forceRelay': forceRelay, 'forceRelay': forceRelay,
'connToken': connToken,
}); });
} }
} else { } else {
if (isFileTransfer) { if (isFileTransfer) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) { if (isAndroid) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) { if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
return; if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
}
} }
} }
Navigator.push( if (isWeb) {
context, Navigator.push(
MaterialPageRoute( context,
builder: (BuildContext context) => FileManagerPage( MaterialPageRoute(
id: id, password: password, isSharedPassword: isSharedPassword), builder: (BuildContext context) =>
), desktop_file_manager.FileManagerPage(
); id: id,
password: password,
isSharedPassword: isSharedPassword),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
} else { } else {
if (isWebDesktop) { if (isWeb) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -2396,6 +2416,7 @@ connect(BuildContext context, String id,
); );
} }
} }
stateGlobal.isInMainPage = false;
} }
FocusScopeNode currentFocus = FocusScope.of(context); FocusScopeNode currentFocus = FocusScope.of(context);
@@ -2480,9 +2501,20 @@ Future<void> onActiveWindowChanged() async {
// But the app will not close. // But the app will not close.
// //
// No idea why we need to delay here, `terminate()` itself is also an async function. // No idea why we need to delay here, `terminate()` itself is also an async function.
Future.delayed(Duration.zero, () { //
RdPlatformChannel.instance.terminate(); // A quick workaround, use `Timer.periodic` to avoid the app not closing.
}); // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
// may not work since `Flutter 3.24.4`, see the following logs.
// A delay will allow the app to close.
//
//```
// embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
// 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
// embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
// 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
// ```
periodic_immediate(
Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
} }
} }
} }
@@ -2575,7 +2607,7 @@ class ServerConfig {
config['relay'] = relayServer.trim(); config['relay'] = relayServer.trim();
config['api'] = apiServer.trim(); config['api'] = apiServer.trim();
config['key'] = key.trim(); config['key'] = key.trim();
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
.split('') .split('')
.reversed .reversed
.join(); .join();
@@ -2698,30 +2730,6 @@ Future<bool> osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
} }
class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that does not let the user scroll.
const DraggableNeverScrollableScrollPhysics({super.parent});
@override
DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
}
@override
bool shouldAcceptUserOffset(ScrollMetrics position) {
// TODO: find a better solution to check if the offset change is caused by the scrollbar.
// Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
if (position is ScrollPositionWithSingleContext) {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
return position.activity is IdleScrollActivity;
}
return false;
}
@override
bool get allowImplicitScrolling => false;
}
Widget futureBuilder( Widget futureBuilder(
{required Future? future, required Widget Function(dynamic data) hasData}) { {required Future? future, required Widget Function(dynamic data) hasData}) {
return FutureBuilder( return FutureBuilder(
@@ -2801,7 +2809,7 @@ Widget buildRemoteBlock(
onExit: (event) => block.value = false, onExit: (event) => block.value = false,
child: Stack(children: [ child: Stack(children: [
// scope block tab // 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 // mask block click, cm not block click and still use check_click_time to avoid block local click
if (mask) if (mask)
Offstage( Offstage(
@@ -2813,6 +2821,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, Widget unreadMessageCountBuilder(RxInt? count,
{double? size, double? fontSize}) { {double? size, double? fontSize}) {
return Obx(() => Offstage( return Obx(() => Offstage(
@@ -3145,9 +3158,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs, importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
String? text) { String? text) {
text = text?.trim();
if (text != null && text.isNotEmpty) { if (text != null && text.isNotEmpty) {
try { try {
final sc = ServerConfig.decode(text); final sc = ServerConfig.decode(text);
if (isWeb || isIOS) {
sc.relayServer = '';
}
if (sc.idServer.isNotEmpty) { if (sc.idServer.isNotEmpty) {
Future<bool> success = setServerConfig(controllers, errMsgs, sc); Future<bool> success = setServerConfig(controllers, errMsgs, sc);
success.then((value) { success.then((value) {
@@ -3587,3 +3604,37 @@ List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
SubWindowResizeEdge.topRight, SubWindowResizeEdge.topRight,
] ]
: null; : null;
void earlyAssert() {
assert('\1' == '1');
}
void checkUpdate() {
if (!isWeb) {
if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
(Map<String, dynamic> evt) async {
if (evt['url'] is String) {
stateGlobal.updateUrl.value = evt['url'];
}
});
Timer(const Duration(seconds: 1), () async {
bind.mainGetSoftwareUpdateUrl();
});
}
}
}
// 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;
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:bot_toast/bot_toast.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -11,6 +12,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -61,15 +63,16 @@ class _AddressBookState extends State<AddressBook> {
retry: null, // remove retry retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''), close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildAddressBookDesktop() ? _buildAddressBookPortrait()
: _buildAddressBookMobile()) : _buildAddressBookLandscape()),
),
], ],
); );
} }
}); });
Widget _buildAddressBookDesktop() { Widget _buildAddressBookLandscape() {
return Row( return Row(
children: [ children: [
Offstage( Offstage(
@@ -106,7 +109,7 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
Widget _buildAddressBookMobile() { Widget _buildAddressBookPortrait() {
const padding = 8.0; const padding = 8.0;
return Column( return Column(
children: [ children: [
@@ -239,14 +242,15 @@ class _AddressBookState extends State<AddressBook> {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
} }
}, },
customButton: Container( customButton: Obx(() => Container(
height: isDesktop ? 48 : 40, height: stateGlobal.isPortrait.isFalse ? 48 : 40,
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)), child:
Icon(Icons.arrow_drop_down), buildItem(gFFI.abModel.currentName.value, button: true)),
]), Icon(Icons.arrow_drop_down),
), ]),
)),
underline: Container( underline: Container(
height: 0.7, height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
@@ -282,7 +286,7 @@ class _AddressBookState extends State<AddressBook> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
), ).workaroundFreezeLinuxMint(),
), ),
searchMatchFn: (item, searchValue) { searchMatchFn: (item, searchValue) {
return item.value return item.value
@@ -313,13 +317,14 @@ class _AddressBookState extends State<AddressBook> {
Widget _buildTags() { Widget _buildTags() {
return Obx(() { return Obx(() {
final List tags; List tags;
if (gFFI.abModel.sortTags.value) { if (gFFI.abModel.sortTags.value) {
tags = gFFI.abModel.currentAbTags.toList(); tags = gFFI.abModel.currentAbTags.toList();
tags.sort(); tags.sort();
} else { } else {
tags = gFFI.abModel.currentAbTags; tags = gFFI.abModel.currentAbTags.toList();
} }
tags = [kUntagged, ...tags].toList();
final editPermission = gFFI.abModel.current.canWrite(); final editPermission = gFFI.abModel.current.canWrite();
tagBuilder(String e) { tagBuilder(String e) {
return AddressBookTag( return AddressBookTag(
@@ -335,8 +340,8 @@ class _AddressBookState extends State<AddressBook> {
showActionMenu: editPermission); showActionMenu: editPermission);
} }
final gridView = DynamicGridView.builder( gridView(bool isPortrait) => DynamicGridView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(), gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length, itemCount: tags.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -344,9 +349,9 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e); return tagBuilder(e);
}); });
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? gridView ? gridView(false)
: LimitedBox(maxHeight: maxHeight, child: gridView); : LimitedBox(maxHeight: maxHeight, child: gridView(true)));
}); });
} }
@@ -356,7 +361,6 @@ class _AddressBookState extends State<AddressBook> {
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.abModel.currentAbPeers,
)), )),
); );
} }
@@ -506,20 +510,21 @@ class _AddressBookState extends State<AddressBook> {
double marginBottom = 4; double marginBottom = 4;
row({required Widget lable, required Widget input}) { row({required Widget lable, required Widget input}) {
return Row( makeChild(bool isPortrait) => Row(
children: [ children: [
!isMobile !isPortrait
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10)) child: lable.marginOnly(right: 10))
: SizedBox.shrink(), : SizedBox.shrink(),
Expanded( Expanded(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200), constraints: const BoxConstraints(minWidth: 200),
child: input), child: input),
), ),
], ],
).marginOnly(bottom: !isMobile ? 8 : 0); ).marginOnly(bottom: !isPortrait ? 8 : 0);
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
} }
return CustomAlertDialog( return CustomAlertDialog(
@@ -542,24 +547,29 @@ class _AddressBookState extends State<AddressBook> {
), ),
], ],
), ),
input: TextField( input: Obx(() => TextField(
controller: idController, controller: idController,
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'), labelText: stateGlobal.isPortrait.isFalse
errorText: errorMsg, ? null
errorMaxLines: 5), : translate('ID'),
)), errorText: errorMsg,
errorMaxLines: 5),
).workaroundFreezeLinuxMint())),
row( row(
lable: Text( lable: Text(
translate('Alias'), translate('Alias'),
style: style, style: style,
), ),
input: TextField( input: Obx(() => TextField(
controller: aliasController, controller: aliasController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'), labelText: stateGlobal.isPortrait.isFalse
)), ? null
: translate('Alias'),
),
).workaroundFreezeLinuxMint()),
), ),
if (isCurrentAbShared) if (isCurrentAbShared)
row( row(
@@ -567,24 +577,28 @@ class _AddressBookState extends State<AddressBook> {
translate('Password'), translate('Password'),
style: style, style: style,
), ),
input: TextField( input: Obx(
controller: passwordController, () => TextField(
obscureText: !passwordVisible, controller: passwordController,
decoration: InputDecoration( obscureText: !passwordVisible,
labelText: !isMobile ? null : translate('Password'), decoration: InputDecoration(
suffixIcon: IconButton( labelText: stateGlobal.isPortrait.isFalse
icon: Icon( ? null
passwordVisible : translate('Password'),
? Icons.visibility suffixIcon: IconButton(
: Icons.visibility_off, icon: Icon(
color: MyTheme.lightTheme.primaryColor), passwordVisible
onPressed: () { ? Icons.visibility
setState(() { : Icons.visibility_off,
passwordVisible = !passwordVisible; color: MyTheme.lightTheme.primaryColor),
}); onPressed: () {
}, setState(() {
passwordVisible = !passwordVisible;
});
},
),
), ),
), ).workaroundFreezeLinuxMint(),
)), )),
if (gFFI.abModel.currentAbTags.isNotEmpty) if (gFFI.abModel.currentAbTags.isNotEmpty)
Align( Align(
@@ -657,6 +671,14 @@ class _AddressBookState extends State<AddressBook> {
} else { } else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+")); final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(','); field = tags.join(',');
for (var t in [kUntagged, translate(kUntagged)]) {
if (tags.contains(t)) {
BotToast.showText(
contentColor: Colors.red, text: 'Tag name cannot be "$t"');
isInProgress = false;
return;
}
}
gFFI.abModel.addTags(tags); gFFI.abModel.addTags(tags);
// final currentPeers // final currentPeers
} }
@@ -682,7 +704,7 @@ class _AddressBookState extends State<AddressBook> {
), ),
controller: controller, controller: controller,
autofocus: true, autofocus: true,
), ).workaroundFreezeLinuxMint(),
), ),
], ],
), ),
@@ -729,12 +751,14 @@ class AddressBookTag extends StatelessWidget {
} }
const double radius = 8; const double radius = 8;
final isUnTagged = name == kUntagged;
final showAction = showActionMenu && !isUnTagged;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
onTapDown: showActionMenu ? setPosition : null, onTapDown: showAction ? setPosition : null,
onSecondaryTapDown: showActionMenu ? setPosition : null, onSecondaryTapDown: showAction ? setPosition : null,
onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null, onSecondaryTap: showAction ? () => _showMenu(context, pos) : null,
onLongPress: showActionMenu ? () => _showMenu(context, pos) : null, onLongPress: showAction ? () => _showMenu(context, pos) : null,
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: tags.contains(name) color: tags.contains(name)
@@ -746,17 +770,18 @@ class AddressBookTag extends StatelessWidget {
child: IntrinsicWidth( child: IntrinsicWidth(
child: Row( child: Row(
children: [ children: [
Container( if (!isUnTagged)
width: radius, Container(
height: radius, width: radius,
decoration: BoxDecoration( height: radius,
shape: BoxShape.circle, decoration: BoxDecoration(
color: tags.contains(name) shape: BoxShape.circle,
? Colors.white color: tags.contains(name)
: gFFI.abModel.getCurrentAbTagColor(name)), ? Colors.white
).marginOnly(right: radius / 2), : gFFI.abModel.getCurrentAbTagColor(name)),
).marginOnly(right: radius / 2),
Expanded( Expanded(
child: Text(name, child: Text(isUnTagged ? translate(name) : name,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: tags.contains(name) ? Colors.white : null)), color: tags.contains(name) ? Colors.white : null)),

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
const _kWindowsSystemSound = 'System Sound'; const _kSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device); typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function( typedef AudioInputBuilder = Widget Function(
@@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget {
: super(key: key); : super(key: key);
static String getDefault() { static String getDefault() {
if (isWindows) return translate('System Sound'); if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound);
return ''; return '';
} }
@@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget {
static Future<Map<String, Object>> getDevicesInfo( static Future<Map<String, Object>> getDevicesInfo(
bool isCm, bool isVoiceCall) async { bool isCm, bool isVoiceCall) async {
List<String> devices = (await bind.mainGetSoundInputs()).toList(); List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) { if (bind.mainAudioSupportLoopback()) {
devices.insert(0, translate(_kWindowsSystemSound)); devices.insert(0, translate(_kSystemSound));
} }
String current = await getValue(isCm, isVoiceCall); String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current}; return {'devices': devices, 'current': current};

View File

@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: widget.peer.tags.isNotEmpty : widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'

View File

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

View File

@@ -0,0 +1,38 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
return Row(
children: [
Expanded(
child: Row(
children: [
AutoSizeText(
translate('Control Remote Desktop'),
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
],
);
}

View File

@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
String get name => translate('uppercase'); String get name => translate('uppercase');
@override @override
bool validate(String value) { bool validate(String value) {
return value.contains(RegExp(r'[A-Z]')); return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toUpperCase() == character &&
character.toLowerCase() != character;
});
} }
} }
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
@override @override
bool validate(String value) { bool validate(String value) {
return value.contains(RegExp(r'[a-z]')); return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toLowerCase() == character &&
character.toUpperCase() != character;
});
} }
} }

View File

@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@@ -139,7 +140,7 @@ void changeIdDialog() {
msg = ''; msg = '';
}); });
}, },
), ).workaroundFreezeLinuxMint(),
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
@@ -200,13 +201,14 @@ void changeWhiteList({Function()? callback}) async {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
maxLines: null, maxLines: null,
decoration: InputDecoration( decoration: InputDecoration(
errorText: msg.isEmpty ? null : translate(msg), errorText: msg.isEmpty ? null : translate(msg),
), ),
controller: controller, controller: controller,
enabled: !isOptFixed, enabled: !isOptFixed,
autofocus: true), autofocus: true)
.workaroundFreezeLinuxMint(),
), ),
], ],
), ),
@@ -286,22 +288,23 @@ Future<String> changeDirectAccessPort(
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
maxLines: null, maxLines: null,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: InputDecoration( decoration: InputDecoration(
hintText: '21118', hintText: '21118',
isCollapsed: true, isCollapsed: true,
prefix: Text('$currentIP : '), prefix: Text('$currentIP : '),
suffix: IconButton( suffix: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16), icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())), onPressed: () => controller.clear())),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow(RegExp( 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])$')), 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, controller: controller,
autofocus: true), autofocus: true)
.workaroundFreezeLinuxMint(),
), ),
], ],
), ),
@@ -334,21 +337,22 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
maxLines: null, maxLines: null,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: InputDecoration( decoration: InputDecoration(
hintText: '10', hintText: '10',
isCollapsed: true, isCollapsed: true,
suffix: IconButton( suffix: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16), icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())), onPressed: () => controller.clear())),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow(RegExp( 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])$')), 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, controller: controller,
autofocus: true), autofocus: true)
.workaroundFreezeLinuxMint(),
), ),
], ],
), ),
@@ -380,6 +384,7 @@ class DialogTextField extends StatelessWidget {
final FocusNode? focusNode; final FocusNode? focusNode;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
static const kUsernameTitle = 'Username'; static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined); static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -397,6 +402,7 @@ class DialogTextField extends StatelessWidget {
this.hintText, this.hintText,
this.keyboardType, this.keyboardType,
this.inputFormatters, this.inputFormatters,
this.maxLength,
required this.title, required this.title,
required this.controller}) required this.controller})
: super(key: key); : super(key: key);
@@ -423,7 +429,8 @@ class DialogTextField extends StatelessWidget {
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
), maxLength: maxLength,
).workaroundFreezeLinuxMint(),
), ),
], ],
).paddingSymmetric(vertical: 4.0); ).paddingSymmetric(vertical: 4.0);
@@ -680,6 +687,7 @@ class PasswordWidget extends StatefulWidget {
this.hintText, this.hintText,
this.errorText, this.errorText,
this.title, this.title,
this.maxLength,
}) : super(key: key); }) : super(key: key);
final TextEditingController controller; final TextEditingController controller;
@@ -688,6 +696,7 @@ class PasswordWidget extends StatefulWidget {
final String? hintText; final String? hintText;
final String? errorText; final String? errorText;
final String? title; final String? title;
final int? maxLength;
@override @override
State<PasswordWidget> createState() => _PasswordWidgetState(); State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -750,6 +759,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
obscureText: !_passwordVisible, obscureText: !_passwordVisible,
errorText: widget.errorText, errorText: widget.errorText,
focusNode: _focusNode, focusNode: _focusNode,
maxLength: widget.maxLength,
); );
} }
} }
@@ -1123,7 +1133,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value, errorText: errPwd.isEmpty ? null : errPwd.value,
), ),
], ],
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10), ).marginOnly(top: 10),
], ],
), ),
@@ -1494,7 +1504,7 @@ showAuditDialog(FFI ffi) async {
maxLength: 256, maxLength: 256,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
)), ).workaroundFreezeLinuxMint()),
actions: [ actions: [
dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit) dialogButton('OK', onPressed: submit)
@@ -1741,7 +1751,7 @@ void renameDialog(
autofocus: true, autofocus: true,
decoration: InputDecoration(labelText: translate('Name')), decoration: InputDecoration(labelText: translate('Name')),
validator: validator, validator: validator,
), ).workaroundFreezeLinuxMint(),
), ),
), ),
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
@@ -1801,7 +1811,7 @@ void changeBot({Function()? callback}) async {
decoration: InputDecoration( decoration: InputDecoration(
hintText: translate('Token'), hintText: translate('Token'),
), ),
); ).workaroundFreezeLinuxMint();
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("Telegram bot")), title: Text(translate("Telegram bot")),
@@ -2171,7 +2181,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
}, },
), ),
), ),
), ).workaroundFreezeLinuxMint(),
if (!gFFI.abModel.current.isPersonal()) if (!gFFI.abModel.current.isPersonal())
Row(children: [ Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
@@ -2244,6 +2254,7 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
final confirmController = TextEditingController(text: oldPin); final confirmController = TextEditingController(text: oldPin);
String? pinErrorText; String? pinErrorText;
String? confirmationErrorText; String? confirmationErrorText;
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() async { submit() async {
pinErrorText = null; pinErrorText = null;
@@ -2277,12 +2288,14 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
controller: pinController, controller: pinController,
obscureText: true, obscureText: true,
errorText: pinErrorText, errorText: pinErrorText,
maxLength: maxLength,
), ),
DialogTextField( DialogTextField(
title: translate('Confirmation'), title: translate('Confirmation'),
controller: confirmController, controller: confirmController,
obscureText: true, obscureText: true,
errorText: confirmationErrorText, errorText: confirmationErrorText,
maxLength: maxLength,
) )
], ],
).marginOnly(bottom: 12), ).marginOnly(bottom: 12),

View File

@@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
// end // end
switch (_currentState) { switch (_currentState) {
case GestureState.oneFingerPan: case GestureState.oneFingerPan:
debugPrint("TwoFingerState.pan onEnd"); debugPrint("OneFingerState.pan onEnd");
if (onOneFingerPanEnd != null) { if (onOneFingerPanEnd != null) {
onOneFingerPanEnd!(_getDragEndDetails(d)); onOneFingerPanEnd!(_getDragEndDetails(d));
} }

View File

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

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@@ -45,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
retry: null, retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''), close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildDesktop() ? _buildPortrait()
: _buildMobile()) : _buildLandscape())),
], ],
); );
}); });
} }
Widget _buildDesktop() { Widget _buildLandscape() {
return Row( return Row(
children: [ children: [
Container( Container(
@@ -82,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.groupModel.peers)), )),
) )
], ],
); );
} }
Widget _buildMobile() { Widget _buildPortrait() {
return Column( return Column(
children: [ children: [
Container( Container(
@@ -114,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.groupModel.peers)), )),
) )
], ],
); );
@@ -144,7 +145,7 @@ class _MyGroupState extends State<MyGroup> {
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
), ),
)), ).workaroundFreezeLinuxMint()),
], ],
); );
} }
@@ -159,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
} }
return true; return true;
}).toList(); }).toList();
final listView = ListView.builder( listView(bool isPortrait) => ListView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? listView ? listView(false)
: LimitedBox(maxHeight: maxHeight, child: listView); : LimitedBox(maxHeight: maxHeight, child: listView(true)));
}); });
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (isDesktop || isWebDesktop) { return Obx(() =>
return _buildDesktop(); stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
} else {
return _buildMobile();
}
} }
Widget _buildMobile() { Widget gestureDetector({required Widget child}) {
final peer = super.widget.peer;
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer;
return GestureDetector(
onDoubleTap: peerTabModel.multiSelectionMode
? null
: () => widget.connect(context, peer.id),
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (isMobile) {
widget.connect(context, peer.id);
} else {
peerTabModel.select(peer);
}
}
},
onLongPress: () => peerTabModel.select(peer),
child: child);
}
Widget _buildPortrait() {
final peer = super.widget.peer;
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: gestureDetector(
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container( child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: _buildPeerTile(context, peer, null)), child: _buildPeerTile(context, peer, null)),
)); ));
} }
Widget _buildDesktop() { Widget _buildLandscape() {
final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer; final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>( var deco = Rx<BoxDecoration?>(
BoxDecoration( BoxDecoration(
@@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
}, },
child: GestureDetector( child: gestureDetector(
onDoubleTap:
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
? null
: () => widget.connect(context, peer.id),
onTap: () => peerTabModel.select(peer),
onLongPress: () => peerTabModel.select(peer),
child: Obx(() => peerCardUiType.value == PeerUiType.grid child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco) ? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))), : _buildPeerTile(context, peer, deco))),
); );
} }
Widget _buildPeerTile( makeChild(bool isPortrait, Peer peer) {
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true final name = hideUsernameOnCard == true
? peer.hostname ? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Row( return Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f), color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile borderRadius: isPortrait
? BorderRadius.circular(_tileRadius) ? BorderRadius.circular(_tileRadius)
: BorderRadius.only( : BorderRadius.only(
topLeft: Radius.circular(_tileRadius), topLeft: Radius.circular(_tileRadius),
@@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard>
), ),
), ),
alignment: Alignment.center, alignment: Alignment.center,
width: isMobile ? 50 : 42, width: isPortrait ? 50 : 42,
height: isMobile ? 50 : null, height: isPortrait ? 50 : null,
child: Stack( child: Stack(
children: [ children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30) getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6), .paddingAll(6),
if (_shouldBuildPasswordIcon(peer)) if (_shouldBuildPasswordIcon(peer))
Positioned( Positioned(
@@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column( child: Column(
children: [ children: [
Row(children: [ Row(children: [
getOnline(isMobile ? 4 : 8, peer.online), getOnline(isPortrait ? 4 : 8, peer.online),
Expanded( Expanded(
child: Text( child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias, peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).marginOnly(top: isMobile ? 0 : 2), ]).marginOnly(top: isPortrait ? 0 : 2),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
name, name,
style: isMobile ? null : greyStyle, style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start, textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard>
], ],
).marginOnly(top: 2), ).marginOnly(top: 2),
), ),
isMobile isPortrait
? checkBoxOrActionMoreMobile(peer) ? checkBoxOrActionMorePortrait(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true), : checkBoxOrActionMoreLandscape(peer, isTile: true),
], ],
).paddingOnly(left: 10.0, top: 3.0), ).paddingOnly(left: 10.0, top: 3.0),
), ),
) )
], ],
); );
}
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final colors = _frontN(peer.tags, 25) final colors = _frontN(peer.tags, 25)
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: peer.tags.isNotEmpty : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '', : '',
child: Stack(children: [ child: Stack(children: [
deco == null Obx(
? child () => deco == null
: Obx( ? makeChild(stateGlobal.isPortrait.isTrue, peer)
() => Container( : Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
child: child, child: makeChild(stateGlobal.isPortrait.isTrue, peer),
), ),
), ),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Obx(() => Positioned(
top: 2, top: 2,
right: isMobile ? 20 : 10, right: stateGlobal.isPortrait.isTrue ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
) ))
]), ]),
); );
} }
@@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard>
color: Colors.transparent, color: Colors.transparent,
elevation: 0, elevation: 0,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
// to-do: memory leak here, more investigation needed.
// Continious rebuilds of `Obx()` will cause memory leak here.
// The simple demo does not have this issue.
child: Obx( child: Obx(
() => Container( () => Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
@@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).paddingSymmetric(vertical: 8)), ]).paddingSymmetric(vertical: 8)),
checkBoxOrActionMoreDesktop(peer, isTile: false), checkBoxOrActionMoreLandscape(peer, isTile: false),
], ],
).paddingSymmetric(horizontal: 12.0), ).paddingSymmetric(horizontal: 12.0),
) )
@@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreMobile(Peer peer) { Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -390,7 +393,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) { Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
} }
void _rdpDialog(String id) async { void _rdpDialog(String id) async {
final maxLength = bind.mainMaxEncryptLen();
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port); final portController = TextEditingController(text: port);
@@ -1253,58 +1257,58 @@ void _rdpDialog(String id) async {
hintText: '3389'), hintText: '3389'),
controller: portController, controller: portController,
autofocus: true, autofocus: true,
), ).workaroundFreezeLinuxMint(),
), ),
], ],
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: isDesktop ? 8 : 0),
Row( Obx(() => Row(
children: [ children: [
(isDesktop || isWebDesktop) stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
"${translate('Username')}:", "${translate('Username')}:",
textAlign: TextAlign.right, textAlign: TextAlign.right,
).marginOnly(right: 10)) ).marginOnly(right: 10))
: SizedBox.shrink(), : SizedBox.shrink(),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop)
? null
: translate('Username')),
controller: userController,
),
),
],
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
Row(
children: [
(isDesktop || isWebDesktop)
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Password')}:",
textAlign: TextAlign.right,
).marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop) labelText:
? null isDesktop ? null : translate('Username')),
: translate('Password'), controller: userController,
suffixIcon: IconButton( ).workaroundFreezeLinuxMint(),
onPressed: () => secure.value = !secure.value, ),
icon: Icon(secure.value ],
? Icons.visibility_off ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
: Icons.visibility))), Obx(() => Row(
controller: passwordController, children: [
)), stateGlobal.isPortrait.isFalse
), ? ConstrainedBox(
], constraints: const BoxConstraints(minWidth: 140),
) child: Text(
"${translate('Password')}:",
textAlign: TextAlign.right,
).marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
maxLength: maxLength,
decoration: InputDecoration(
labelText:
isDesktop ? null : translate('Password'),
suffixIcon: IconButton(
onPressed: () =>
secure.value = !secure.value,
icon: Icon(secure.value
? Icons.visibility_off
: Icons.visibility))),
controller: passwordController,
).workaroundFreezeLinuxMint()),
),
],
))
], ],
), ),
), ),

View File

@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -107,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget selectionWrap(Widget widget) { Widget selectionWrap(Widget widget) {
return model.multiSelectionMode ? createMultiSelectionBar() : widget; return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
} }
return Column( return Column(
textBaseline: TextBaseline.ideographic, textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Obx(() => SizedBox(
height: 32, height: 32,
child: Container( child: Container(
padding: (isDesktop || isWebDesktop) padding: stateGlobal.isPortrait.isTrue
? null ? EdgeInsets.symmetric(horizontal: 2)
: EdgeInsets.symmetric(horizontal: 2), : null,
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: child: visibleContextMenuListener(
visibleContextMenuListener(_createSwitchBar(context))), _createSwitchBar(context))),
if (isMobile) if (stateGlobal.isPortrait.isTrue)
..._mobileRightActions(context) ..._portraitRightActions(context)
else else
..._desktopRightActions(context) ..._landscapeRightActions(context)
], ],
)), )),
), ),
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(), _createPeersView(),
], ],
); );
@@ -299,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget visibleContextMenuListener(Widget child) { Widget visibleContextMenuListener(Widget child) {
if (isMobile) { if (!(isDesktop || isWebDesktop)) {
return GestureDetector( return GestureDetector(
onLongPressDown: (e) { onLongPressDown: (e) {
final x = e.globalPosition.dx; final x = e.globalPosition.dx;
@@ -361,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
.toList()); .toList());
} }
Widget createMultiSelectionBar() { Widget createMultiSelectionBar(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -380,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Row( Row(
children: [ children: [
selectionCount(model.selectedPeers.length), selectionCount(model.selectedPeers.length),
selectAll(), selectAll(model),
closeSelection(), closeSelection(),
], ],
) )
@@ -456,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -477,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -500,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
}, },
child: Icon(Icons.tag)) child: Icon(Icons.tag))
.marginOnly(left: isMobile ? 11 : 6), .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -511,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
); );
} }
Widget selectAll() { Widget selectAll(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Offstage( return Offstage(
offstage: offstage:
model.selectedPeers.length >= model.currentTabCachedPeers.length, model.selectedPeers.length >= model.currentTabCachedPeers.length,
@@ -556,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
} }
List<Widget> _desktopRightActions(BuildContext context) { List<Widget> _landscapeRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return [ return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), const PeerSearchBar().marginOnly(right: 13),
_createRefresh( _createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh( _createRefresh(
@@ -580,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
]; ];
} }
List<Widget> _mobileRightActions(BuildContext context) { List<Widget> _portraitRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
@@ -701,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
baseOffset: 0, baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length); extentOffset: peerSearchTextController.value.text.length);
}); });
return Container( return Obx(() => Container(
width: isMobile ? 120 : 140, width: stateGlobal.isPortrait.isTrue ? 120 : 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Obx(() => Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
@@ -744,7 +743,7 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
), ),
), ).workaroundFreezeLinuxMint(),
), ),
// Icon(Icons.close), // Icon(Icons.close),
IconButton( IconButton(
@@ -768,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
), ),
) )
], ],
)), ),
); ));
} }
} }

View File

@@ -5,7 +5,9 @@ import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
@@ -41,6 +43,14 @@ class LoadEvent {
static const String group = 'load_group_peers'; static const String group = 'load_group_peers';
} }
class PeersModelName {
static const String recent = 'recent peer';
static const String favorite = 'fav peer';
static const String lan = 'discovered peer';
static const String addressBook = 'address book peer';
static const String group = 'group peer';
}
/// for peer search text, global obs value /// for peer search text, global obs value
final peerSearchText = "".obs; final peerSearchText = "".obs;
@@ -88,6 +98,7 @@ class _PeersViewState extends State<_PeersView>
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now(); var _lastQueryTime = DateTime.now();
var _lastWindowRestoreTime = DateTime.now();
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
bool _isActive = true; bool _isActive = true;
@@ -116,11 +127,38 @@ class _PeersViewState extends State<_PeersView>
@override @override
void onWindowFocus() { void onWindowFocus() {
_queryCount = 0; _queryCount = 0;
_isActive = true;
}
@override
void onWindowBlur() {
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
// Maybe it's a bug of the window manager, but the source code seems to be correct.
//
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
// we need the following comparison to ensure that `_isActive` is true in the end.
if (isWindows &&
DateTime.now().difference(_lastWindowRestoreTime) <
const Duration(milliseconds: 300)) {
return;
}
_queryCount = _maxQueryCount;
_isActive = false;
}
@override
void onWindowRestore() {
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
if (!isWindows) return;
_queryCount = 0;
_isActive = true;
_lastWindowRestoreTime = DateTime.now();
} }
@override @override
void onWindowMinimize() { void onWindowMinimize() {
_queryCount = _maxQueryCount; // Window minimize also triggers `onWindowBlur()`.
} }
// This function is required for mobile. // This function is required for mobile.
@@ -128,7 +166,7 @@ class _PeersViewState extends State<_PeersView>
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
if (isDesktop) return; if (isDesktop || isWebDesktop) return;
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
_isActive = true; _isActive = true;
_queryCount = 0; _queryCount = 0;
@@ -139,8 +177,11 @@ class _PeersViewState extends State<_PeersView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<Peers>( // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
create: (context) => widget.peers, // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
// Simple demo can reproduce this issue.
return ChangeNotifierProvider<Peers>.value(
value: widget.peers,
child: Consumer<Peers>(builder: (context, peers, child) { child: Consumer<Peers>(builder: (context, peers, child) {
if (peers.peers.isEmpty) { if (peers.peers.isEmpty) {
gFFI.peerTabModel.setCurrentTabCachedPeers([]); gFFI.peerTabModel.setCurrentTabCachedPeers([]);
@@ -194,7 +235,7 @@ class _PeersViewState extends State<_PeersView>
var peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000); if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
buildOnePeer(Peer peer) { buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
@@ -206,7 +247,7 @@ class _PeersViewState extends State<_PeersView>
// No need to listen the currentTab change event. // No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event, // Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView(). // and the peers change event will trigger _buildPeersView().
return (isDesktop || isWebDesktop) return !isPortrait
? Obx(() => peerCardUiType.value == PeerUiType.list ? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild) ? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid : peerCardUiType.value == PeerUiType.grid
@@ -217,44 +258,36 @@ class _PeersViewState extends State<_PeersView>
: Container(child: visibilityChild); : Container(child: visibilityChild);
} }
final Widget child; // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
if (isMobile) { // Continious rebuilds of `ListView.builder` will cause memory leak.
child = ListView.builder( // Simple demo can reproduce this issue.
itemCount: peers.length, final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
itemBuilder: (BuildContext context, int index) { ? ListView.builder(
return buildOnePeer(peers[index]).marginOnly( itemCount: peers.length,
top: index == 0 ? 0 : space / 2, bottom: space / 2); itemBuilder: (BuildContext context, int index) {
}, return buildOnePeer(peers[index], true).marginOnly(
); top: index == 0 ? 0 : space / 2, bottom: space / 2);
} else { },
child = Obx(() => peerCardUiType.value == PeerUiType.list )
? DesktopScrollWrapper( : peerCardUiType.value == PeerUiType.list
scrollController: _scrollController, ? ListView.builder(
child: ListView.builder( controller: _scrollController,
controller: _scrollController, itemCount: peers.length,
physics: DraggableNeverScrollableScrollPhysics(), itemBuilder: (BuildContext context, int index) {
itemCount: peers.length, return buildOnePeer(peers[index], false).marginOnly(
itemBuilder: (BuildContext context, int index) { right: space,
return buildOnePeer(peers[index]).marginOnly( top: index == 0 ? 0 : space / 2,
right: space, bottom: space / 2);
top: index == 0 ? 0 : space / 2, },
bottom: space / 2); )
}), : DynamicGridView.builder(
) gridDelegate: SliverGridDelegateWithWrapping(
: DesktopScrollWrapper( mainAxisSpacing: space / 2,
scrollController: _scrollController, crossAxisSpacing: space),
child: DynamicGridView.builder( itemCount: peers.length,
controller: _scrollController, itemBuilder: (BuildContext context, int index) {
physics: DraggableNeverScrollableScrollPhysics(), return buildOnePeer(peers[index], false);
gridDelegate: SliverGridDelegateWithWrapping( }));
mainAxisSpacing: space / 2,
crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
));
}
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();
@@ -290,7 +323,12 @@ class _PeersViewState extends State<_PeersView>
_queryOnlines(false); _queryOnlines(false);
} }
} else { } else {
if (_isActive && (_queryCount < _maxQueryCount || !p)) { final skipIfIsWeb =
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
final skipIfMobile =
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
if (now.difference(_lastQueryTime) >= _queryInterval) { if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
@@ -371,28 +409,39 @@ class _PeersViewState extends State<_PeersView>
} }
abstract class BasePeersView extends StatelessWidget { abstract class BasePeersView extends StatelessWidget {
final String name; final PeerTabIndex peerTabIndex;
final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final GetInitPeers? getInitPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
required this.name, required this.peerTabIndex,
required this.loadEvent,
this.peerFilter, this.peerFilter,
required this.peerCardBuilder, required this.peerCardBuilder,
required this.getInitPeers,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Peers peers;
switch (peerTabIndex) {
case PeerTabIndex.recent:
peers = gFFI.recentPeersModel;
break;
case PeerTabIndex.fav:
peers = gFFI.favoritePeersModel;
break;
case PeerTabIndex.lan:
peers = gFFI.lanPeersModel;
break;
case PeerTabIndex.ab:
peers = gFFI.abModel.peersModel;
break;
case PeerTabIndex.group:
peers = gFFI.groupModel.peersModel;
break;
}
return _PeersView( return _PeersView(
peers: peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder);
} }
} }
@@ -401,13 +450,11 @@ class RecentPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'recent peer', peerTabIndex: PeerTabIndex.recent,
loadEvent: LoadEvent.recent,
peerCardBuilder: (Peer peer) => RecentPeerCard( peerCardBuilder: (Peer peer) => RecentPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -423,13 +470,11 @@ class FavoritePeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'favorite peer', peerTabIndex: PeerTabIndex.fav,
loadEvent: LoadEvent.favorite,
peerCardBuilder: (Peer peer) => FavoritePeerCard( peerCardBuilder: (Peer peer) => FavoritePeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -445,13 +490,11 @@ class DiscoveredPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'discovered peer', peerTabIndex: PeerTabIndex.lan,
loadEvent: LoadEvent.lan,
peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -464,36 +507,38 @@ class DiscoveredPeersView extends BasePeersView {
class AddressBookPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView {
AddressBookPeersView( AddressBookPeersView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required GetInitPeers getInitPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', peerTabIndex: PeerTabIndex.ab,
loadEvent: LoadEvent.addressBook,
peerFilter: (Peer peer) => peerFilter: (Peer peer) =>
_hitTag(gFFI.abModel.selectedTags, peer.tags), _hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardBuilder: (Peer peer) => AddressBookPeerCard( peerCardBuilder: (Peer peer) => AddressBookPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: getInitPeers,
); );
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) { static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
if (selectedTags.isEmpty) { if (selectedTags.isEmpty) {
return true; return true;
} }
// The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations.
final selectedNormalTags =
selectedTags.where((tag) => tag != kUntagged).toList();
if (selectedTags.contains(kUntagged)) {
if (idents.isEmpty) return true;
if (selectedNormalTags.isEmpty) return false;
}
if (gFFI.abModel.filterByIntersection.value) { if (gFFI.abModel.filterByIntersection.value) {
for (final tag in selectedTags) { for (final tag in selectedNormalTags) {
if (!idents.contains(tag)) { if (!idents.contains(tag)) {
return false; return false;
} }
} }
return true; return true;
} else { } else {
for (final tag in selectedTags) { for (final tag in selectedNormalTags) {
if (idents.contains(tag)) { if (idents.contains(tag)) {
return true; return true;
} }
@@ -505,20 +550,15 @@ class AddressBookPeersView extends BasePeersView {
class MyGroupPeerView extends BasePeersView { class MyGroupPeerView extends BasePeersView {
MyGroupPeerView( MyGroupPeerView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required GetInitPeers getInitPeers})
: super( : super(
key: key, key: key,
name: 'group peer', peerTabIndex: PeerTabIndex.group,
loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: getInitPeers,
); );
static bool filter(Peer peer) { static bool filter(Peer peer) {

View File

@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// https://github.com/flutter/flutter/issues/154053
final useRawKeyEvents = isLinux && !isWeb;
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
// while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope( return FocusScope(
autofocus: true, autofocus: true,
child: Focus( child: Focus(
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true, canRequestFocus: true,
focusNode: focusNode, focusNode: focusNode,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
onKey: (FocusNode data, RawKeyEvent e) => onKey: useRawKeyEvents
inputModel.handleRawKeyEvent(e), ? (FocusNode data, RawKeyEvent event) =>
inputModel.handleRawKeyEvent(event)
: null,
onKeyEvent: useRawKeyEvents
? null
: (FocusNode node, KeyEvent event) =>
inputModel.handleKeyEvent(event),
child: child)); child: child));
} }
} }
@@ -74,8 +84,17 @@ class _RawTouchGestureDetectorRegionState
double _mouseScrollIntegral = 0; // mouse scroll speed controller double _mouseScrollIntegral = 0; // mouse scroll speed controller
double _scale = 1; double _scale = 1;
// Workaround tap down event when two fingers are used to scale(mobile)
TapDownDetails? _lastTapDownDetails;
PointerDeviceKind? lastDeviceKind; PointerDeviceKind? lastDeviceKind;
// For touch mode, onDoubleTap
// `onDoubleTap()` does not provide the position of the tap event.
Offset _lastPosOfDoubleTapDown = Offset.zero;
bool _touchModePanStarted = false;
Offset _doubleFinerTapPosition = Offset.zero;
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel; FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel; InputModel get inputModel => widget.inputModel;
@@ -90,152 +109,190 @@ class _RawTouchGestureDetectorRegionState
); );
} }
onTapDown(TapDownDetails d) { onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
_lastPosOfDoubleTapDown = d.localPosition;
// Desktop or mobile "Touch mode" // Desktop or mobile "Touch mode"
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { _lastTapDownDetails = d;
inputModel.tapDown(MouseButtons.left);
}
} }
} }
onTapUp(TapUpDetails d) { onTapUp(TapUpDetails d) async {
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { final isMoved =
inputModel.tapUp(MouseButtons.left); await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
if (isMoved) {
if (lastTapDownDetails != null) {
await inputModel.tapDown(MouseButtons.left);
}
await inputModel.tapUp(MouseButtons.left);
} }
} }
} }
onTap() { onTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
// Mobile, "Mouse mode" // Mobile, "Mouse mode"
inputModel.tap(MouseButtons.left); await inputModel.tap(MouseButtons.left);
} }
} }
onDoubleTapDown(TapDownDetails d) { onDoubleTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); _lastPosOfDoubleTapDown = d.localPosition;
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} }
} }
onDoubleTap() { onDoubleTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
return; return;
} }
inputModel.tap(MouseButtons.left); if (handleTouch &&
inputModel.tap(MouseButtons.left); !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
return;
}
await inputModel.tap(MouseButtons.left);
await inputModel.tap(MouseButtons.left);
} }
onLongPressDown(LongPressDownDetails d) { onLongPressDown(LongPressDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); _lastPosOfDoubleTapDown = d.localPosition;
_cacheLongPressPosition = d.localPosition; _cacheLongPressPosition = d.localPosition;
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
return;
}
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
} }
} }
onLongPressUp() { onLongPressUp() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
inputModel.tapUp(MouseButtons.left); await inputModel.tapUp(MouseButtons.left);
} }
} }
// for mobiles // for mobiles
onLongPress() { onLongPress() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
ffi.cursorModel final isMoved = await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
if (!isMoved) {
return;
}
}
if (!ffi.ffiModel.isPeerMobile) {
await inputModel.tap(MouseButtons.right);
} }
inputModel.tap(MouseButtons.right);
} }
onDoubleFinerTapDown(TapDownDetails d) { onDoubleFinerTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
_doubleFinerTapPosition = d.localPosition;
// ignore for desktop and mobile // ignore for desktop and mobile
} }
onDoubleFinerTap(TapDownDetails d) { onDoubleFinerTap(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
inputModel.tap(MouseButtons.right); // mobile mouse mode or desktop touch screen
final isMobileMouseMode = isMobile && !ffiModel.touchMode;
// We can't use `d.localPosition` here because it's always (0, 0) on desktop.
final isDesktopInRemoteRect = (isDesktop || isWebDesktop) &&
ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition);
if (isMobileMouseMode || isDesktopInRemoteRect) {
await inputModel.tap(MouseButtons.right);
} }
} }
onHoldDragStart(DragStartDetails d) { onHoldDragStart(DragStartDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
inputModel.sendMouse('down', MouseButtons.left); await inputModel.sendMouse('down', MouseButtons.left);
} }
} }
onHoldDragUpdate(DragUpdateDetails d) { onHoldDragUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
} }
onHoldDragEnd(DragEndDetails d) { onHoldDragEnd(DragEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
inputModel.sendMouse('up', MouseButtons.left); await inputModel.sendMouse('up', MouseButtons.left);
} }
} }
onOneFingerPanStart(BuildContext context, DragStartDetails d) { onOneFingerPanStart(BuildContext context, DragStartDetails d) async {
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null;
lastDeviceKind = d.kind ?? lastDeviceKind; lastDeviceKind = d.kind ?? lastDeviceKind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
if (lastTapDownDetails != null) {
await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx,
lastTapDownDetails.localPosition.dy);
}
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return; return;
} }
if (isDesktop) { if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
return;
}
_touchModePanStarted = true;
if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords(); ffi.cursorModel.trySetRemoteWindowCoords();
} }
// Workaround for the issue that the first pan event is sent a long time after the start event. // Workaround for the issue that the first pan event is sent a long time after the start event.
// If the time interval between the start event and the first pan event is less than 500ms, // If the time interval between the start event and the first pan event is less than 500ms,
// we consider to use the long press position as the start position. // we consider to use the long press position as the start position.
@@ -243,11 +300,11 @@ class _RawTouchGestureDetectorRegionState
// TODO: We should find a better way to send the first pan event as soon as possible. // TODO: We should find a better way to send the first pan event as soon as possible.
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs < if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
500) { 500) {
ffi.cursorModel await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
} }
inputModel.sendMouse('down', MouseButtons.left); await inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else { } else {
final offset = ffi.cursorModel.offset; final offset = ffi.cursorModel.offset;
final cursorX = offset.dx; final cursorX = offset.dx;
@@ -256,39 +313,46 @@ class _RawTouchGestureDetectorRegionState
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
final size = MediaQueryData.fromView(View.of(context)).size; final size = MediaQueryData.fromView(View.of(context)).size;
if (!visible.contains(Offset(cursorX, cursorY))) { if (!visible.contains(Offset(cursorX, cursorY))) {
ffi.cursorModel.move(size.width / 2, size.height / 2); await ffi.cursorModel.move(size.width / 2, size.height / 2);
} }
} }
} }
onOneFingerPanUpdate(DragUpdateDetails d) { onOneFingerPanUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return; return;
} }
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); if (handleTouch && !_touchModePanStarted) {
return;
}
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
onOneFingerPanEnd(DragEndDetails d) { onOneFingerPanEnd(DragEndDetails d) async {
_touchModePanStarted = false;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords(); ffi.cursorModel.clearRemoteWindowCoords();
} }
inputModel.sendMouse('up', MouseButtons.left); if (handleTouch) {
await inputModel.sendMouse('up', MouseButtons.left);
}
} }
// scale + pan event // scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) { onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
} }
onTwoFingerScaleUpdate(ScaleUpdateDetails d) { onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
@@ -297,7 +361,7 @@ class _RawTouchGestureDetectorRegionState
_scale = d.scale; _scale = d.scale;
if (scale != 0) { if (scale != 0) {
bind.sessionSendPointer( await bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode( msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', scale) PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
@@ -312,21 +376,22 @@ class _RawTouchGestureDetectorRegionState
} }
} }
onTwoFingerScaleEnd(ScaleEndDetails d) { onTwoFingerScaleEnd(ScaleEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if ((isDesktop || isWebDesktop)) { if ((isDesktop || isWebDesktop)) {
bind.sessionSendPointer( await bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode( msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson())); PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
} else { } else {
// mobile // mobile
_scale = 1; _scale = 1;
bind.sessionSetViewStyle(sessionId: sessionId, value: ""); // No idea why we need to set the view style to "" here.
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
} }
inputModel.sendMouse('up', MouseButtons.left); await inputModel.sendMouse('up', MouseButtons.left);
} }
get onHoldDragCancel => null; get onHoldDragCancel => null;

View File

@@ -147,12 +147,23 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
child: Text(translate('Reset canvas')), child: Text(translate('Reset canvas')),
onPressed: () => ffi.cursorModel.reset())); onPressed: () => ffi.cursorModel.reset()));
} }
connectWithToken(
{required bool isFileTransfer, required bool isTcpTunneling}) {
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
connect(context, id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
connToken: connToken);
}
// transferFile // transferFile
if (isDesktop) { if (isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Transfer file')), child: Text(translate('Transfer file')),
onPressed: () => connect(context, id, isFileTransfer: true)), onPressed: () =>
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
); );
} }
// tcpTunneling // tcpTunneling
@@ -160,7 +171,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('TCP tunneling')), child: Text(translate('TCP tunneling')),
onPressed: () => connect(context, id, isTcpTunneling: true)), onPressed: () =>
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
); );
} }
// note // note
@@ -183,7 +195,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text('${translate("Insert")} Ctrl + Alt + Del'), child: Text('${translate("Insert Ctrl + Alt + Del")}'),
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
); );
} }

View File

@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android"; const String kPeerPlatformAndroid = "Android";
const String kPeerPlatformWebDesktop = "WebDesktop";
const double kScrollbarThickness = 12.0; const double kScrollbarThickness = 12.0;
@@ -88,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout"; const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
const String kOptionEnableHwcodec = "enable-hwcodec"; const String kOptionEnableHwcodec = "enable-hwcodec";
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming"; const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode"; const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard"; const String kOptionEnableKeyboard = "enable-keyboard";
@@ -167,6 +169,13 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch"; const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse"; const String kPointerEventKindMouse = "mouse";
const String kMouseEventTypeDefault = "";
const String kMouseEventTypePanStart = "pan_start";
const String kMouseEventTypePanUpdate = "pan_update";
const String kMouseEventTypePanEnd = "pan_end";
const String kMouseEventTypeDown = "down";
const String kMouseEventTypeUp = "up";
const String kKeyFlutterKey = "flutter_key"; const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows = const String kKeyShowDisplaysAsIndividualWindows =
@@ -235,10 +244,6 @@ const double kDesktopIconButtonSplashRadius = 20;
/// [kMinCursorSize] indicates min cursor (w, h) /// [kMinCursorSize] indicates min cursor (w, h)
const int kMinCursorSize = 12; const int kMinCursorSize = 12;
/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse
const kDefaultScrollAmountMultiplier = 5.0;
const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0; const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0;
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
@@ -569,3 +574,5 @@ enum WindowsTarget {
extension WindowsTargetExt on int { extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this); WindowsTarget get windowsVersion => getWindowsTarget(this);
} }
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';

View File

@@ -3,8 +3,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; 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/consts.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -39,7 +39,7 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
double? get height => bind.isIncomingOnly() ? null : em * 3; double? get height => bind.isIncomingOnly() ? null : em * 3;
void onUsePublicServerGuide() { void onUsePublicServerGuide() {
const url = "https://rustdesk.com/pricing.html"; const url = "https://rustdesk.com/pricing";
canLaunchUrlString(url).then((can) { canLaunchUrlString(url).then((can) {
if (can) { if (can) {
launchUrlString(url); launchUrlString(url);
@@ -179,6 +179,9 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
stateGlobal.svcStatus.value = SvcStatus.notReady; stateGlobal.svcStatus.value = SvcStatus.notReady;
} }
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); _svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
try {
stateGlobal.videoConnCount.value = status['video_conn_count'] as int;
} catch (_) {}
} }
} }
@@ -203,6 +206,8 @@ class _ConnectionPageState extends State<ConnectionPage>
bool isPeersLoading = false; bool isPeersLoading = false;
bool isPeersLoaded = false; bool isPeersLoaded = false;
// https://github.com/flutter/flutter/issues/157244
Iterable<Peer> _autocompleteOpts = [];
@override @override
void initState() { void initState() {
@@ -323,43 +328,14 @@ class _ConnectionPageState extends State<ConnectionPage>
child: Ink( child: Ink(
child: Column( child: Column(
children: [ children: [
Row( getConnectionPageTitle(context, false).marginOnly(bottom: 15),
children: [
Expanded(
child: Row(
children: [
AutoSizeText(
translate('Control Remote Desktop'),
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
],
).marginOnly(bottom: 15),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Autocomplete<Peer>( child: Autocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) { optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') { if (textEditingValue.text == '') {
return const Iterable<Peer>.empty(); _autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) { } else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer( Peer emptyPeer = Peer(
id: '', id: '',
@@ -375,7 +351,7 @@ class _ConnectionPageState extends State<ConnectionPage>
rdpUsername: '', rdpUsername: '',
loginName: '', loginName: '',
); );
return [emptyPeer]; _autocompleteOpts = [emptyPeer];
} else { } else {
String textWithoutSpaces = String textWithoutSpaces =
textEditingValue.text.replaceAll(" ", ""); textEditingValue.text.replaceAll(" ", "");
@@ -386,8 +362,7 @@ class _ConnectionPageState extends State<ConnectionPage>
); );
} }
String textToFind = textEditingValue.text.toLowerCase(); String textToFind = textEditingValue.text.toLowerCase();
_autocompleteOpts = peers
return peers
.where((peer) => .where((peer) =>
peer.id.toLowerCase().contains(textToFind) || peer.id.toLowerCase().contains(textToFind) ||
peer.username peer.username
@@ -399,6 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
peer.alias.toLowerCase().contains(textToFind)) peer.alias.toLowerCase().contains(textToFind))
.toList(); .toList();
} }
return _autocompleteOpts;
}, },
fieldViewBuilder: ( fieldViewBuilder: (
BuildContext context, BuildContext context,
@@ -448,7 +424,7 @@ class _ConnectionPageState extends State<ConnectionPage>
onSubmitted: (_) { onSubmitted: (_) {
onConnect(); onConnect();
}, },
)); ).workaroundFreezeLinuxMint());
}, },
onSelected: (option) { onSelected: (option) {
setState(() { setState(() {
@@ -459,6 +435,7 @@ class _ConnectionPageState extends State<ConnectionPage>
optionsViewBuilder: (BuildContext context, optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Peer> onSelected, AutocompleteOnSelected<Peer> onSelected,
Iterable<Peer> options) { Iterable<Peer> options) {
options = _autocompleteOpts;
double maxHeight = options.length * 50; double maxHeight = options.length * 50;
if (options.length == 1) { if (options.length == 1) {
maxHeight = 52; maxHeight = 52;

View File

@@ -12,9 +12,9 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/ui_manager.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -35,12 +35,11 @@ class DesktopHomePage extends StatefulWidget {
const borderColor = Color(0xFF2F65BA); const borderColor = Color(0xFF2F65BA);
class _DesktopHomePageState extends State<DesktopHomePage> class _DesktopHomePageState extends State<DesktopHomePage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _leftPaneScrollController = ScrollController(); final _leftPaneScrollController = ScrollController();
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
var updateUrl = '';
var systemError = ''; var systemError = '';
StreamSubscription? _uniLinksSubscription; StreamSubscription? _uniLinksSubscription;
var svcStopped = false.obs; var svcStopped = false.obs;
@@ -52,6 +51,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bool isCardClosed = false; bool isCardClosed = false;
final RxBool _editHover = false.obs; final RxBool _editHover = false.obs;
final RxBool _block = false.obs;
final GlobalKey _childKey = GlobalKey(); final GlobalKey _childKey = GlobalKey();
@@ -59,14 +59,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final isIncomingOnly = bind.isIncomingOnly(); final isIncomingOnly = bind.isIncomingOnly();
return Row( return _buildBlock(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildLeftPane(context), buildLeftPane(context),
if (!isIncomingOnly) const VerticalDivider(width: 1), if (!isIncomingOnly) const VerticalDivider(width: 1),
if (!isIncomingOnly) Expanded(child: buildRightPane(context)), 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) { Widget buildLeftPane(BuildContext context) {
@@ -87,7 +93,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
if (!isOutgoingOnly) buildIDBoard(context), if (!isOutgoingOnly) buildIDBoard(context),
if (!isOutgoingOnly) buildPasswordBoard(context), if (!isOutgoingOnly) buildPasswordBoard(context),
FutureBuilder<Widget>( FutureBuilder<Widget>(
future: buildHelpCards(), future: Future.value(
Obx(() => buildHelpCards(stateGlobal.updateUrl.value))),
builder: (_, data) { builder: (_, data) {
if (data.hasData) { if (data.hasData) {
if (isIncomingOnly) { if (isIncomingOnly) {
@@ -125,47 +132,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
child: Container( child: Container(
width: isIncomingOnly ? 280.0 : 200.0, width: isIncomingOnly ? 280.0 : 200.0,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: DesktopScrollWrapper( child: Stack(
scrollController: _leftPaneScrollController, children: [
child: Stack( SingleChildScrollView(
children: [ controller: _leftPaneScrollController,
SingleChildScrollView( child: Column(
controller: _leftPaneScrollController, key: _childKey,
physics: DraggableNeverScrollableScrollPhysics(), children: children,
child: Column(
key: _childKey,
children: children,
),
), ),
if (isOutgoingOnly) ),
Positioned( if (isOutgoingOnly)
bottom: 6, Positioned(
left: 12, bottom: 6,
child: Align( left: 12,
alignment: Alignment.centerLeft, child: Align(
child: InkWell( alignment: Alignment.centerLeft,
child: Obx( child: InkWell(
() => Icon( child: Obx(
Icons.settings, () => Icon(
color: _editHover.value Icons.settings,
? textColor color: _editHover.value
: Colors.grey.withOpacity(0.5), ? textColor
size: 22, : Colors.grey.withOpacity(0.5),
), size: 22,
), ),
onTap: () => {
if (DesktopSettingPage.tabKeys.isNotEmpty)
{
DesktopSettingPage.switch2page(
DesktopSettingPage.tabKeys[0])
}
},
onHover: (value) => _editHover.value = value,
), ),
onTap: () => {
if (DesktopSettingPage.tabKeys.isNotEmpty)
{
DesktopSettingPage.switch2page(
DesktopSettingPage.tabKeys[0])
}
},
onHover: (value) => _editHover.value = value,
), ),
) ),
], )
), ],
), ),
), ),
); );
@@ -234,7 +237,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
style: TextStyle( style: TextStyle(
fontSize: 22, fontSize: 22,
), ),
), ).workaroundFreezeLinuxMint(),
), ),
) )
], ],
@@ -272,10 +275,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
buildPasswordBoard(BuildContext context) { buildPasswordBoard(BuildContext context) {
final model = gFFI.serverModel; return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Consumer<ServerModel>(
builder: (context, model, child) {
return buildPasswordBoard2(context, model);
},
));
}
buildPasswordBoard2(BuildContext context, ServerModel model) {
RxBool refreshHover = false.obs; RxBool refreshHover = false.obs;
RxBool editHover = false.obs; RxBool editHover = false.obs;
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
final showOneTime = model.approveMode != 'click' &&
model.verificationMethod != kUsePermanentPassword;
return Container( return Container(
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
child: Row( child: Row(
@@ -304,8 +318,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onDoubleTap: () { onDoubleTap: () {
if (model.verificationMethod != if (showOneTime) {
kUsePermanentPassword) {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: model.serverPasswd.text)); ClipboardData(text: model.serverPasswd.text));
showToast(translate("Copied")); showToast(translate("Copied"));
@@ -320,25 +333,26 @@ class _DesktopHomePageState extends State<DesktopHomePage>
EdgeInsets.only(top: 14, bottom: 10), EdgeInsets.only(top: 14, bottom: 10),
), ),
style: TextStyle(fontSize: 15), style: TextStyle(fontSize: 15),
), ).workaroundFreezeLinuxMint(),
), ),
), ),
AnimatedRotationWidget( if (showOneTime)
onPressed: () => bind.mainUpdateTemporaryPassword(), AnimatedRotationWidget(
child: Tooltip( onPressed: () => bind.mainUpdateTemporaryPassword(),
message: translate('Refresh Password'), child: Tooltip(
child: Obx(() => RotatedBox( message: translate('Refresh Password'),
quarterTurns: 2, child: Obx(() => RotatedBox(
child: Icon( quarterTurns: 2,
Icons.refresh, child: Icon(
color: refreshHover.value Icons.refresh,
? textColor color: refreshHover.value
: Color(0xFFDDDDDD), ? textColor
size: 22, : Color(0xFFDDDDDD),
))), size: 22,
), ))),
onHover: (value) => refreshHover.value = value, ),
).marginOnly(right: 8, top: 4), onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
if (!bind.isDisableSettings()) if (!bind.isDisableSettings())
InkWell( InkWell(
child: Tooltip( child: Tooltip(
@@ -409,14 +423,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
); );
} }
Future<Widget> buildHelpCards() async { Widget buildHelpCards(String updateUrl) {
if (!bind.isCustomClient() && if (!bind.isCustomClient() &&
updateUrl.isNotEmpty && updateUrl.isNotEmpty &&
!isCardClosed && !isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) { bind.mainUriPrefixSync().contains('rustdesk')) {
return buildInstallCard( return buildInstallCard(
"Status", "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 { "Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download'); final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url); await launchUrl(url);
@@ -663,12 +677,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!bind.isCustomClient()) {
Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
});
}
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID(); await gFFI.serverModel.fetchID();
final error = await bind.mainGetError(); final error = await bind.mainGetError();
@@ -766,6 +774,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
isRDP: call.arguments['isRDP'], isRDP: call.arguments['isRDP'],
password: call.arguments['password'], password: call.arguments['password'],
forceRelay: call.arguments['forceRelay'], forceRelay: call.arguments['forceRelay'],
connToken: call.arguments['connToken'],
); );
} else if (call.method == kWindowEventMoveTabToNewWindow) { } else if (call.method == kWindowEventMoveTabToNewWindow) {
final args = call.arguments.split(','); final args = call.arguments.split(',');
@@ -803,6 +812,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_updateWindowSize(); _updateWindowSize();
}); });
} }
WidgetsBinding.instance.addObserver(this);
} }
_updateWindowSize() { _updateWindowSize() {
@@ -824,9 +834,18 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_uniLinksSubscription?.cancel(); _uniLinksSubscription?.cancel();
Get.delete<RxBool>(tag: 'stop-service'); Get.delete<RxBool>(tag: 'stop-service');
_updateTimer?.cancel(); _updateTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
}
}
Widget buildPluginEntry() { Widget buildPluginEntry() {
final entries = PluginUiManager.instance.entries.entries; final entries = PluginUiManager.instance.entries.entries;
return Offstage( return Offstage(
@@ -857,6 +876,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
// SpecialCharacterValidationRule(), // SpecialCharacterValidationRule(),
MinCharactersValidationRule(8), MinCharactersValidationRule(8),
]; ];
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() { submit() {
@@ -915,7 +935,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg0 = ''; errMsg0 = '';
}); });
}, },
), maxLength: maxLength,
).workaroundFreezeLinuxMint(),
), ),
], ],
), ),
@@ -941,7 +962,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg1 = ''; errMsg1 = '';
}); });
}, },
), maxLength: maxLength,
).workaroundFreezeLinuxMint(),
), ),
], ],
), ),

View File

@@ -11,15 +11,16 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_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/platform_model.dart';
import 'package:flutter_hbb/models/server_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'; import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart'; import '../../common/widgets/login.dart';
@@ -61,7 +62,8 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey; final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [ static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general, SettingsTabKey.general,
if (!bind.isOutgoingOnly() && if (!isWeb &&
!bind.isOutgoingOnly() &&
!bind.isDisableSettings() && !bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety, SettingsTabKey.safety,
@@ -105,13 +107,20 @@ class DesktopSettingPage extends StatefulWidget {
} }
class _DesktopSettingPageState extends State<DesktopSettingPage> class _DesktopSettingPageState extends State<DesktopSettingPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { with
TickerProviderStateMixin,
AutomaticKeepAliveClientMixin,
WidgetsBindingObserver {
late PageController controller; late PageController controller;
late Rx<SettingsTabKey> selectedTab; late Rx<SettingsTabKey> selectedTab;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
final RxBool _block = false.obs;
final RxBool _canBeBlocked = false.obs;
Timer? _videoConnTimer;
_DesktopSettingPageState(SettingsTabKey initialTabkey) { _DesktopSettingPageState(SettingsTabKey initialTabkey) {
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey); var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
if (initialIndex == -1) { if (initialIndex == -1) {
@@ -131,11 +140,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 @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag); Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag); Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
WidgetsBinding.instance.removeObserver(this);
_videoConnTimer?.cancel();
} }
List<_TabInfo> _settingTabs() { List<_TabInfo> _settingTabs() {
@@ -205,18 +237,41 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
return children; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
body: Row( body: _buildBlock(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
width: _kTabWidth, width: _kTabWidth,
child: Column( child: Column(
children: [ children: [
_header(), _header(context),
Flexible(child: _listView(tabs: _settingTabs())), Flexible(child: _listView(tabs: _settingTabs())),
], ],
), ),
@@ -225,13 +280,11 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
Expanded( Expanded(
child: Container( child: Container(
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
child: DesktopScrollWrapper( child: PageView(
scrollController: controller, controller: controller,
child: PageView( physics: NeverScrollableScrollPhysics(),
controller: controller, children: _children(),
physics: NeverScrollableScrollPhysics(), ),
children: _children(),
)),
), ),
) )
], ],
@@ -239,21 +292,40 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
); );
} }
Widget _header() { Widget _header(BuildContext context) {
final settingsText = Text(
translate('Settings'),
textAlign: TextAlign.left,
style: const TextStyle(
color: _accentColor,
fontSize: _kTitleFontSize,
fontWeight: FontWeight.w400,
),
);
return Row( return Row(
children: [ children: [
SizedBox( if (isWeb)
height: 62, IconButton(
child: Text( onPressed: () {
translate('Settings'), if (Navigator.canPop(context)) {
textAlign: TextAlign.left, Navigator.pop(context);
style: const TextStyle( }
color: _accentColor, },
fontSize: _kTitleFontSize, icon: Icon(Icons.arrow_back),
fontWeight: FontWeight.w400, ).marginOnly(left: 5),
if (isWeb)
SizedBox(
height: 62,
child: Align(
alignment: Alignment.center,
child: settingsText,
), ),
), ).marginOnly(left: 20),
).marginOnly(left: 20, top: 10), if (!isWeb)
SizedBox(
height: 62,
child: settingsText,
).marginOnly(left: 20, top: 10),
const Spacer(), const Spacer(),
], ],
); );
@@ -261,13 +333,10 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
Widget _listView({required List<_TabInfo> tabs}) { Widget _listView({required List<_TabInfo> tabs}) {
final scrollController = ScrollController(); final scrollController = ScrollController();
return DesktopScrollWrapper( return ListView(
scrollController: scrollController, controller: scrollController,
child: ListView( children: tabs.map((tab) => _listItem(tab: tab)).toList(),
physics: DraggableNeverScrollableScrollPhysics(), );
controller: scrollController,
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
));
} }
Widget _listItem({required _TabInfo tab}) { Widget _listItem({required _TabInfo tab}) {
@@ -322,34 +391,32 @@ class _General extends StatefulWidget {
} }
class _GeneralState extends State<_General> { class _GeneralState extends State<_General> {
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service'); final RxBool serviceStop =
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs; RxBool serviceBtnEnabled = true.obs;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scrollController = ScrollController(); final scrollController = ScrollController();
return DesktopScrollWrapper( return ListView(
scrollController: scrollController, controller: scrollController,
child: ListView( children: [
physics: DraggableNeverScrollableScrollPhysics(), if (!isWeb) service(),
controller: scrollController, theme(),
children: [ _Card(title: 'Language', children: [language()]),
service(), if (!isWeb) hwcodec(),
theme(), if (!isWeb) audio(context),
_Card(title: 'Language', children: [language()]), if (!isWeb) record(context),
hwcodec(), if (!isWeb) WaylandCard(),
audio(context), other()
record(context), ],
WaylandCard(), ).marginOnly(bottom: _kListViewBottomMargin);
other()
],
).marginOnly(bottom: _kListViewBottomMargin));
} }
Widget theme() { Widget theme() {
final current = MyTheme.getThemeModePreference().toShortString(); final current = MyTheme.getThemeModePreference().toShortString();
onChanged(String value) { onChanged(String value) async {
MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
setState(() {}); setState(() {});
} }
@@ -394,13 +461,13 @@ class _GeneralState extends State<_General> {
Widget other() { Widget other() {
final children = <Widget>[ final children = <Widget>[
if (!bind.isIncomingOnly()) if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs', _OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs, kOptionEnableConfirmClosingTabs,
isServer: false), isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
wallpaper(), if (!isWeb) wallpaper(),
if (!bind.isIncomingOnly()) ...[ if (!isWeb && !bind.isIncomingOnly()) ...[
_OptionCheckBox( _OptionCheckBox(
context, context,
'Open connection in new tab', 'Open connection in new tab',
@@ -417,18 +484,19 @@ class _GeneralState extends State<_General> {
kOptionAllowAlwaysSoftwareRender, kOptionAllowAlwaysSoftwareRender,
), ),
), ),
Tooltip( if (!isWeb)
message: translate('texture_render_tip'), Tooltip(
child: _OptionCheckBox( message: translate('texture_render_tip'),
context, child: _OptionCheckBox(
"Use texture rendering", context,
kOptionTextureRender, "Use texture rendering",
optGetter: bind.mainGetUseTextureRender, kOptionTextureRender,
optSetter: (k, v) async => optGetter: bind.mainGetUseTextureRender,
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), optSetter: (k, v) async =>
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
),
), ),
), if (!isWeb && !bind.isCustomClient())
if (!bind.isCustomClient())
_OptionCheckBox( _OptionCheckBox(
context, context,
'Check for software update on startup', 'Check for software update on startup',
@@ -443,7 +511,7 @@ class _GeneralState extends State<_General> {
) )
], ],
]; ];
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
children.add(_OptionCheckBox( children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless)); context, 'Allow linux headless', kOptionAllowLinuxHeadless));
} }
@@ -539,7 +607,6 @@ class _GeneralState extends State<_General> {
bool user_dir_exists = await Directory(user_dir).exists(); bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists = bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false; showRootDir ? await Directory(root_dir).exists() : false;
// canLaunchUrl blocked on windows portable, user SYSTEM
return { return {
'user_dir': user_dir, 'user_dir': user_dir,
'root_dir': root_dir, 'root_dir': root_dir,
@@ -553,12 +620,18 @@ class _GeneralState extends State<_General> {
bool root_dir_exists = map['root_dir_exists']!; bool root_dir_exists = map['root_dir_exists']!;
bool user_dir_exists = map['user_dir_exists']!; bool user_dir_exists = map['user_dir_exists']!;
return _Card(title: 'Recording', children: [ return _Card(title: 'Recording', children: [
_OptionCheckBox(context, 'Automatically record incoming sessions', if (!bind.isOutgoingOnly())
kOptionAllowAutoRecordIncoming), _OptionCheckBox(context, 'Automatically record incoming sessions',
if (showRootDir) kOptionAllowAutoRecordIncoming),
if (!bind.isIncomingOnly())
_OptionCheckBox(context, 'Automatically record outgoing sessions',
kOptionAllowAutoRecordOutgoing,
isServer: false),
if (showRootDir && !bind.isOutgoingOnly())
Row( Row(
children: [ children: [
Text('${translate("Incoming")}:'), Text(
'${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: root_dir_exists onTap: root_dir_exists
@@ -575,45 +648,49 @@ class _GeneralState extends State<_General> {
), ),
], ],
).marginOnly(left: _kContentHMargin), ).marginOnly(left: _kContentHMargin),
Row( if (!(showRootDir && bind.isIncomingOnly()))
children: [ Row(
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'), children: [
Expanded( Text(
child: GestureDetector( '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
onTap: user_dir_exists Expanded(
? () => launchUrl(Uri.file(user_dir)) child: GestureDetector(
: null, onTap: user_dir_exists
child: Text( ? () => launchUrl(Uri.file(user_dir))
user_dir,
softWrap: true,
style: user_dir_exists
? const TextStyle(decoration: TextDecoration.underline)
: null, : null,
)).marginOnly(left: 10), child: Text(
), user_dir,
ElevatedButton( softWrap: true,
onPressed: isOptionFixed(kOptionVideoSaveDirectory) style: user_dir_exists
? null ? const TextStyle(
: () async { decoration: TextDecoration.underline)
String? initialDirectory; : null,
if (await Directory.fromUri(Uri.directory(user_dir)) )).marginOnly(left: 10),
.exists()) { ),
initialDirectory = user_dir; ElevatedButton(
} onPressed: isOptionFixed(kOptionVideoSaveDirectory)
String? selectedDirectory = ? null
await FilePicker.platform.getDirectoryPath( : () async {
initialDirectory: initialDirectory); String? initialDirectory;
if (selectedDirectory != null) { if (await Directory.fromUri(
await bind.mainSetOption( Uri.directory(user_dir))
key: kOptionVideoSaveDirectory, .exists()) {
value: selectedDirectory); initialDirectory = user_dir;
setState(() {}); }
} String? selectedDirectory =
}, await FilePicker.platform.getDirectoryPath(
child: Text(translate('Change'))) initialDirectory: initialDirectory);
.marginOnly(left: 5), if (selectedDirectory != null) {
], await bind.mainSetLocalOption(
).marginOnly(left: _kContentHMargin), key: kOptionVideoSaveDirectory,
value: selectedDirectory);
setState(() {});
}
},
child: Text(translate('Change')))
.marginOnly(left: 5),
],
).marginOnly(left: _kContentHMargin),
]); ]);
}); });
} }
@@ -641,8 +718,9 @@ class _GeneralState extends State<_General> {
initialKey: currentKey, initialKey: currentKey,
onChanged: (key) async { onChanged: (key) async {
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
reloadAllWindows(); if (isWeb) reloadCurrentWindow();
bind.mainChangeLanguage(lang: key); if (!isWeb) reloadAllWindows();
if (!isWeb) bind.mainChangeLanguage(lang: key);
}, },
enabled: !isOptFixed, enabled: !isOptFixed,
).marginOnly(left: _kContentHMargin); ).marginOnly(left: _kContentHMargin);
@@ -672,29 +750,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return DesktopScrollWrapper( return SingleChildScrollView(
scrollController: scrollController, controller: scrollController,
child: SingleChildScrollView( child: Column(
physics: DraggableNeverScrollableScrollPhysics(), children: [
controller: scrollController, _lock(locked, 'Unlock Security Settings', () {
child: Column( locked = false;
children: [ setState(() => {});
_lock(locked, 'Unlock Security Settings', () { }),
locked = false; preventMouseKeyBuilder(
setState(() => {}); block: locked,
}), child: Column(children: [
AbsorbPointer( permissions(context),
absorbing: locked, password(context),
child: Column(children: [ _Card(title: '2FA', children: [tfa()]),
permissions(context), _Card(title: 'ID', children: [changeId()]),
password(context), more(context),
_Card(title: '2FA', children: [tfa()]), ]),
_Card(title: 'ID', children: [changeId()]), ),
more(context), ],
]), )).marginOnly(bottom: _kListViewBottomMargin);
),
],
)).marginOnly(bottom: _kListViewBottomMargin));
} }
Widget tfa() { Widget tfa() {
@@ -996,7 +1071,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
translate('Accept sessions via both'), translate('Accept sessions via both'),
]; ];
var modeInitialKey = model.approveMode; var modeInitialKey = model.approveMode;
if (!modeKeys.contains(modeInitialKey)) modeInitialKey = ''; if (!modeKeys.contains(modeInitialKey)) {
modeInitialKey = defaultOptionApproveMode;
}
final usePassword = model.approveMode != 'click'; final usePassword = model.approveMode != 'click';
final isApproveModeFixed = isOptionFixed(kOptionApproveMode); final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
@@ -1111,7 +1188,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding: contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12), EdgeInsets.symmetric(vertical: 12, horizontal: 12),
), ),
).marginOnly(right: 15), ).workaroundFreezeLinuxMint().marginOnly(right: 15),
), ),
Obx(() => ElevatedButton( Obx(() => ElevatedButton(
onPressed: applyEnabled.value && onPressed: applyEnabled.value &&
@@ -1268,7 +1345,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
contentPadding: contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12), EdgeInsets.symmetric(vertical: 12, horizontal: 12),
), ),
).marginOnly(right: 15), ).workaroundFreezeLinuxMint().marginOnly(right: 15),
), ),
Obx(() => ElevatedButton( Obx(() => ElevatedButton(
onPressed: onPressed:
@@ -1337,112 +1414,84 @@ class _Network extends StatefulWidget {
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled(); bool locked = !isWeb && bind.mainIsInstalled();
final scrollController = ScrollController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
bool enabled = !locked; return ListView(controller: scrollController, children: [
final scrollController = ScrollController(); _lock(locked, 'Unlock Network Settings', () {
locked = false;
setState(() => {});
}),
preventMouseKeyBuilder(
block: locked,
child: Column(children: [
network(context),
]),
),
]).marginOnly(bottom: _kListViewBottomMargin);
}
Widget network(BuildContext context) {
final hideServer = final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy = final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
controller: scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
children: [
_lock(locked, 'Unlock Network Settings', () {
locked = false;
setState(() => {});
}),
AbsorbPointer(
absorbing: locked,
child: Column(children: [
if (!hideServer) server(enabled),
if (!hideProxy)
_Card(title: 'Proxy', children: [
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled),
]),
]),
),
]).marginOnly(bottom: _kListViewBottomMargin));
}
server(bool enabled) { if (hideServer && hideProxy) {
// Simple temp wrapper for PR check return Offstage();
tmpWrapper() {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
Map<String, dynamic> oldOptions = jsonDecode(bind.mainGetOptionsSync());
old(String key) {
return (oldOptions[key] ?? '').trim();
}
RxString idErrMsg = ''.obs;
RxString relayErrMsg = ''.obs;
RxString apiErrMsg = ''.obs;
var idController =
TextEditingController(text: old('custom-rendezvous-server'));
var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key'));
final controllers = [
idController,
relayController,
apiController,
keyController,
];
final errMsgs = [
idErrMsg,
relayErrMsg,
apiErrMsg,
];
submit() async {
bool result = await setServerConfig(
null,
errMsgs,
ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text));
if (result) {
setState(() {});
showToast(translate('Successful'));
} else {
showToast(translate('Failed'));
}
}
bool secure = !enabled;
return _Card(
title: 'ID/Relay Server',
title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
children: [
Column(
children: [
Obx(() => _LabeledTextField(context, 'ID Server', idController,
idErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'Relay Server',
relayController, relayErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'API Server',
apiController, apiErrMsg.value, enabled, secure)),
_LabeledTextField(
context, 'Key', keyController, '', enabled, secure),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
).marginOnly(top: 10),
],
)
]);
} }
return tmpWrapper(); return _Card(
title: 'Network',
children: [
Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!hideServer)
ListTile(
leading: Icon(Icons.dns_outlined, color: _accentColor),
title: Text(
translate('ID/Relay Server'),
style: TextStyle(fontSize: _kContentFontSize),
),
enabled: !locked,
onTap: () => showServerSettings(gFFI.dialogManager),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16),
minLeadingWidth: 0,
horizontalTitleGap: 10,
),
if (!hideServer && !hideProxy)
Divider(height: 1, indent: 16, endIndent: 16),
if (!hideProxy)
ListTile(
leading:
Icon(Icons.network_ping_outlined, color: _accentColor),
title: Text(
translate('Socks5/Http(s) Proxy'),
style: TextStyle(fontSize: _kContentFontSize),
),
enabled: !locked,
onTap: changeSocks5Proxy,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16),
minLeadingWidth: 0,
horizontalTitleGap: 10,
),
],
),
),
],
);
} }
} }
@@ -1457,19 +1506,14 @@ class _DisplayState extends State<_Display> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scrollController = ScrollController(); final scrollController = ScrollController();
return DesktopScrollWrapper( return ListView(controller: scrollController, children: [
scrollController: scrollController, viewStyle(context),
child: ListView( scrollStyle(context),
controller: scrollController, imageQuality(context),
physics: DraggableNeverScrollableScrollPhysics(), codec(context),
children: [ if (!isWeb) privacyModeImpl(context),
viewStyle(context), other(context),
scrollStyle(context), ]).marginOnly(bottom: _kListViewBottomMargin);
imageQuality(context),
codec(context),
privacyModeImpl(context),
other(context),
]).marginOnly(bottom: _kListViewBottomMargin));
} }
Widget viewStyle(BuildContext context) { Widget viewStyle(BuildContext context) {
@@ -1692,15 +1736,12 @@ class _AccountState extends State<_Account> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scrollController = ScrollController(); final scrollController = ScrollController();
return DesktopScrollWrapper( return ListView(
scrollController: scrollController, controller: scrollController,
child: ListView( children: [
physics: DraggableNeverScrollableScrollPhysics(), _Card(title: 'Account', children: [accountAction(), useInfo()]),
controller: scrollController, ],
children: [ ).marginOnly(bottom: _kListViewBottomMargin);
_Card(title: 'Account', children: [accountAction(), useInfo()]),
],
).marginOnly(bottom: _kListViewBottomMargin));
} }
Widget accountAction() { Widget accountAction() {
@@ -1797,18 +1838,14 @@ class _PluginState extends State<_Plugin> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
bind.pluginListReload(); bind.pluginListReload();
final scrollController = ScrollController(); final scrollController = ScrollController();
return DesktopScrollWrapper( return ChangeNotifierProvider.value(
scrollController: scrollController, value: pluginManager,
child: ChangeNotifierProvider.value( child: Consumer<PluginManager>(builder: (context, model, child) {
value: pluginManager, return ListView(
child: Consumer<PluginManager>(builder: (context, model, child) { controller: scrollController,
return ListView( children: model.plugins.map((entry) => pluginCard(entry)).toList(),
physics: DraggableNeverScrollableScrollPhysics(), ).marginOnly(bottom: _kListViewBottomMargin);
controller: scrollController, }),
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
).marginOnly(bottom: _kListViewBottomMargin);
}),
),
); );
} }
@@ -1860,74 +1897,72 @@ class _AboutState extends State<_About> {
final fingerprint = data['fingerprint'].toString(); final fingerprint = data['fingerprint'].toString();
const linkStyle = TextStyle(decoration: TextDecoration.underline); const linkStyle = TextStyle(decoration: TextDecoration.underline);
final scrollController = ScrollController(); final scrollController = ScrollController();
return DesktopScrollWrapper( return SingleChildScrollView(
scrollController: scrollController, controller: scrollController,
child: SingleChildScrollView( child: _Card(title: translate('About RustDesk'), children: [
controller: scrollController, Column(
physics: DraggableNeverScrollableScrollPhysics(), crossAxisAlignment: CrossAxisAlignment.start,
child: _Card(title: translate('About RustDesk'), children: [ children: [
Column( const SizedBox(
crossAxisAlignment: CrossAxisAlignment.start, height: 8.0,
children: [ ),
const SizedBox( SelectionArea(
height: 8.0, child: Text('${translate('Version')}: $version')
), .marginSymmetric(vertical: 4.0)),
SelectionArea( SelectionArea(
child: Text('${translate('Version')}: $version') child: Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0)), .marginSymmetric(vertical: 4.0)),
SelectionArea( if (!isWeb)
child: Text('${translate('Build Date')}: $buildDate') SelectionArea(
.marginSymmetric(vertical: 4.0)), child: Text('${translate('Fingerprint')}: $fingerprint')
SelectionArea( .marginSymmetric(vertical: 4.0)),
child: Text('${translate('Fingerprint')}: $fingerprint') InkWell(
.marginSymmetric(vertical: 4.0)), onTap: () {
InkWell( launchUrlString('https://rustdesk.com/privacy.html');
onTap: () { },
launchUrlString('https://rustdesk.com/privacy.html'); child: Text(
}, translate('Privacy Statement'),
child: Text( style: linkStyle,
translate('Privacy Statement'), ).marginSymmetric(vertical: 4.0)),
style: linkStyle, InkWell(
).marginSymmetric(vertical: 4.0)), onTap: () {
InkWell( launchUrlString('https://rustdesk.com');
onTap: () { },
launchUrlString('https://rustdesk.com'); child: Text(
}, translate('Website'),
child: Text( style: linkStyle,
translate('Website'), ).marginSymmetric(vertical: 4.0)),
style: linkStyle, Container(
).marginSymmetric(vertical: 4.0)), decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
Container( padding:
decoration: const BoxDecoration(color: Color(0xFF2c8cff)), const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
padding: child: SelectionArea(
const EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Row(
child: SelectionArea( children: [
child: Row( Expanded(
children: [ child: Column(
Expanded( crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
Text( style: const TextStyle(color: Colors.white),
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
style: const TextStyle(color: Colors.white),
),
Text(
translate('Slogan_tip'),
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white),
)
],
), ),
), Text(
], translate('Slogan_tip'),
)), style: TextStyle(
).marginSymmetric(vertical: 4.0) fontWeight: FontWeight.w800,
], color: Colors.white),
).marginOnly(left: _kContentHMargin) )
]), ],
)); ),
),
],
)),
).marginSymmetric(vertical: 4.0)
],
).marginOnly(left: _kContentHMargin)
]),
);
}); });
} }
} }
@@ -2245,26 +2280,39 @@ _LabeledTextField(
String errorText, String errorText,
bool enabled, bool enabled,
bool secure) { bool secure) {
return Row( return Table(
columnWidths: const {
0: FixedColumnWidth(150),
1: FlexColumnWidth(),
},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [ children: [
ConstrainedBox( TableRow(
constraints: const BoxConstraints(minWidth: 140), children: [
child: Text( Padding(
'${translate(label)}:', padding: const EdgeInsets.only(right: 10),
textAlign: TextAlign.right, child: Text(
style: TextStyle( '${translate(label)}:',
fontSize: 16, color: disabledTextColor(context, enabled)), textAlign: TextAlign.right,
).marginOnly(right: 10)), style: TextStyle(
Expanded( fontSize: 16,
child: TextField( color: disabledTextColor(context, enabled),
),
),
),
TextField(
controller: controller, controller: controller,
enabled: enabled, enabled: enabled,
obscureText: secure, obscureText: secure,
autocorrect: false,
decoration: InputDecoration( decoration: InputDecoration(
errorText: errorText.isNotEmpty ? errorText : null), errorText: errorText.isNotEmpty ? errorText : null,
),
style: TextStyle( style: TextStyle(
color: disabledTextColor(context, enabled), color: disabledTextColor(context, enabled),
)), ),
).workaroundFreezeLinuxMint(),
],
), ),
], ],
).marginOnly(bottom: 8); ).marginOnly(bottom: 8);
@@ -2442,7 +2490,7 @@ void changeSocks5Proxy() async {
controller: proxyController, controller: proxyController,
autofocus: true, autofocus: true,
enabled: !isOptFixed, enabled: !isOptFixed,
), ).workaroundFreezeLinuxMint(),
), ),
], ],
).marginOnly(bottom: 8), ).marginOnly(bottom: 8),
@@ -2462,7 +2510,7 @@ void changeSocks5Proxy() async {
labelText: isMobile ? translate('Username') : null, labelText: isMobile ? translate('Username') : null,
), ),
enabled: !isOptFixed, enabled: !isOptFixed,
), ).workaroundFreezeLinuxMint(),
), ),
], ],
).marginOnly(bottom: 8), ).marginOnly(bottom: 8),
@@ -2487,7 +2535,8 @@ void changeSocks5Proxy() async {
: Icons.visibility))), : Icons.visibility))),
controller: pwdController, controller: pwdController,
enabled: !isOptFixed, enabled: !isOptFixed,
)), maxLength: bind.mainMaxEncryptLen(),
).workaroundFreezeLinuxMint()),
), ),
], ],
), ),

View File

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

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
@@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget {
required this.id, required this.id,
required this.password, required this.password,
required this.isSharedPassword, required this.isSharedPassword,
required this.tabController, this.tabController,
this.connToken,
this.forceRelay}) this.forceRelay})
: super(key: key); : super(key: key);
final String id; final String id;
final String? password; final String? password;
final bool? isSharedPassword; final bool? isSharedPassword;
final bool? forceRelay; final bool? forceRelay;
final DesktopTabController tabController; final String? connToken;
final DesktopTabController? tabController;
@override @override
State<StatefulWidget> createState() => _FileManagerPageState(); State<StatefulWidget> createState() => _FileManagerPageState();
} }
class _FileManagerPageState extends State<FileManagerPage> class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none); final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _dropMaskVisible = false.obs; // TODO impl drop mask final _dropMaskVisible = false.obs; // TODO impl drop mask
@@ -87,6 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
isFileTransfer: true, isFileTransfer: true,
password: widget.password, password: widget.password,
isSharedPassword: widget.isSharedPassword, isSharedPassword: widget.isSharedPassword,
connToken: widget.connToken,
forceRelay: widget.forceRelay); forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager _ffi.dialogManager
@@ -96,12 +102,16 @@ class _FileManagerPageState extends State<FileManagerPage>
if (!isLinux) { if (!isLinux) {
WakelockPlus.enable(); WakelockPlus.enable();
} }
if (isWeb) {
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
}
debugPrint("File manager page init success with id ${widget.id}"); debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState); _ffi.dialogManager.setOverlayState(_overlayKeyState);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id); widget.tabController?.onSelected?.call(widget.id);
}); });
WidgetsBinding.instance.addObserver(this);
} }
@override @override
@@ -114,12 +124,21 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
Get.delete<FFI>(tag: 'ft_${widget.id}'); Get.delete<FFI>(tag: 'ft_${widget.id}');
}); });
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
jobController.jobTable.refresh();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@@ -129,10 +148,11 @@ class _FileManagerPageState extends State<FileManagerPage>
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row( body: Row(
children: [ children: [
Flexible( if (!isWeb)
flex: 3, Flexible(
child: dropArea(FileManagerView( flex: 3,
model.localController, _ffi, _mouseFocusScope))), child: dropArea(FileManagerView(
model.localController, _ffi, _mouseFocusScope))),
Flexible( Flexible(
flex: 3, flex: 3,
child: dropArea(FileManagerView( child: dropArea(FileManagerView(
@@ -173,10 +193,31 @@ class _FileManagerPageState extends State<FileManagerPage>
/// transfer status list /// transfer status list
/// watch transfer status /// watch transfer status
Widget statusList() { Widget statusList() {
Widget getIcon(JobProgress job) {
final color = Theme.of(context).tabBarTheme.labelColor;
switch (job.type) {
case JobType.deleteDir:
case JobType.deleteFile:
return Icon(Icons.delete_outline, color: color);
default:
return Transform.rotate(
angle: isWeb
? job.isRemoteToLocal
? pi / 2
: pi / 2 * 3
: job.isRemoteToLocal
? pi
: 0,
child: Icon(Icons.arrow_forward_ios, color: color),
);
}
}
statusListView(List<JobProgress> jobs) => ListView.builder( statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = jobs[index]; final item = jobs[index];
final status = item.getStatus();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 5), padding: const EdgeInsets.only(bottom: 5),
child: generateCard( child: generateCard(
@@ -186,15 +227,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Transform.rotate( getIcon(item)
angle: item.isRemoteToLocal ? pi : 0, .marginSymmetric(horizontal: 10, vertical: 12),
child: SvgPicture.asset("assets/arrow.svg",
colorFilter: svgColor(
Theme.of(context).tabBarTheme.labelColor)),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -203,45 +237,28 @@ class _FileManagerPageState extends State<FileManagerPage>
Tooltip( Tooltip(
waitDuration: Duration(milliseconds: 500), waitDuration: Duration(milliseconds: 500),
message: item.jobName, message: item.jobName,
child: Text( child: ExtendedText(
item.fileName, item.jobName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).paddingSymmetric(vertical: 10), overflowWidget: TextOverflowWidget(
), child: Text("..."),
Text( position: TextOverflowPosition.start),
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
), ),
), ),
Offstage( Tooltip(
offstage: item.state != JobState.inProgress, waitDuration: Duration(milliseconds: 500),
child: Text( message: status,
'${translate("Speed")} ${readableFileSize(item.speed)}/s', child: Text(status,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: MyTheme.darkGray,
), )).marginOnly(top: 6),
),
), ),
Offstage( Offstage(
offstage: item.state == JobState.inProgress, offstage: item.type != JobType.transfer ||
child: Text( item.state != JobState.inProgress,
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage: item.state != JobState.inProgress,
child: LinearPercentIndicator( child: LinearPercentIndicator(
padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true, animateFromLastPercent: true,
center: Text( center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
@@ -251,7 +268,7 @@ class _FileManagerPageState extends State<FileManagerPage>
progressColor: MyTheme.accent, progressColor: MyTheme.accent,
backgroundColor: Theme.of(context).hoverColor, backgroundColor: Theme.of(context).hoverColor,
lineHeight: kDesktopFileTransferRowHeight, lineHeight: kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15), ).paddingSymmetric(vertical: 8),
), ),
], ],
), ),
@@ -276,7 +293,6 @@ class _FileManagerPageState extends State<FileManagerPage>
), ),
MenuButton( MenuButton(
tooltip: translate("Delete"), tooltip: translate("Delete"),
padding: EdgeInsets.only(right: 15),
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/close.svg", "assets/close.svg",
colorFilter: svgColor(Colors.white), colorFilter: svgColor(Colors.white),
@@ -289,11 +305,11 @@ class _FileManagerPageState extends State<FileManagerPage>
hoverColor: MyTheme.accent80, hoverColor: MyTheme.accent80,
), ),
], ],
), ).marginAll(12),
], ],
), ),
], ],
).paddingSymmetric(vertical: 10), ),
), ),
); );
}, },
@@ -477,6 +493,9 @@ class _FileManagerViewState extends State<FileManagerView> {
} }
Widget headTools() { Widget headTools() {
var uploadButtonTapPosition = RelativeRect.fill;
RxBool isUploadFolder =
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
return Container( return Container(
child: Column( child: Column(
children: [ children: [
@@ -749,7 +768,7 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
controller: name, controller: name,
autofocus: true, autofocus: true,
), ).workaroundFreezeLinuxMint(),
], ],
), ),
actions: [ actions: [
@@ -799,6 +818,66 @@ class _FileManagerViewState extends State<FileManagerView> {
], ],
), ),
), ),
if (isWeb)
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
isLocal
? EdgeInsets.only(left: 10)
: EdgeInsets.only(right: 10)),
backgroundColor: MaterialStateProperty.all(
selectedItems.items.isEmpty
? MyTheme.accent80
: MyTheme.accent,
),
),
onPressed: () =>
{webselectFiles(is_folder: isUploadFolder.value)},
label: InkWell(
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
focusColor: Colors.transparent,
onTapDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
uploadButtonTapPosition =
RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () async {
final value = await showMenu<bool>(
context: context,
position: uploadButtonTapPosition,
items: [
PopupMenuItem<bool>(
value: false,
child: Text(translate('Upload files')),
),
PopupMenuItem<bool>(
value: true,
child: Text(translate('Upload folder')),
),
]);
if (value != null) {
isUploadFolder.value = value;
bind.mainSetLocalOption(
key: 'upload-folder-button',
value: value ? 'Y' : '');
webselectFiles(is_folder: value);
}
},
child: Icon(Icons.arrow_drop_down),
),
icon: Text(
translate(isUploadFolder.isTrue
? 'Upload folder'
: 'Upload files'),
textAlign: TextAlign.right,
style: TextStyle(
color: Colors.white,
),
).marginOnly(left: 8),
)).marginOnly(left: 16),
Obx(() => ElevatedButton.icon( Obx(() => ElevatedButton.icon(
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>( padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
@@ -832,19 +911,22 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white, : Colors.white,
), ),
) )
: RotatedBox( : isWeb
quarterTurns: 2, ? Offstage()
child: SvgPicture.asset( : RotatedBox(
"assets/arrow.svg", quarterTurns: 2,
colorFilter: svgColor(selectedItems.items.isEmpty child: SvgPicture.asset(
? Theme.of(context).brightness == "assets/arrow.svg",
Brightness.light colorFilter: svgColor(
? MyTheme.grayBg selectedItems.items.isEmpty
: MyTheme.darkGray ? Theme.of(context).brightness ==
: Colors.white), Brightness.light
alignment: Alignment.bottomRight, ? MyTheme.grayBg
), : MyTheme.darkGray
), : Colors.white),
alignment: Alignment.bottomRight,
),
),
label: isLocal label: isLocal
? SvgPicture.asset( ? SvgPicture.asset(
"assets/arrow.svg", "assets/arrow.svg",
@@ -856,7 +938,7 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white), : Colors.white),
) )
: Text( : Text(
translate('Receive'), translate(isWeb ? 'Download' : 'Receive'),
style: TextStyle( style: TextStyle(
color: selectedItems.items.isEmpty color: selectedItems.items.isEmpty
? Theme.of(context).brightness == ? Theme.of(context).brightness ==
@@ -943,6 +1025,7 @@ class _FileManagerViewState extends State<FileManagerView> {
BuildContext context, ScrollController scrollController) { BuildContext context, ScrollController scrollController) {
final fd = controller.directory.value; final fd = controller.directory.value;
final entries = fd.entries; final entries = fd.entries;
Rx<Entry?> rightClickEntry = Rx(null);
return ListSearchActionListener( return ListSearchActionListener(
node: _keyboardNode, node: _keyboardNode,
@@ -1002,16 +1085,69 @@ class _FileManagerViewState extends State<FileManagerView> {
? " " ? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} "; : "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0); var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
onTap() {
final items = selectedItems;
// handle double click
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(items, filteredEntries, entry, isLocal);
}
onSecondaryTap() {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
mod_menu.PopupMenuItem(
child: Text(translate("Rename")),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
rightClickEntry.value = entry;
final future = mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
future.then((value) {
rightClickEntry.value = null;
});
future.onError((error, stackTrace) {
rightClickEntry.value = null;
});
}
}
onSecondaryTapDown(details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
}
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selectedItems.items.contains(entry) color: selectedItems.items.contains(entry)
? Theme.of(context).hoverColor ? MyTheme.button
: Theme.of(context).cardColor, : Theme.of(context).cardColor,
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(5.0), Radius.circular(5.0),
), ),
border: rightClickEntry.value == entry
? Border.all(
color: MyTheme.button,
width: 1.0,
)
: null,
), ),
key: ValueKey(entry.name), key: ValueKey(entry.name),
height: kDesktopFileTransferRowHeight, height: kDesktopFileTransferRowHeight,
@@ -1050,51 +1186,19 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
Expanded( Expanded(
child: Text(entry.name.nonBreaking, child: Text(entry.name.nonBreaking,
style: TextStyle(
color: selectedItems.items
.contains(entry)
? Colors.white
: null),
overflow: overflow:
TextOverflow.ellipsis)) TextOverflow.ellipsis))
]), ]),
)), )),
), ),
onTap: () { onTap: onTap,
final items = selectedItems; onSecondaryTap: onSecondaryTap,
// handle double click onSecondaryTapDown: onSecondaryTapDown,
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
onSecondaryTap: () {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version,
"1.3.0") >=
0)
mod_menu.PopupMenuItem(
child: Text("Rename"),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
}
},
onSecondaryTapDown: (details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
},
), ),
SizedBox( SizedBox(
width: 2.0, width: 2.0,
@@ -1111,11 +1215,17 @@ class _FileManagerViewState extends State<FileManagerView> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: selectedItems.items
.contains(entry)
? Colors.white70
: MyTheme.darkGray,
), ),
)), )),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
// Divider from header. // Divider from header.
SizedBox( SizedBox(
@@ -1131,9 +1241,16 @@ class _FileManagerViewState extends State<FileManagerView> {
sizeStr, sizeStr,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 10, color: MyTheme.darkGray), fontSize: 10,
color:
selectedItems.items.contains(entry)
? Colors.white70
: MyTheme.darkGray),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
), ),
], ],
@@ -1540,7 +1657,7 @@ class _FileManagerViewState extends State<FileManagerView> {
onChanged: _locationStatus.value == LocationStatus.fileSearchBar onChanged: _locationStatus.value == LocationStatus.fileSearchBar
? (searchText) => onSearchText(searchText, isLocal) ? (searchText) => onSearchText(searchText, isLocal)
: null, : null,
), ).workaroundFreezeLinuxMint(),
) )
], ],
); );

View File

@@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
isSharedPassword: params['isSharedPassword'], isSharedPassword: params['isSharedPassword'],
tabController: tabController, tabController: tabController,
forceRelay: params['forceRelay'], forceRelay: params['forceRelay'],
connToken: params['connToken'],
))); )));
} }
@@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
super.initState(); super.initState();
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( debugPrint(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
// for simplify, just replace connectionId // for simplify, just replace connectionId
if (call.method == kWindowEventNewFileTransfer) { if (call.method == kWindowEventNewFileTransfer) {
@@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
isSharedPassword: args['isSharedPassword'], isSharedPassword: args['isSharedPassword'],
tabController: tabController, tabController: tabController,
forceRelay: args['forceRelay'], forceRelay: args['forceRelay'],
connToken: args['connToken'],
))); )));
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
tabController.clear(); tabController.clear();

View File

@@ -147,7 +147,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em), contentPadding: EdgeInsets.all(0.75 * em),
), ),
).marginOnly(right: 10), ).workaroundFreezeLinuxMint().marginOnly(right: 10),
), ),
Obx( Obx(
() => OutlinedButton.icon( () => OutlinedButton.icon(

View File

@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
required this.isRDP, required this.isRDP,
required this.isSharedPassword, required this.isSharedPassword,
this.forceRelay, this.forceRelay,
this.connToken,
}) : super(key: key); }) : super(key: key);
final String id; final String id;
final String? password; final String? password;
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
final bool isRDP; final bool isRDP;
final bool? forceRelay; final bool? forceRelay;
final bool? isSharedPassword; final bool? isSharedPassword;
final String? connToken;
@override @override
State<PortForwardPage> createState() => _PortForwardPageState(); State<PortForwardPage> createState() => _PortForwardPageState();
@@ -62,6 +64,7 @@ class _PortForwardPageState extends State<PortForwardPage>
password: widget.password, password: widget.password,
isSharedPassword: widget.isSharedPassword, isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
connToken: widget.connToken,
isRdp: widget.isRDP); isRdp: widget.isRDP);
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}'); Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}"); debugPrint("Port forward page init success with id ${widget.id}");
@@ -235,7 +238,7 @@ class _PortForwardPageState extends State<PortForwardPage>
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
decoration: InputDecoration( decoration: InputDecoration(
hintText: hint, hintText: hint,
))), )).workaroundFreezeLinuxMint()),
); );
} }

View File

@@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
tabController: tabController, tabController: tabController,
isRDP: isRDP, isRDP: isRDP,
forceRelay: params['forceRelay'], forceRelay: params['forceRelay'],
connToken: params['connToken'],
))); )));
} }
@@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
isRDP: isRDP, isRDP: isRDP,
tabController: tabController, tabController: tabController,
forceRelay: args['forceRelay'], forceRelay: args['forceRelay'],
connToken: args['connToken'],
))); )));
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
tabController.clear(); tabController.clear();

View File

@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart'; import '../../consts.dart';
@@ -115,6 +114,8 @@ class _RemotePageState extends State<RemotePage>
_ffi.imageModel.addCallbackOnFirstImage((String peerId) { _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded( showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
}); });
_ffi.start( _ffi.start(
widget.id, widget.id,
@@ -245,13 +246,14 @@ class _RemotePageState extends State<RemotePage>
super.dispose(); super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onRemotePageDispose(closeSession); _ffi.textureModel.onRemotePageDispose(closeSession);
// ensure we leave this session, this is a double check if (closeSession) {
_ffi.inputModel.enterOrLeave(false); // ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay(); _ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage(); _ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages(); _ffi.cursorModel.disposeImages();
_ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose(); _rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession); await _ffi.close(closeSession: closeSession);
_timer?.cancel(); _timer?.cancel();
@@ -739,12 +741,6 @@ class _ImagePaintState extends State<ImagePaint> {
ScrollController horizontal, ScrollController horizontal,
ScrollController vertical, ScrollController vertical,
) { ) {
final scrollConfig = CustomMouseWheelScrollConfig(
scrollDuration: kDefaultScrollDuration,
scrollCurve: Curves.linearToEaseOut,
mouseWheelTurnsThrottleTimeMs:
kDefaultMouseWheelThrottleDuration.inMilliseconds,
scrollAmountMultiplier: kDefaultScrollAmountMultiplier);
var widget = child; var widget = child;
if (layoutSize.width < size.width) { if (layoutSize.width < size.width) {
widget = ScrollConfiguration( widget = ScrollConfiguration(
@@ -790,36 +786,26 @@ class _ImagePaintState extends State<ImagePaint> {
); );
} }
if (layoutSize.width < size.width) { if (layoutSize.width < size.width) {
widget = ImprovedScrolling( widget = RawScrollbar(
scrollController: horizontal, thickness: kScrollbarThickness,
enableCustomMouseWheelScrolling: cursorOverImage.isFalse, thumbColor: Colors.grey,
customMouseWheelScrollConfig: scrollConfig, controller: horizontal,
child: RawScrollbar( thumbVisibility: false,
thickness: kScrollbarThickness, trackVisibility: false,
thumbColor: Colors.grey, notificationPredicate: layoutSize.height < size.height
controller: horizontal, ? (notification) => notification.depth == 1
thumbVisibility: false, : defaultScrollNotificationPredicate,
trackVisibility: false, child: widget,
notificationPredicate: layoutSize.height < size.height
? (notification) => notification.depth == 1
: defaultScrollNotificationPredicate,
child: widget,
),
); );
} }
if (layoutSize.height < size.height) { if (layoutSize.height < size.height) {
widget = ImprovedScrolling( widget = RawScrollbar(
scrollController: vertical, thickness: kScrollbarThickness,
enableCustomMouseWheelScrolling: cursorOverImage.isFalse, thumbColor: Colors.grey,
customMouseWheelScrollConfig: scrollConfig, controller: vertical,
child: RawScrollbar( thumbVisibility: false,
thickness: kScrollbarThickness, trackVisibility: false,
thumbColor: Colors.grey, child: widget,
controller: vertical,
thumbVisibility: false,
trackVisibility: false,
child: widget,
),
); );
} }

View File

@@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
RemoteCountState.find().value = tabController.length; RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async { Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
print( debugPrint(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue; dynamic returnValue;

View File

@@ -110,7 +110,8 @@ class ConnectionManager extends StatefulWidget {
class ConnectionManagerState extends State<ConnectionManager> class ConnectionManagerState extends State<ConnectionManager>
with WidgetsBindingObserver { with WidgetsBindingObserver {
final RxBool _block = false.obs; final RxBool _controlPageBlock = false.obs;
final RxBool _sidePageBlock = false.obs;
ConnectionManagerState() { ConnectionManagerState() {
gFFI.serverModel.tabController.onSelected = (client_id_str) { gFFI.serverModel.tabController.onSelected = (client_id_str) {
@@ -139,7 +140,8 @@ class ConnectionManagerState extends State<ConnectionManager>
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
if (!allowRemoteCMModification()) { if (!allowRemoteCMModification()) {
shouldBeBlocked(_block, null); shouldBeBlocked(_controlPageBlock, null);
shouldBeBlocked(_sidePageBlock, null);
} }
} }
} }
@@ -192,7 +194,6 @@ class ConnectionManagerState extends State<ConnectionManager>
selectedBorderColor: MyTheme.accent, selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100, maxLabelWidth: 100,
tail: null, //buildScrollJumper(), tail: null, //buildScrollJumper(),
blockTab: allowRemoteCMModification() ? null : _block,
tabBuilder: (key, icon, label, themeConf) { tabBuilder: (key, icon, label, themeConf) {
final client = serverModel.clients final client = serverModel.clients
.firstWhereOrNull((client) => client.id.toString() == key); .firstWhereOrNull((client) => client.id.toString() == key);
@@ -237,13 +238,20 @@ class ConnectionManagerState extends State<ConnectionManager>
? buildSidePage() ? buildSidePage()
: buildRemoteBlock( : buildRemoteBlock(
child: buildSidePage(), child: buildSidePage(),
block: _block, block: _sidePageBlock,
mask: true), mask: true),
)), )),
SizedBox( SizedBox(
width: realClosedWidth, width: realClosedWidth,
child: child: SizedBox(
SizedBox(width: realClosedWidth, child: pageView)), width: realClosedWidth,
child: allowRemoteCMModification()
? pageView
: buildRemoteBlock(
child: _buildKeyEventBlock(pageView),
block: _controlPageBlock,
mask: false,
))),
]); ]);
return Container( return Container(
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
@@ -268,6 +276,10 @@ class ConnectionManagerState extends State<ConnectionManager>
} }
} }
Widget _buildKeyEventBlock(Widget child) {
return ExcludeFocus(child: child, excluding: true);
}
Widget buildTitleBar() { Widget buildTitleBar() {
return SizedBox( return SizedBox(
height: kDesktopRemoteTabBarHeight, height: kDesktopRemoteTabBarHeight,

View File

@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
localPlatform = kPeerPlatformWindows; localPlatform = kPeerPlatformWindows;
} else if (isLinux) { } else if (isLinux) {
localPlatform = kPeerPlatformLinux; localPlatform = kPeerPlatformLinux;
} else if (isWebOnWindows || isWebOnLinux) {
localPlatform = kPeerPlatformWebDesktop;
} }
// to-do: web desktop support ?
return localPlatform; return localPlatform;
} }

View File

@@ -305,7 +305,7 @@ class RemoteMenuEntry {
}) { }) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text( childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del', translate("Insert Ctrl + Alt + Del"),
style: style, style: style,
), ),
proc: () { proc: () {
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
shadowColor: MyTheme.color(context).shadow, shadowColor: MyTheme.color(context).shadow,
borderRadius: borderRadius, borderRadius: borderRadius,
child: _DraggableShowHide( child: _DraggableShowHide(
id: widget.id,
sessionId: widget.ffi.sessionId, sessionId: widget.ffi.sessionId,
dragging: _dragging, dragging: _dragging,
fractionX: _fractionX, fractionX: _fractionX,
@@ -452,8 +453,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
Widget _buildToolbar(BuildContext context) { Widget _buildToolbar(BuildContext context) {
final List<Widget> toolbarItems = []; final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) { if (!isWebDesktop) {
toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
} }
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
setFullscreen: _setFullscreen, setFullscreen: _setFullscreen,
)); ));
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) { if (!isWeb) {
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
} }
if (!isWeb) toolbarItems.add(_RecordMenu()); if (!isWeb) toolbarItems.add(_RecordMenu());
@@ -1494,7 +1495,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
); );
} }
TextField _resolutionInput(TextEditingController controller) { Widget _resolutionInput(TextEditingController controller) {
return TextField( return TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
@@ -1508,7 +1509,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
], ],
controller: controller, controller: controller,
); ).workaroundFreezeLinuxMint();
} }
List<Widget> _supportedResolutionMenuButtons() => resolutions List<Widget> _supportedResolutionMenuButtons() => resolutions
@@ -1612,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget {
// If use flutter to grab keys, we can only use one mode. // If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported. // Map mode and Legacy mode, at least one of them is supported.
String? modeOnly; String? modeOnly;
if (isInputSourceFlutter) { // Keep both map and legacy mode on web at the moment.
// TODO: Remove legacy mode after web supports translate mode on web.
if (isInputSourceFlutter && isDesktop) {
if (bind.sessionIsKeyboardModeSupported( if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) { sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode; modeOnly = kKeyMapMode;
@@ -1716,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly); sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value); final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value);
} }
: null, : null,
ffi: ffi, ffi: ffi,
@@ -1776,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _IconSubmenuButton( if (isWeb) {
tooltip: 'Chat', return buildTextChatButton();
key: chatButtonKey, } else {
svg: 'assets/chat.svg', return _IconSubmenuButton(
ffi: widget.ffi, tooltip: 'Chat',
color: _ToolbarTheme.blueColor, key: chatButtonKey,
hoverColor: _ToolbarTheme.hoverBlueColor, svg: 'assets/chat.svg',
menuChildrenGetter: () => [textChat(), voiceCall()]); ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildrenGetter: () => [textChat(), voiceCall()]);
}
}
buildTextChatButton() {
return _IconMenuButton(
assetName: 'assets/message_24dp_5F6368.svg',
tooltip: 'Text chat',
key: chatButtonKey,
onPressed: _textChatOnPressed,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
);
} }
textChat() { textChat() {
return MenuButton( return MenuButton(
child: Text(translate('Text chat')), child: Text(translate('Text chat')),
ffi: widget.ffi, ffi: widget.ffi,
onPressed: () { onPressed: _textChatOnPressed);
RenderBox? renderBox = }
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos; _textChatOnPressed() {
if (renderBox != null) { RenderBox? renderBox =
final pos = renderBox.localToGlobal(Offset.zero); chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); Offset? initPos;
} if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
widget.ffi.chatModel.changeCurrentKey( initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
MessageKey(widget.ffi.id, ChatModel.clientModeID)); }
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); widget.ffi.chatModel
}); .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
} }
voiceCall() { voiceCall() {
@@ -1904,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
var ffi = Provider.of<FfiModel>(context); var ffi = Provider.of<FfiModel>(context);
var recordingModel = Provider.of<RecordingModel>(context); var recordingModel = Provider.of<RecordingModel>(context);
final visible = final visible =
(recordingModel.start || ffi.permissions['recording'] != false) && (recordingModel.start || ffi.permissions['recording'] != false);
ffi.pi.currentDisplay != kAllDisplayValue;
if (!visible) return Offstage(); if (!visible) return Offstage();
return _IconMenuButton( return _IconMenuButton(
assetName: 'assets/rec.svg', assetName: 'assets/rec.svg',
@@ -2214,6 +2233,7 @@ class RdoMenuButton<T> extends StatelessWidget {
} }
class _DraggableShowHide extends StatefulWidget { class _DraggableShowHide extends StatefulWidget {
final String id;
final SessionID sessionId; final SessionID sessionId;
final RxDouble fractionX; final RxDouble fractionX;
final RxBool dragging; final RxBool dragging;
@@ -2225,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget {
const _DraggableShowHide({ const _DraggableShowHide({
Key? key, Key? key,
required this.id,
required this.sessionId, required this.sessionId,
required this.fractionX, required this.fractionX,
required this.dragging, required this.dragging,
@@ -2314,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
); );
final isFullscreen = stateGlobal.fullscreen; final isFullscreen = stateGlobal.fullscreen;
const double iconSize = 20; const double iconSize = 20;
buttonWrapper(VoidCallback? onPressed, Widget child,
{Color hoverColor = _ToolbarTheme.blueColor}) {
final bgColor = buttonStyle.backgroundColor?.resolve({});
return TextButton(
onPressed: onPressed,
child: child,
style: buttonStyle.copyWith(
backgroundColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered)) {
return (bgColor ?? hoverColor).withOpacity(0.15);
}
return bgColor;
}),
),
);
}
final child = Row( final child = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildDraggable(context), _buildDraggable(context),
Obx(() => TextButton( Obx(() => buttonWrapper(
onPressed: () { () {
widget.setFullscreen(!isFullscreen.value); widget.setFullscreen(!isFullscreen.value);
}, },
child: Tooltip( Tooltip(
message: translate( message: translate(
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'), isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
child: Icon( child: Icon(
@@ -2333,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
), ),
)), )),
if (!isMacOS) if (!isMacOS && !isWebDesktop)
Obx(() => Offstage( Obx(() => Offstage(
offstage: isFullscreen.isFalse, offstage: isFullscreen.isFalse,
child: TextButton( child: buttonWrapper(
onPressed: () => widget.setMinimize(), widget.setMinimize,
child: Tooltip( Tooltip(
message: translate('Minimize'), message: translate('Minimize'),
child: Icon( child: Icon(
Icons.remove, Icons.remove,
@@ -2347,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
), ),
)), )),
TextButton( buttonWrapper(
onPressed: () => setState(() { () => setState(() {
widget.toolbarState.switchShow(widget.sessionId); widget.toolbarState.switchShow(widget.sessionId);
}), }),
child: Obx((() => Tooltip( Obx((() => Tooltip(
message: message:
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon( child: Icon(
@@ -2360,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
))), ))),
), ),
if (isWebDesktop)
Obx(() {
if (show.isTrue) {
return Offstage();
} else {
return buttonWrapper(
() => closeConnection(id: widget.id),
Tooltip(
message: translate('Close'),
child: Icon(
Icons.close,
size: iconSize,
color: _ToolbarTheme.redColor,
),
),
hoverColor: _ToolbarTheme.redColor,
).paddingOnly(left: iconSize / 2);
}
})
], ],
); );
return TextButtonTheme( return TextButtonTheme(

View File

@@ -1,27 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
class DesktopScrollWrapper extends StatelessWidget {
final ScrollController scrollController;
final Widget child;
const DesktopScrollWrapper(
{Key? key, required this.scrollController, required this.child})
: super(key: key);
@override
Widget build(BuildContext context) {
return ImprovedScrolling(
scrollController: scrollController,
enableCustomMouseWheelScrolling: true,
// enableKeyboardScrolling: true, // strange behavior on mac
customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
scrollDuration: kDefaultScrollDuration,
scrollCurve: Curves.linearToEaseOut,
mouseWheelTurnsThrottleTimeMs:
kDefaultMouseWheelThrottleDuration.inMilliseconds,
scrollAmountMultiplier: kDefaultScrollAmountMultiplier),
child: child,
);
}
}

View File

@@ -246,7 +246,6 @@ class DesktopTab extends StatefulWidget {
final Color? selectedTabBackgroundColor; final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor; final Color? unSelectedTabBackgroundColor;
final Color? selectedBorderColor; final Color? selectedBorderColor;
final RxBool? blockTab;
final DesktopTabController controller; final DesktopTabController controller;
@@ -272,7 +271,6 @@ class DesktopTab extends StatefulWidget {
this.selectedTabBackgroundColor, this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor, this.unSelectedTabBackgroundColor,
this.selectedBorderColor, this.selectedBorderColor,
this.blockTab,
}) : super(key: key); }) : super(key: key);
static RxString tablabelGetter(String peerId) { static RxString tablabelGetter(String peerId) {
@@ -311,7 +309,6 @@ class _DesktopTabState extends State<DesktopTab>
Color? get unSelectedTabBackgroundColor => Color? get unSelectedTabBackgroundColor =>
widget.unSelectedTabBackgroundColor; widget.unSelectedTabBackgroundColor;
Color? get selectedBorderColor => widget.selectedBorderColor; Color? get selectedBorderColor => widget.selectedBorderColor;
RxBool? get blockTab => widget.blockTab;
DesktopTabController get controller => widget.controller; DesktopTabController get controller => widget.controller;
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys; RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
Debouncer get _scrollDebounce => widget._scrollDebounce; Debouncer get _scrollDebounce => widget._scrollDebounce;
@@ -533,25 +530,20 @@ 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 = []; List<Widget> _tabWidgets = [];
Widget _buildPageView() { Widget _buildPageView() {
final child = _buildBlock( final child = Container(
child: Obx(() => PageView( child: Obx(() => PageView(
controller: state.value.pageController, controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: () { children: () {
if (DesktopTabType.cm == tabType) {
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
return state.value.tabs.map((tab) {
return tab.page;
}).toList();
}
/// to-do refactor, separate connection state and UI state for remote session. /// to-do refactor, separate connection state and UI state for remote session.
/// [workaround] PageView children need an immutable list, after it has been passed into PageView /// [workaround] PageView children need an immutable list, after it has been passed into PageView
final tabLen = state.value.tabs.length; final tabLen = state.value.tabs.length;

View File

@@ -36,6 +36,7 @@ WindowType? kWindowType;
late List<String> kBootArgs; late List<String> kBootArgs;
Future<void> main(List<String> args) async { Future<void> main(List<String> args) async {
earlyAssert();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args"); debugPrint("launch args: $args");
@@ -119,6 +120,7 @@ Future<void> initEnv(String appType) async {
void runMainApp(bool startService) async { void runMainApp(bool startService) async {
// register uni links // register uni links
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
checkUpdate();
// trigger connection status updater // trigger connection status updater
await bind.mainCheckConnectStatus(); await bind.mainCheckConnectStatus();
if (startService) { if (startService) {
@@ -155,13 +157,14 @@ void runMainApp(bool startService) async {
void runMobileApp() async { void runMobileApp() async {
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
checkUpdate();
if (isAndroid) androidChannelInit(); if (isAndroid) androidChannelInit();
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath(); if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
draggablePositions.load(); draggablePositions.load();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
if (!isWeb) await initUniLinks(); await initUniLinks();
} }
void runMultiWindow( void runMultiWindow(
@@ -372,7 +375,7 @@ class App extends StatefulWidget {
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
class _AppState extends State<App> { class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -396,6 +399,34 @@ class _AppState extends State<App> {
bind.mainChangeTheme(dark: to.toShortString()); bind.mainChangeTheme(dark: to.toShortString());
} }
}; };
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
_updateOrientation();
}
void _updateOrientation() {
if (isDesktop) return;
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
// https://github.com/flutter/flutter/issues/60899
// stateGlobal.isPortrait.value =
// MediaQuery.of(context).orientation == Orientation.portrait;
final orientation = View.of(context).physicalSize.aspectRatio > 1
? Orientation.landscape
: Orientation.portrait;
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
} }
@override @override
@@ -416,7 +447,9 @@ class _AppState extends State<App> {
child: GetMaterialApp( child: GetMaterialApp(
navigatorKey: globalKey, navigatorKey: globalKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'RustDesk', title: isWeb
? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
: bind.mainGetAppNameSync(),
theme: MyTheme.lightTheme, theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme, darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.currentThemeMode(), themeMode: MyTheme.currentThemeMode(),
@@ -447,7 +480,8 @@ class _AppState extends State<App> {
: (context, child) { : (context, child) {
child = _keepScaleBuilder(context, child); child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child); child = botToastBuilder(context, child);
if (isDesktop && desktopType == DesktopType.main) { if ((isDesktop && desktopType == DesktopType.main) ||
isWebDesktop) {
child = keyListenerBuilder(context, child); child = keyListenerBuilder(context, child);
} }
if (isLinux) { if (isLinux) {
@@ -475,7 +509,7 @@ _registerEventHandler() {
platformFFI.registerEventHandler('theme', 'theme', (evt) async { platformFFI.registerEventHandler('theme', 'theme', (evt) async {
String? dark = evt['dark']; String? dark = evt['dark'];
if (dark != null) { if (dark != null) {
MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
} }
}); });
platformFFI.registerEventHandler('language', 'language', (_) async { platformFFI.registerEventHandler('language', 'language', (_) async {

View File

@@ -3,25 +3,24 @@ import 'dart:async';
import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:auto_size_text_field/auto_size_text_field.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart'; import '../../common/widgets/autocomplete.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import 'home_page.dart'; import 'home_page.dart';
import 'scan_page.dart';
import 'settings_page.dart';
/// Connection page for connecting to a remote peer. /// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget implements PageShape { class ConnectionPage extends StatefulWidget implements PageShape {
ConnectionPage({Key? key}) : super(key: key); ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
@override @override
final icon = const Icon(Icons.connected_tv); final icon = const Icon(Icons.connected_tv);
@@ -30,7 +29,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection"); final title = translate("Connection");
@override @override
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[]; final List<Widget> appBarActions;
@override @override
State<ConnectionPage> createState() => _ConnectionPageState(); State<ConnectionPage> createState() => _ConnectionPageState();
@@ -42,14 +41,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
final _idController = IDTextEditingController(); final _idController = IDTextEditingController();
final RxBool _idEmpty = true.obs; final RxBool _idEmpty = true.obs;
/// Update url. If it's not null, means an update is available.
var _updateUrl = '';
List<Peer> peers = []; List<Peer> peers = [];
bool isPeersLoading = false; bool isPeersLoading = false;
bool isPeersLoaded = false; bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription; StreamSubscription? _uniLinksSubscription;
// https://github.com/flutter/flutter/issues/157244
Iterable<Peer> _autocompleteOpts = [];
_ConnectionPageState() { _ConnectionPageState() {
if (!isWeb) _uniLinksSubscription = listenUniLinks(); if (!isWeb) _uniLinksSubscription = listenUniLinks();
_idController.addListener(() { _idController.addListener(() {
@@ -71,14 +71,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
} }
}); });
} }
if (isAndroid) {
if (!bind.isCustomClient()) {
Timer(const Duration(seconds: 1), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (_updateUrl.isNotEmpty) setState(() {});
});
}
}
} }
@override @override
@@ -88,7 +80,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
slivers: [ slivers: [
SliverList( SliverList(
delegate: SliverChildListDelegate([ delegate: SliverChildListDelegate([
if (!bind.isCustomClient()) _buildUpdateUI(), if (!bind.isCustomClient() && !isIOS)
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
_buildRemoteIDTextField(), _buildRemoteIDTextField(),
])), ])),
SliverFillRemaining( SliverFillRemaining(
@@ -107,16 +100,22 @@ class _ConnectionPageState extends State<ConnectionPage> {
} }
/// UI for software update. /// UI for software update.
/// If [_updateUrl] is not empty, shows a button to update the software. /// If _updateUrl] is not empty, shows a button to update the software.
Widget _buildUpdateUI() { Widget _buildUpdateUI(String updateUrl) {
return _updateUrl.isEmpty return updateUrl.isEmpty
? const SizedBox(height: 0) ? const SizedBox(height: 0)
: InkWell( : InkWell(
onTap: () async { onTap: () async {
final url = 'https://rustdesk.com/download'; final url = 'https://rustdesk.com/download';
if (await canLaunchUrl(Uri.parse(url))) { // https://pub.dev/packages/url_launcher#configuration
await launchUrl(Uri.parse(url)); // https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs
} //
// `await launchUrl(Uri.parse(url))` can also run if skip
// 1. The following check
// 2. `<action android:name="android.support.customtabs.action.CustomTabsService" />` in AndroidManifest.xml
//
// But it is better to add the check.
await launchUrl(Uri.parse(url));
}, },
child: Container( child: Container(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
@@ -160,7 +159,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
child: Autocomplete<Peer>( child: Autocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) { optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') { if (textEditingValue.text == '') {
return const Iterable<Peer>.empty(); _autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) { } else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer( Peer emptyPeer = Peer(
id: '', id: '',
@@ -176,7 +175,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
rdpUsername: '', rdpUsername: '',
loginName: '', loginName: '',
); );
return [emptyPeer]; _autocompleteOpts = [emptyPeer];
} else { } else {
String textWithoutSpaces = String textWithoutSpaces =
textEditingValue.text.replaceAll(" ", ""); textEditingValue.text.replaceAll(" ", "");
@@ -188,7 +187,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
} }
String textToFind = textEditingValue.text.toLowerCase(); String textToFind = textEditingValue.text.toLowerCase();
return peers _autocompleteOpts = peers
.where((peer) => .where((peer) =>
peer.id.toLowerCase().contains(textToFind) || peer.id.toLowerCase().contains(textToFind) ||
peer.username peer.username
@@ -200,12 +199,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
peer.alias.toLowerCase().contains(textToFind)) peer.alias.toLowerCase().contains(textToFind))
.toList(); .toList();
} }
return _autocompleteOpts;
}, },
fieldViewBuilder: (BuildContext context, fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController, TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode, FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) { VoidCallback onFieldSubmitted) {
fieldTextEditingController.text = _idController.text; fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(
fieldTextEditingController);
fieldFocusNode.addListener(() async { fieldFocusNode.addListener(() async {
_idEmpty.value = _idEmpty.value =
fieldTextEditingController.text.isEmpty; fieldTextEditingController.text.isEmpty;
@@ -252,6 +254,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
), ),
), ),
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
onSubmitted: (_) {
onConnect();
},
); );
}, },
onSelected: (option) { onSelected: (option) {
@@ -263,6 +268,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
optionsViewBuilder: (BuildContext context, optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Peer> onSelected, AutocompleteOnSelected<Peer> onSelected,
Iterable<Peer> options) { Iterable<Peer> options) {
options = _autocompleteOpts;
double maxHeight = options.length * 50; double maxHeight = options.length * 50;
if (options.length == 1) { if (options.length == 1) {
maxHeight = 52; maxHeight = 52;
@@ -341,9 +347,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
), ),
), ),
); );
final child = Column(children: [
if (isWebDesktop)
getConnectionPageTitle(context, true)
.marginOnly(bottom: 10, top: 15, left: 12),
w
]);
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Container(constraints: kMobilePageConstraints, child: w)); child: Container(constraints: kMobilePageConstraints, child: child));
} }
@override @override
@@ -353,76 +365,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (Get.isRegistered<IDTextEditingController>()) { if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>(); Get.delete<IDTextEditingController>();
} }
if (Get.isRegistered<TextEditingController>()) {
Get.delete<TextEditingController>();
}
super.dispose(); super.dispose();
} }
} }
class WebMenu extends StatefulWidget {
const WebMenu({Key? key}) : super(key: key);
@override
State<WebMenu> createState() => _WebMenuState();
}
class _WebMenuState extends State<WebMenu> {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
tooltip: "",
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
return (isIOS
? [
const PopupMenuItem(
value: "scan",
child: Icon(Icons.qr_code_scanner, color: Colors.black),
)
]
: <PopupMenuItem<String>>[]) +
[
PopupMenuItem(
value: "server",
child: Text(translate('ID/Relay Server')),
)
] +
[
PopupMenuItem(
value: "login",
child: Text(gFFI.userModel.userName.value.isEmpty
? translate("Login")
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
)
] +
[
PopupMenuItem(
value: "about",
child: Text(translate('About RustDesk')),
)
];
},
onSelected: (value) {
if (value == 'server') {
showServerSettings(gFFI.dialogManager);
}
if (value == 'about') {
showAbout(gFFI.dialogManager);
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
} else {
logOutConfirmDialog();
}
}
if (value == 'scan') {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ScanPage(),
),
);
}
});
}
}

View File

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

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:flutter_hbb/web/settings_page.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/chat_page.dart'; import '../../common/widgets/chat_page.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../models/state_model.dart';
import 'connection_page.dart'; import 'connection_page.dart';
abstract class PageShape extends Widget { abstract class PageShape extends Widget {
@@ -45,7 +47,11 @@ class HomePageState extends State<HomePage> {
void initPages() { void initPages() {
_pages.clear(); _pages.clear();
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); if (!bind.isIncomingOnly()) {
_pages.add(ConnectionPage(
appBarActions: [],
));
}
if (isAndroid && !bind.isOutgoingOnly()) { if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length; _chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
@@ -149,18 +155,80 @@ class HomePageState extends State<HomePage> {
} }
class WebHomePage extends StatelessWidget { class WebHomePage extends StatelessWidget {
final connectionPage = ConnectionPage(); final connectionPage =
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
stateGlobal.isInMainPage = true;
handleUnilink(context);
return Scaffold( return Scaffold(
// backgroundColor: MyTheme.grayBg, // backgroundColor: MyTheme.grayBg,
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text(bind.mainGetAppNameSync()), title: Text("${bind.mainGetAppNameSync()} (Preview)"),
actions: connectionPage.appBarActions, actions: connectionPage.appBarActions,
), ),
body: connectionPage, body: connectionPage,
); );
} }
handleUnilink(BuildContext context) {
if (webInitialLink.isEmpty) {
return;
}
final link = webInitialLink;
webInitialLink = '';
final splitter = ["/#/", "/#", "#/", "#"];
var fakelink = '';
for (var s in splitter) {
if (link.contains(s)) {
var list = link.split(s);
if (list.length < 2 || list[1].isEmpty) {
return;
}
list.removeAt(0);
fakelink = "rustdesk://${list.join(s)}";
break;
}
}
if (fakelink.isEmpty) {
return;
}
final uri = Uri.tryParse(fakelink);
if (uri == null) {
return;
}
final args = urlLinkToCmdArgs(uri);
if (args == null || args.isEmpty) {
return;
}
bool isFileTransfer = false;
String? id;
String? password;
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case '--connect':
case '--play':
isFileTransfer = false;
id = args[i + 1];
i++;
break;
case '--file-transfer':
isFileTransfer = true;
id = args[i + 1];
i++;
break;
case '--password':
password = args[i + 1];
i++;
break;
default:
break;
}
}
if (id != null) {
connect(context, id, isFileTransfer: isFileTransfer, password: password);
}
}
} }

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -25,6 +26,19 @@ import '../widgets/dialog.dart';
final initText = '1' * 1024; 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 RemotePage extends StatefulWidget { class RemotePage extends StatefulWidget {
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
: super(key: key); : super(key: key);
@@ -37,12 +51,15 @@ class RemotePage extends StatefulWidget {
State<RemotePage> createState() => _RemotePageState(id); State<RemotePage> createState() => _RemotePageState(id);
} }
class _RemotePageState extends State<RemotePage> { class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
Timer? _timer; Timer? _timer;
bool _showBar = !isWebDesktop; bool _showBar = !isWebDesktop;
bool _showGestureHelp = false; bool _showGestureHelp = false;
String _value = ''; String _value = '';
Orientation? _currentOrientation; Orientation? _currentOrientation;
double _viewInsetsBottom = 0;
Timer? _timerDidChangeMetrics;
final _blockableOverlayState = BlockableOverlayState(); final _blockableOverlayState = BlockableOverlayState();
@@ -89,10 +106,21 @@ class _RemotePageState extends State<RemotePage> {
gFFI.chatModel gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
_blockableOverlayState.applyFfi(gFFI); _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 @override
Future<void> dispose() async { Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
// https://github.com/flutter/flutter/issues/64935 // https://github.com/flutter/flutter/issues/64935
super.dispose(); super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(store: false); gFFI.dialogManager.hideMobileActionsOverlay(store: false);
@@ -104,6 +132,7 @@ class _RemotePageState extends State<RemotePage> {
_physicalFocusNode.dispose(); _physicalFocusNode.dispose();
await gFFI.close(); await gFFI.close();
_timer?.cancel(); _timer?.cancel();
_timerDidChangeMetrics?.cancel();
gFFI.dialogManager.dismissAll(); gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values); overlays: SystemUiOverlay.values);
@@ -118,6 +147,39 @@ class _RemotePageState extends State<RemotePage> {
gFFI.chatModel.onVoiceCallClosed("End connetion"); 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)
// 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. // 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. // 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. // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
@@ -155,9 +217,9 @@ class _RemotePageState extends State<RemotePage> {
var oldValue = _value; var oldValue = _value;
_value = newValue; _value = newValue;
var i = newValue.length - 1; var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {} for (; i >= 0 && newValue[i] != '1'; --i) {}
var j = oldValue.length - 1; var j = oldValue.length - 1;
for (; j >= 0 && oldValue[j] != '\1'; --j) {} for (; j >= 0 && oldValue[j] != '1'; --j) {}
if (i < j) j = i; if (i < j) j = i;
var subNewValue = newValue.substring(j + 1); var subNewValue = newValue.substring(j + 1);
var subOldValue = oldValue.substring(j + 1); var subOldValue = oldValue.substring(j + 1);
@@ -206,8 +268,8 @@ class _RemotePageState extends State<RemotePage> {
_value = newValue; _value = newValue;
if (oldValue.isNotEmpty && if (oldValue.isNotEmpty &&
newValue.isNotEmpty && newValue.isNotEmpty &&
oldValue[0] == '\1' && oldValue[0] == '1' &&
newValue[0] != '\1') { newValue[0] != '1') {
// clipboard // clipboard
oldValue = ''; oldValue = '';
} }
@@ -511,7 +573,9 @@ class _RemotePageState extends State<RemotePage> {
right: 10, right: 10,
child: QualityMonitor(gFFI.qualityMonitorModel), child: QualityMonitor(gFFI.qualityMonitorModel),
), ),
KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), KeyHelpTools(
keyboardIsVisible: keyboardIsVisible,
showGestureHelp: _showGestureHelp),
SizedBox( SizedBox(
width: 0, width: 0,
height: 0, height: 0,
@@ -531,8 +595,16 @@ class _RemotePageState extends State<RemotePage> {
controller: _textController, controller: _textController,
// trick way to make backspace work always // trick way to make backspace work always
keyboardType: TextInputType.multiline, 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: handleSoftKeyboardInput, onChanged: handleSoftKeyboardInput,
), ).workaroundFreezeLinuxMint(),
), ),
]; ];
if (showCursorPaint) { if (showCursorPaint) {
@@ -740,10 +812,14 @@ class _RemotePageState extends State<RemotePage> {
} }
class KeyHelpTools extends StatefulWidget { class KeyHelpTools extends StatefulWidget {
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] final bool keyboardIsVisible;
final bool requestShow; final bool showGestureHelp;
KeyHelpTools({required this.requestShow}); /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
bool get requestShow => keyboardIsVisible || showGestureHelp;
KeyHelpTools(
{required this.keyboardIsVisible, required this.showGestureHelp});
@override @override
State<KeyHelpTools> createState() => _KeyHelpToolsState(); State<KeyHelpTools> createState() => _KeyHelpToolsState();
@@ -788,7 +864,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
final size = renderObject.size; final size = renderObject.size;
Offset pos = renderObject.localToGlobal(Offset.zero); Offset pos = renderObject.localToGlobal(Offset.zero);
gFFI.cursorModel.keyHelpToolsVisibilityChanged( gFFI.cursorModel.keyHelpToolsVisibilityChanged(
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height)); Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height),
widget.keyboardIsVisible);
} }
} }
@@ -800,13 +877,16 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
inputModel.command; inputModel.command;
if (!_pin && !hasModifierOn && !widget.requestShow) { if (!_pin && !hasModifierOn && !widget.requestShow) {
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null); gFFI.cursorModel
.keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible);
return Offstage(); return Offstage();
} }
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final pi = gFFI.ffiModel.pi; final pi = gFFI.ffiModel.pi;
final isMac = pi.platform == kPeerPlatformMacOS; final isMac = pi.platform == kPeerPlatformMacOS;
final isWin = pi.platform == kPeerPlatformWindows;
final isLinux = pi.platform == kPeerPlatformLinux;
final modifiers = <Widget>[ final modifiers = <Widget>[
wrap('Ctrl ', () { wrap('Ctrl ', () {
setState(() => inputModel.ctrl = !inputModel.ctrl); setState(() => inputModel.ctrl = !inputModel.ctrl);
@@ -887,6 +967,28 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
wrap('PgDn', () { wrap('PgDn', () {
inputModel.inputKey('VK_NEXT'); inputModel.inputKey('VK_NEXT');
}), }),
// to-do: support PrtScr on Mac
if (isWin || isLinux)
wrap('PrtScr', () {
inputModel.inputKey('VK_SNAPSHOT');
}),
if (isWin || isLinux)
wrap('ScrollLock', () {
inputModel.inputKey('VK_SCROLL');
}),
if (isWin || isLinux)
wrap('Pause', () {
inputModel.inputKey('VK_PAUSE');
}),
if (isWin || isLinux)
// Maybe it's better to call it "Menu"
// https://en.wikipedia.org/wiki/Menu_key
wrap('Menu', () {
inputModel.inputKey('Apps');
}),
wrap('Enter', () {
inputModel.inputKey('VK_ENTER');
}),
SizedBox(width: 9999), SizedBox(width: 9999),
wrap('', () { wrap('', () {
inputModel.inputKey('VK_LEFT'); inputModel.inputKey('VK_LEFT');
@@ -937,11 +1039,11 @@ class ImagePaint extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context); final m = Provider.of<ImageModel>(context);
final c = Provider.of<CanvasModel>(context); final c = Provider.of<CanvasModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard();
var s = c.scale; var s = c.scale;
final adjust = c.getAdjustY();
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s), image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
); );
} }
} }
@@ -955,7 +1057,6 @@ class CursorPaint extends StatelessWidget {
final m = Provider.of<CursorModel>(context); final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context); final c = Provider.of<CanvasModel>(context);
final ffiModel = Provider.of<FfiModel>(context); final ffiModel = Provider.of<FfiModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard();
final s = c.scale; final s = c.scale;
double hotx = m.hotx; double hotx = m.hotx;
double hoty = m.hoty; double hoty = m.hoty;
@@ -987,11 +1088,12 @@ class CursorPaint extends StatelessWidget {
factor = s / mins; factor = s / mins;
} }
final s2 = s < mins ? mins : s; final s2 = s < mins ? mins : s;
final adjust = c.getAdjustY();
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: image, image: image,
x: (m.x - hotx) * factor + c.x / s2, x: (m.x - hotx) * factor + c.x / s2,
y: (m.y - hoty) * factor + (c.y - adjust) / s2, y: (m.y - hoty) * factor + (c.y + adjust) / s2,
scale: s2), scale: s2),
); );
} }
@@ -1194,7 +1296,9 @@ void showOptions(
toggles + toggles +
[privacyModeWidget]), [privacyModeWidget]),
); );
}, clickMaskDismiss: true, backDismiss: true); }, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
} }
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
@@ -1213,7 +1317,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
children: children, children: children,
), ),
); );
}, clickMaskDismiss: true, backDismiss: true); }, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
}, },
); );
} }
@@ -1255,7 +1361,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) {
children: children, children: children,
), ),
); );
}, clickMaskDismiss: true, backDismiss: true); }, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
}, },
); );
} }

View File

@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
class _ScanPageState extends State<ScanPage> { class _ScanPageState extends State<ScanPage> {
QRViewController? controller; QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
StreamSubscription? scanSubscription;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
@override @override
void reassemble() { void reassemble() {
super.reassemble(); super.reassemble();
if (isAndroid) { if (isAndroid && controller != null) {
controller!.pauseCamera(); controller!.pauseCamera();
} else if (controller != null) {
controller!.resumeCamera();
} }
controller!.resumeCamera();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Scan QR'), title: const Text('Scan QR'),
actions: [ actions: [
IconButton( _buildImagePickerButton(),
color: Colors.white, _buildFlashToggleButton(),
icon: Icon(Icons.image_search), _buildCameraSwitchButton(),
iconSize: 32.0, ],
onPressed: () async { ),
final ImagePicker picker = ImagePicker(); body: _buildQrView(context),
final XFile? file = );
await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
var image = img.decodeNamedImage(
file.path, File(file.path).readAsBytesSync())!;
LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List());
var bitmap = BinaryBitmap(HybridBinarizer(source));
var reader = QRCodeReader();
try {
var result = reader.decode(bitmap);
if (result.text.startsWith(bind.mainUriPrefixSync())) {
handleUriLink(uriString: result.text);
} else {
showServerSettingFromQr(result.text);
}
} catch (e) {
showToast('No QR code found');
}
}
}),
IconButton(
color: Colors.yellow,
icon: Icon(Icons.flash_on),
iconSize: 32.0,
onPressed: () async {
await controller?.toggleFlash();
}),
IconButton(
color: Colors.white,
icon: Icon(Icons.switch_camera),
iconSize: 32.0,
onPressed: () async {
await controller?.flipCamera();
},
),
],
),
body: _buildQrView(context));
} }
Widget _buildQrView(BuildContext context) { Widget _buildQrView(BuildContext context) {
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly. var scanArea = MediaQuery.of(context).size.width < 400 ||
var scanArea = (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400
MediaQuery.of(context).size.height < 400)
? 150.0 ? 150.0
: 300.0; : 300.0;
// To ensure the Scanner view is properly sizes after rotation
// we need to listen for Flutter SizeChanged notification and update controller
return QRView( return QRView(
key: qrKey, key: qrKey,
onQRViewCreated: _onQRViewCreated, onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape( overlay: QrScannerOverlayShape(
borderColor: Colors.red, borderColor: Colors.red,
borderRadius: 10, borderRadius: 10,
borderLength: 30, borderLength: 30,
borderWidth: 10, borderWidth: 10,
cutOutSize: scanArea), cutOutSize: scanArea,
),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
); );
} }
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
setState(() { setState(() {
this.controller = controller; this.controller = controller;
}); });
controller.scannedDataStream.listen((scanData) { scanSubscription = controller.scannedDataStream.listen((scanData) {
if (scanData.code != null) { if (scanData.code != null) {
showServerSettingFromQr(scanData.code!); showServerSettingFromQr(scanData.code!);
} }
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
} }
} }
Future<void> _pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
try {
var image = img.decodeImage(await File(file.path).readAsBytes())!;
LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(),
);
var bitmap = BinaryBitmap(HybridBinarizer(source));
var reader = QRCodeReader();
var result = reader.decode(bitmap);
if (result.text.startsWith(bind.mainUriPrefixSync())) {
handleUriLink(uriString: result.text);
} else {
showServerSettingFromQr(result.text);
}
} catch (e) {
showToast('No QR code found');
}
}
}
Widget _buildImagePickerButton() {
return IconButton(
color: Colors.white,
icon: Icon(Icons.image_search),
iconSize: 32.0,
onPressed: _pickImage,
);
}
Widget _buildFlashToggleButton() {
return IconButton(
color: Colors.yellow,
icon: Icon(Icons.flash_on),
iconSize: 32.0,
onPressed: () async {
await controller?.toggleFlash();
},
);
}
Widget _buildCameraSwitchButton() {
return IconButton(
color: Colors.white,
icon: Icon(Icons.switch_camera),
iconSize: 32.0,
onPressed: () async {
await controller?.flipCamera();
},
);
}
@override @override
void dispose() { void dispose() {
scanSubscription?.cancel();
controller?.dispose(); controller?.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -446,7 +446,6 @@ class ServerInfo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isPermanent = model.verificationMethod == kUsePermanentPassword;
final serverModel = Provider.of<ServerModel>(context); final serverModel = Provider.of<ServerModel>(context);
const Color colorPositive = Colors.green; const Color colorPositive = Colors.green;
@@ -486,6 +485,8 @@ class ServerInfo extends StatelessWidget {
} }
} }
final showOneTime = serverModel.approveMode != 'click' &&
serverModel.verificationMethod != kUsePermanentPassword;
return PaddingCard( return PaddingCard(
title: translate('Your Device'), title: translate('Your Device'),
child: Column( child: Column(
@@ -523,10 +524,10 @@ class ServerInfo extends StatelessWidget {
]), ]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text( Text(
isPermanent ? '-' : model.serverPasswd.value.text, !showOneTime ? '-' : model.serverPasswd.value.text,
style: textStyleValue, style: textStyleValue,
), ),
isPermanent !showOneTime
? SizedBox.shrink() ? SizedBox.shrink()
: Row(children: [ : Row(children: [
IconButton( IconButton(
@@ -595,7 +596,9 @@ class _PermissionCheckerState extends State<PermissionChecker> {
translate("android_version_audio_tip"), translate("android_version_audio_tip"),
style: const TextStyle(color: MyTheme.darkGray), style: const TextStyle(color: MyTheme.darkGray),
)) ))
]) ]),
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
])); ]));
} }
} }

View File

@@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:settings_ui/settings_ui.dart'; import 'package:settings_ui/settings_ui.dart';
@@ -70,6 +71,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
false; //androidVersion >= 26; // remove because not work on every device false; //androidVersion >= 26; // remove because not work on every device
var _ignoreBatteryOpt = false; var _ignoreBatteryOpt = false;
var _enableStartOnBoot = false; var _enableStartOnBoot = false;
var _checkUpdateOnStartup = false;
var _floatingWindowDisabled = false; var _floatingWindowDisabled = false;
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
var _enableAbr = false; var _enableAbr = false;
@@ -79,6 +81,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _enableRecordSession = false; var _enableRecordSession = false;
var _enableHardwareCodec = false; var _enableHardwareCodec = false;
var _autoRecordIncomingSession = false; var _autoRecordIncomingSession = false;
var _autoRecordOutgoingSession = false;
var _allowAutoDisconnect = false; var _allowAutoDisconnect = false;
var _localIP = ""; var _localIP = "";
var _directAccessPort = ""; var _directAccessPort = "";
@@ -104,6 +107,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
bind.mainGetOptionSync(key: kOptionEnableHwcodec)); bind.mainGetOptionSync(key: kOptionEnableHwcodec));
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming, _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming)); bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr'); _localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort); _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect, _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
@@ -151,6 +156,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_enableStartOnBoot = enableStartOnBoot; _enableStartOnBoot = enableStartOnBoot;
} }
var checkUpdateOnStartup =
mainGetLocalBoolOptionSync(kOptionEnableCheckUpdate);
if (checkUpdateOnStartup != _checkUpdateOnStartup) {
update = true;
_checkUpdateOnStartup = checkUpdateOnStartup;
}
var floatingWindowDisabled = var floatingWindowDisabled =
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" || bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
!await AndroidPermissionManager.check(kSystemAlertWindow); !await AndroidPermissionManager.check(kSystemAlertWindow);
@@ -231,6 +243,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Provider.of<FfiModel>(context); Provider.of<FfiModel>(context);
final outgoingOnly = bind.isOutgoingOnly(); final outgoingOnly = bind.isOutgoingOnly();
final incommingOnly = bind.isIncomingOnly();
final customClientSection = CustomSettingsSection( final customClientSection = CustomSettingsSection(
child: Column( child: Column(
children: [ children: [
@@ -548,6 +561,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
})); }));
if (!bind.isCustomClient()) {
enhancementsTiles.add(
SettingsTile.switchTile(
initialValue: _checkUpdateOnStartup,
title:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(translate('Check for software update on startup')),
]),
onToggle: (bool toValue) async {
await mainSetLocalBoolOption(kOptionEnableCheckUpdate, toValue);
setState(() => _checkUpdateOnStartup = toValue);
},
),
);
}
onFloatingWindowChanged(bool toValue) async { onFloatingWindowChanged(bool toValue) async {
if (toValue) { if (toValue) {
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
@@ -674,32 +703,55 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
}, },
), ),
]), ]),
if (isAndroid && !outgoingOnly) if (isAndroid)
SettingsSection( SettingsSection(
title: Text(translate("Recording")), title: Text(translate("Recording")),
tiles: [ tiles: [
SettingsTile.switchTile( if (!outgoingOnly)
title: SettingsTile.switchTile(
Text(translate('Automatically record incoming sessions')), title:
leading: Icon(Icons.videocam), Text(translate('Automatically record incoming sessions')),
description: Text( initialValue: _autoRecordIncomingSession,
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"), onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
initialValue: _autoRecordIncomingSession, ? null
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) : (v) async {
? null await bind.mainSetOption(
: (v) async { key: kOptionAllowAutoRecordIncoming,
await bind.mainSetOption( value: bool2option(
key: kOptionAllowAutoRecordIncoming, kOptionAllowAutoRecordIncoming, v));
value: final newValue = option2bool(
bool2option(kOptionAllowAutoRecordIncoming, v)); kOptionAllowAutoRecordIncoming,
final newValue = option2bool( await bind.mainGetOption(
kOptionAllowAutoRecordIncoming, key: kOptionAllowAutoRecordIncoming));
await bind.mainGetOption( setState(() {
key: kOptionAllowAutoRecordIncoming)); _autoRecordIncomingSession = newValue;
setState(() { });
_autoRecordIncomingSession = newValue; },
}); ),
}, if (!incommingOnly)
SettingsTile.switchTile(
title:
Text(translate('Automatically record outgoing sessions')),
initialValue: _autoRecordOutgoingSession,
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
? null
: (v) async {
await bind.mainSetLocalOption(
key: kOptionAllowAutoRecordOutgoing,
value: bool2option(
kOptionAllowAutoRecordOutgoing, v));
final newValue = option2bool(
kOptionAllowAutoRecordOutgoing,
bind.mainGetLocalOption(
key: kOptionAllowAutoRecordOutgoing));
setState(() {
_autoRecordOutgoingSession = newValue;
});
},
),
SettingsTile(
title: Text(translate("Directory")),
description: Text(bind.mainVideoSaveDirectory(root: false)),
), ),
], ],
), ),
@@ -730,9 +782,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
tiles: [ tiles: [
SettingsTile( SettingsTile(
onPressed: (context) async { onPressed: (context) async {
if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url));
await launchUrl(Uri.parse(url));
}
}, },
title: Text(translate("Version: ") + version), title: Text(translate("Version: ") + version),
value: Padding( value: Padding(
@@ -801,11 +851,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
} }
} }
void showServerSettings(OverlayDialogManager dialogManager) async {
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
}
void showLanguageSettings(OverlayDialogManager dialogManager) async { void showLanguageSettings(OverlayDialogManager dialogManager) async {
try { try {
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>; final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
@@ -881,9 +926,7 @@ void showAbout(OverlayDialogManager dialogManager) {
InkWell( InkWell(
onTap: () async { onTap: () async {
const url = 'https://rustdesk.com/'; const url = 'https://rustdesk.com/';
if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url));
await launchUrl(Uri.parse(url));
}
}, },
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: 8),

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart';
@@ -65,7 +66,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
? null ? null
: translate('Too short, at least 6 characters.'); : translate('Too short, at least 6 characters.');
}, },
), ).workaroundFreezeLinuxMint(),
TextFormField( TextFormField(
obscureText: true, obscureText: true,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
@@ -84,7 +85,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
? null ? null
: translate('The confirmation is not identical.'); : translate('The confirmation is not identical.');
}, },
), ).workaroundFreezeLinuxMint(),
])), ])),
onCancel: close, onCancel: close,
onSubmit: (validateLength && validateSame) ? submit : null, onSubmit: (validateLength && validateSame) ? submit : null,
@@ -146,6 +147,16 @@ void setTemporaryPasswordLengthDialog(
}, backDismiss: true, clickMaskDismiss: true); }, backDismiss: true, clickMaskDismiss: true);
} }
void showServerSettings(OverlayDialogManager dialogManager) async {
Map<String, dynamic> options = {};
try {
options = jsonDecode(await bind.mainGetOptions());
} catch (e) {
print("Invalid server config: $e");
}
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
}
void showServerSettingsWithValue( void showServerSettingsWithValue(
ServerConfig serverConfig, OverlayDialogManager dialogManager) async { ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
var isInProgress = false; var isInProgress = false;
@@ -184,6 +195,43 @@ void showServerSettingsWithValue(
return ret; return ret;
} }
Widget buildField(
String label, TextEditingController controller, String errorMsg,
{String? Function(String?)? validator, bool autofocus = false}) {
if (isDesktop || isWeb) {
return Row(
children: [
SizedBox(
width: 120,
child: Text(label),
),
SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: controller,
decoration: InputDecoration(
errorText: errorMsg.isEmpty ? null : errorMsg,
contentPadding:
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
),
validator: validator,
autofocus: autofocus,
).workaroundFreezeLinuxMint(),
),
],
);
}
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: label,
errorText: errorMsg.isEmpty ? null : errorMsg,
),
validator: validator,
).workaroundFreezeLinuxMint();
}
return CustomAlertDialog( return CustomAlertDialog(
title: Row( title: Row(
children: [ children: [
@@ -191,55 +239,45 @@ void showServerSettingsWithValue(
...ServerConfigImportExportWidgets(controllers, errMsgs), ...ServerConfigImportExportWidgets(controllers, errMsgs),
], ],
), ),
content: Form( content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Form(
child: Obx(() => Column( child: Obx(() => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: [
TextFormField( buildField(translate('ID Server'), idCtrl, idServerMsg.value,
controller: idCtrl, autofocus: true),
decoration: InputDecoration( SizedBox(height: 8),
labelText: translate('ID Server'), if (!isIOS && !isWeb) ...[
errorText: idServerMsg.value.isEmpty buildField(translate('Relay Server'), relayCtrl,
? null relayServerMsg.value),
: idServerMsg.value), SizedBox(height: 8),
) ],
] + buildField(
[ translate('API Server'),
TextFormField( apiCtrl,
controller: relayCtrl, apiServerMsg.value,
decoration: InputDecoration( validator: (v) {
labelText: translate('Relay Server'), if (v != null && v.isNotEmpty) {
errorText: relayServerMsg.value.isEmpty if (!(v.startsWith('http://') ||
? null v.startsWith("https://"))) {
: relayServerMsg.value), return translate("invalid_http");
)
] +
[
TextFormField(
controller: apiCtrl,
decoration: InputDecoration(
labelText: translate('API Server'),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (v) {
if (v != null && v.isNotEmpty) {
if (!(v.startsWith('http://') ||
v.startsWith("https://"))) {
return translate("invalid_http");
}
} }
return null; }
}, return null;
},
),
SizedBox(height: 8),
buildField('Key', keyCtrl, ''),
if (isInProgress)
Padding(
padding: EdgeInsets.only(top: 8),
child: LinearProgressIndicator(),
), ),
TextFormField( ],
controller: keyCtrl, )),
decoration: InputDecoration( ),
labelText: 'Key', ),
),
),
// NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(),
]))),
actions: [ actions: [
dialogButton('Cancel', onPressed: () { dialogButton('Cancel', onPressed: () {
close(); close();

View File

@@ -33,6 +33,8 @@ bool filterAbTagByIntersection() {
const _personalAddressBookName = "My address book"; const _personalAddressBookName = "My address book";
const _legacyAddressBookName = "Legacy address book"; const _legacyAddressBookName = "Legacy address book";
const kUntagged = "Untagged";
enum ForcePullAb { enum ForcePullAb {
listAndCurrent, listAndCurrent,
current, current,
@@ -66,10 +68,16 @@ class AbModel {
var listInitialized = false; var listInitialized = false;
var _maxPeerOneAb = 0; var _maxPeerOneAb = 0;
late final Peers peersModel;
WeakReference<FFI> parent; WeakReference<FFI> parent;
AbModel(this.parent) { AbModel(this.parent) {
addressbooks.clear(); addressbooks.clear();
peersModel = Peers(
name: PeersModelName.addressBook,
getInitPeers: () => currentAbPeers,
loadEvent: LoadEvent.addressBook);
if (desktopType == DesktopType.main) { if (desktopType == DesktopType.main) {
Timer.periodic(Duration(milliseconds: 500), (timer) async { Timer.periodic(Duration(milliseconds: 500), (timer) async {
if (_timerCounter++ % 6 == 0) { if (_timerCounter++ % 6 == 0) {
@@ -418,6 +426,7 @@ class AbModel {
// #region tags // #region tags
Future<bool> addTags(List<String> tagList) async { Future<bool> addTags(List<String> tagList) async {
tagList.removeWhere((e) => e == kUntagged);
final ret = await current.addTags(tagList, {}); final ret = await current.addTags(tagList, {});
await pullNonLegacyAfterChange(); await pullNonLegacyAfterChange();
_saveCache(); _saveCache();
@@ -639,6 +648,9 @@ class AbModel {
} }
Color getCurrentAbTagColor(String tag) { Color getCurrentAbTagColor(String tag) {
if (tag == kUntagged) {
return MyTheme.accent;
}
int? colorValue = current.tagColors[tag]; int? colorValue = current.tagColors[tag];
if (colorValue != null) { if (colorValue != null) {
return Color(colorValue); return Color(colorValue);

View File

@@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier {
} }
} }
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || _isChatOverlayHide() =>
chatWindowOverlayEntry == null); ((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
chatWindowOverlayEntry == null);
toggleChatOverlay({Offset? chatInitPos}) { toggleChatOverlay({Offset? chatInitPos}) {
if (_isChatOverlayHide()) { if (_isChatOverlayHide()) {
gFFI.invokeMethod("enable_soft_keyboard", true); gFFI.invokeMethod("enable_soft_keyboard", true);
if (!isDesktop) { if (!(isDesktop || isWebDesktop)) {
showChatIconOverlay(); showChatIconOverlay();
} }
showChatWindowOverlay(chatInitPos: chatInitPos); showChatWindowOverlay(chatInitPos: chatInitPos);

View File

@@ -181,6 +181,7 @@ class TextureModel {
} }
updateCurrentDisplay(int curDisplay) { updateCurrentDisplay(int curDisplay) {
if (isWeb) return;
final ffi = parent.target; final ffi = parent.target;
if (ffi == null) return; if (ffi == null) return;
tryCreateTexture(int idx) { tryCreateTexture(int idx) {

View File

@@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:flutter_hbb/utils/event_loop.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../consts.dart'; import '../consts.dart';
import 'model.dart'; import 'model.dart';
@@ -34,6 +36,7 @@ class JobID {
} }
typedef GetSessionID = SessionID Function(); typedef GetSessionID = SessionID Function();
typedef GetDialogManager = OverlayDialogManager? Function();
class FileModel { class FileModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
@@ -45,13 +48,15 @@ class FileModel {
late final FileController remoteController; late final FileController remoteController;
late final GetSessionID getSessionID; late final GetSessionID getSessionID;
late final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
late final FileDialogEventLoop evtLoop; late final FileDialogEventLoop evtLoop;
FileModel(this.parent) { FileModel(this.parent) {
getSessionID = () => parent.target!.sessionId; getSessionID = () => parent.target!.sessionId;
getDialogManager = () => parent.target?.dialogManager;
fileFetcher = FileFetcher(getSessionID); fileFetcher = FileFetcher(getSessionID);
jobController = JobController(getSessionID); jobController = JobController(getSessionID, getDialogManager);
localController = FileController( localController = FileController(
isLocal: true, isLocal: true,
getSessionID: getSessionID, getSessionID: getSessionID,
@@ -71,7 +76,7 @@ class FileModel {
Future<void> onReady() async { Future<void> onReady() async {
await evtLoop.onReady(); await evtLoop.onReady();
await localController.onReady(); if (!isWeb) await localController.onReady();
await remoteController.onReady(); await remoteController.onReady();
} }
@@ -83,7 +88,7 @@ class FileModel {
} }
Future<void> refreshAll() async { Future<void> refreshAll() async {
await localController.refresh(); if (!isWeb) await localController.refresh();
await remoteController.refresh(); await remoteController.refresh();
} }
@@ -95,6 +100,10 @@ class FileModel {
fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
} }
void receiveEmptyDirs(Map<String, dynamic> evt) {
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
}
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async { Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
evtLoop.pushEvent( evtLoop.pushEvent(
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
@@ -225,6 +234,54 @@ class FileModel {
); );
}, useAnimation: false); }, useAnimation: false);
} }
void onSelectedFiles(dynamic obj) {
localController.selectedItems.clear();
try {
int handleIndex = int.parse(obj['handleIndex']);
final file = jsonDecode(obj['file']);
var entry = Entry.fromJson(file);
entry.path = entry.name;
final otherSideData = remoteController.directoryData();
final toPath = otherSideData.directory.path;
final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden;
final jobID = jobController.addTransferJob(entry, false);
webSendLocalFiles(
handleIndex: handleIndex,
actId: jobID,
path: entry.path,
to: PathUtil.join(toPath, entry.name, isWindows),
fileNum: 0,
includeHidden: showHidden,
isRemote: false,
);
} catch (e) {
debugPrint("Failed to decode onSelectedFiles: $e");
}
}
void sendEmptyDirs(dynamic obj) {
late final List<dynamic> emptyDirs;
try {
emptyDirs = jsonDecode(obj['dirs'] as String);
} catch (e) {
debugPrint("Failed to decode sendEmptyDirs: $e");
}
final otherSideData = remoteController.directoryData();
final toPath = otherSideData.directory.path;
final isPeerWindows = otherSideData.options.isWindows;
final isLocalWindows = isWindows || isWebOnWindows;
for (var dir in emptyDirs) {
if (isLocalWindows != isPeerWindows) {
dir = PathUtil.convert(dir, isLocalWindows, isPeerWindows);
}
var peerPath = PathUtil.join(toPath, dir, isPeerWindows);
remoteController.createDirWithRemote(peerPath, true);
}
}
} }
class DirectoryData { class DirectoryData {
@@ -438,7 +495,8 @@ class FileController {
} }
/// sendFiles from current side (FileController.isLocal) to other side (SelectedItems). /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems).
void sendFiles(SelectedItems items, DirectoryData otherSideData) { Future<void> sendFiles(
SelectedItems items, DirectoryData otherSideData) async {
/// ignore wrong items side status /// ignore wrong items side status
if (items.isLocal != isLocal) { if (items.isLocal != isLocal) {
return; return;
@@ -451,7 +509,7 @@ class FileController {
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
for (var from in items.items) { for (var from in items.items) {
final jobID = jobController.add(from, isRemoteToLocal); final jobID = jobController.addTransferJob(from, isRemoteToLocal);
bind.sessionSendFiles( bind.sessionSendFiles(
sessionId: sessionId, sessionId: sessionId,
actId: jobID, actId: jobID,
@@ -459,10 +517,48 @@ class FileController {
to: PathUtil.join(toPath, from.name, isWindows), to: PathUtil.join(toPath, from.name, isWindows),
fileNum: 0, fileNum: 0,
includeHidden: showHidden, includeHidden: showHidden,
isRemote: isRemoteToLocal); isRemote: isRemoteToLocal,
isDir: from.isDirectory);
debugPrint( debugPrint(
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
} }
if (isWeb ||
(!isLocal &&
versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0)) {
return;
}
final List<Entry> entrys = items.items.toList();
var isRemote = isLocal == true ? true : false;
await Future.forEach(entrys, (Entry item) async {
if (!item.isDirectory) {
return;
}
final List<String> paths = [];
final emptyDirs =
await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden);
if (emptyDirs.isEmpty) {
return;
} else {
for (var dir in emptyDirs) {
paths.add(dir.path);
}
}
final dirs = paths.map((path) {
return PathUtil.getOtherSidePath(directory.value.path, path,
options.value.isWindows, toPath, isWindows);
});
for (var dir in dirs) {
createDirWithRemote(dir, isRemote);
}
});
} }
bool _removeCheckboxRemember = false; bool _removeCheckboxRemember = false;
@@ -486,7 +582,7 @@ class FileController {
} else if (item.isDirectory) { } else if (item.isDirectory) {
title = translate("Not an empty directory"); title = translate("Not an empty directory");
dialogManager?.showLoading(translate("Waiting")); dialogManager?.showLoading(translate("Waiting"));
final fd = await fileFetcher.fetchDirectoryRecursive( final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
jobID, item.path, items.isLocal, true); jobID, item.path, items.isLocal, true);
if (fd.path.isEmpty) { if (fd.path.isEmpty) {
fd.path = item.path; fd.path = item.path;
@@ -494,13 +590,21 @@ class FileController {
fd.format(isWindows); fd.format(isWindows);
dialogManager?.dismissAll(); dialogManager?.dismissAll();
if (fd.entries.isEmpty) { if (fd.entries.isEmpty) {
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
final confirm = await showRemoveDialog( final confirm = await showRemoveDialog(
translate( translate(
"Are you sure you want to delete this empty directory?"), "Are you sure you want to delete this empty directory?"),
item.name, item.name,
false); false);
if (confirm == true) { if (confirm == true) {
sendRemoveEmptyDir(item.path, 0); sendRemoveEmptyDir(
item.path,
0,
deleteJobId,
);
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel", state: JobState.done);
} }
return; return;
} }
@@ -508,6 +612,13 @@ class FileController {
} else { } else {
entries = []; entries = [];
} }
int deleteJobId;
if (item.isDirectory) {
deleteJobId =
jobController.addDeleteDirJob(item, !isLocal, entries.length);
} else {
deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
}
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
final dirShow = item.isDirectory final dirShow = item.isDirectory
@@ -522,24 +633,32 @@ class FileController {
); );
try { try {
if (confirm == true) { if (confirm == true) {
sendRemoveFile(entries[i].path, i); sendRemoveFile(entries[i].path, i, deleteJobId);
final res = await jobController.jobResultListener.start(); final res = await jobController.jobResultListener.start();
// handle remove res; // handle remove res;
if (item.isDirectory && if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) { res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i); sendRemoveEmptyDir(item.path, i, deleteJobId);
} }
} else {
jobController.updateJobStatus(deleteJobId,
file_num: i, error: "cancel");
} }
if (_removeCheckboxRemember) { if (_removeCheckboxRemember) {
if (confirm == true) { if (confirm == true) {
for (var j = i + 1; j < entries.length; j++) { for (var j = i + 1; j < entries.length; j++) {
sendRemoveFile(entries[j].path, j); sendRemoveFile(entries[j].path, j, deleteJobId);
final res = await jobController.jobResultListener.start(); final res = await jobController.jobResultListener.start();
if (item.isDirectory && if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) { res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i); sendRemoveEmptyDir(item.path, i, deleteJobId);
} }
} }
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel",
file_num: entries.length,
state: JobState.done);
} }
break; break;
} }
@@ -618,30 +737,31 @@ class FileController {
}, useAnimation: false); }, useAnimation: false);
} }
void sendRemoveFile(String path, int fileNum) { void sendRemoveFile(String path, int fileNum, int actId) {
bind.sessionRemoveFile( bind.sessionRemoveFile(
sessionId: sessionId, sessionId: sessionId,
actId: JobController.jobID.next(), actId: actId,
path: path, path: path,
isRemote: !isLocal, isRemote: !isLocal,
fileNum: fileNum); fileNum: fileNum);
} }
void sendRemoveEmptyDir(String path, int fileNum) { void sendRemoveEmptyDir(String path, int fileNum, int actId) {
history.removeWhere((element) => element.contains(path)); history.removeWhere((element) => element.contains(path));
bind.sessionRemoveAllEmptyDirs( bind.sessionRemoveAllEmptyDirs(
sessionId: sessionId, sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
actId: JobController.jobID.next(),
path: path,
isRemote: !isLocal);
} }
Future<void> createDir(String path) async { Future<void> createDirWithRemote(String path, bool isRemote) async {
bind.sessionCreateDir( bind.sessionCreateDir(
sessionId: sessionId, sessionId: sessionId,
actId: JobController.jobID.next(), actId: JobController.jobID.next(),
path: path, path: path,
isRemote: !isLocal); isRemote: isRemote);
}
Future<void> createDir(String path) async {
await createDirWithRemote(path, !isLocal);
} }
Future<void> renameAction(Entry item, bool isLocal) async { Future<void> renameAction(Entry item, bool isLocal) async {
@@ -716,27 +836,29 @@ class FileController {
} }
} }
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
class JobController { class JobController {
static final JobID jobID = JobID(); static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs; final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>(); final jobResultListener = JobResultListener<Map<String, dynamic>>();
final GetSessionID getSessionID; final GetSessionID getSessionID;
final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
OverlayDialogManager? get alogManager => getDialogManager();
int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
JobController(this.getSessionID); JobController(this.getSessionID, this.getDialogManager);
int getJob(int id) { int getJob(int id) {
return jobTable.indexWhere((element) => element.id == id); return jobTable.indexWhere((element) => element.id == id);
} }
// JobProgress? getJob(int id) {
// return jobTable.firstWhere((element) => element.id == id);
// }
// return jobID // return jobID
int add(Entry from, bool isRemoteToLocal) { int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next(); final jobID = JobController.jobID.next();
jobTable.add(JobProgress() jobTable.add(JobProgress()
..type = JobType.transfer
..fileName = path.basename(from.path) ..fileName = path.basename(from.path)
..jobName = from.path ..jobName = from.path
..totalSize = from.size ..totalSize = from.size
@@ -746,6 +868,33 @@ class JobController {
return jobID; return jobID;
} }
int addDeleteFileJob(Entry file, bool isRemote) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.deleteFile
..fileName = path.basename(file.path)
..jobName = file.path
..totalSize = file.size
..state = JobState.none
..id = jobID
..isRemoteToLocal = isRemote);
return jobID;
}
int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.deleteDir
..fileName = path.basename(file.path)
..jobName = file.path
..fileCount = fileCount
..totalSize = file.size
..state = JobState.none
..id = jobID
..isRemoteToLocal = isRemote);
return jobID;
}
void tryUpdateJobProgress(Map<String, dynamic> evt) { void tryUpdateJobProgress(Map<String, dynamic> evt) {
try { try {
int id = int.parse(evt['id']); int id = int.parse(evt['id']);
@@ -756,7 +905,7 @@ class JobController {
job.fileNum = int.parse(evt['file_num']); job.fileNum = int.parse(evt['file_num']);
job.speed = double.parse(evt['speed']); job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']); job.finishedSize = int.parse(evt['finished_size']);
debugPrint("update job $id with $evt"); job.recvJobRes = true;
jobTable.refresh(); jobTable.refresh();
} }
} catch (e) { } catch (e) {
@@ -764,20 +913,48 @@ class JobController {
} }
} }
void jobDone(Map<String, dynamic> evt) async { Future<bool> jobDone(Map<String, dynamic> evt) async {
if (jobResultListener.isListening) { if (jobResultListener.isListening) {
jobResultListener.complete(evt); jobResultListener.complete(evt);
return; // return;
} }
int id = -1;
int id = int.parse(evt['id']); int? fileNum = 0;
double? speed = 0;
try {
id = int.parse(evt['id']);
} catch (_) {}
final jobIndex = getJob(id); final jobIndex = getJob(id);
if (jobIndex != -1) { if (jobIndex == -1) return true;
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.finishedSize = job.totalSize; job.recvJobRes = true;
if (job.type == JobType.deleteFile) {
job.state = JobState.done; job.state = JobState.done;
job.fileNum = int.parse(evt['file_num']); } else if (job.type == JobType.deleteDir) {
jobTable.refresh(); try {
fileNum = int.tryParse(evt['file_num']);
} catch (_) {}
if (fileNum != null) {
if (fileNum < job.fileNum) return true; // file_num can be 0 at last
job.fileNum = fileNum;
if (fileNum >= job.fileCount - 1) {
job.state = JobState.done;
}
}
} else {
try {
fileNum = int.tryParse(evt['file_num']);
speed = double.tryParse(evt['speed']);
} catch (_) {}
if (fileNum != null) job.fileNum = fileNum;
if (speed != null) job.speed = speed;
job.state = JobState.done;
}
jobTable.refresh();
if (job.type == JobType.deleteDir) {
return job.state == JobState.done;
} else {
return true;
} }
} }
@@ -788,16 +965,61 @@ class JobController {
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.state = JobState.error; job.state = JobState.error;
job.err = err; job.err = err;
job.fileNum = int.parse(evt['file_num']); job.recvJobRes = true;
if (err == "skipped") { if (job.type == JobType.transfer) {
job.state = JobState.done; int? fileNum = int.tryParse(evt['file_num']);
job.finishedSize = job.totalSize; if (fileNum != null) job.fileNum = fileNum;
if (err == "skipped") {
job.state = JobState.done;
job.finishedSize = job.totalSize;
}
} else if (job.type == JobType.deleteDir) {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
}
int? fileNum = int.tryParse(evt['file_num']);
if (fileNum != null) job.fileNum = fileNum;
} else if (job.type == JobType.deleteFile) {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
}
} }
jobTable.refresh(); jobTable.refresh();
} }
if (err == _kOneWayFileTransferError) {
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
final dm = alogManager;
if (dm != null) {
_lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm);
}
}
}
debugPrint("jobError $evt"); debugPrint("jobError $evt");
} }
void updateJobStatus(int id,
{int? file_num, String? error, JobState? state}) {
final jobIndex = getJob(id);
if (jobIndex < 0) return;
final job = jobTable[jobIndex];
job.recvJobRes = true;
if (file_num != null) {
job.fileNum = file_num;
}
if (error != null) {
job.err = error;
job.state = JobState.error;
}
if (state != null) {
job.state = state;
}
if (job.type == JobType.deleteFile && error == null) {
job.state = JobState.done;
}
jobTable.refresh();
}
Future<void> cancelJob(int id) async { Future<void> cancelJob(int id) async {
await bind.sessionCancelJob(sessionId: sessionId, actId: id); await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} }
@@ -814,6 +1036,7 @@ class JobController {
final currJobId = JobController.jobID.next(); final currJobId = JobController.jobID.next();
String fileName = path.basename(isRemote ? remote : to); String fileName = path.basename(isRemote ? remote : to);
var jobProgress = JobProgress() var jobProgress = JobProgress()
..type = JobType.transfer
..fileName = fileName ..fileName = fileName
..jobName = isRemote ? remote : to ..jobName = isRemote ? remote : to
..id = currJobId ..id = currJobId
@@ -908,6 +1131,7 @@ class JobResultListener<T> {
class FileFetcher { class FileFetcher {
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync // Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
Map<String, Completer<FileDirectory>> remoteTasks = {}; Map<String, Completer<FileDirectory>> remoteTasks = {};
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
Map<int, Completer<FileDirectory>> readRecursiveTasks = {}; Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
final GetSessionID getSessionID; final GetSessionID getSessionID;
@@ -915,6 +1139,24 @@ class FileFetcher {
FileFetcher(this.getSessionID); FileFetcher(this.getSessionID);
Future<List<FileDirectory>> registerReadEmptyDirsTask(
bool isLocal, String path) {
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
final tasks = remoteEmptyDirsTasks; // bypass now
if (tasks.containsKey(path)) {
throw "Failed to registerReadEmptyDirsTask, already have same read job";
}
final c = Completer<List<FileDirectory>>();
tasks[path] = c;
Timer(Duration(seconds: 2), () {
tasks.remove(path);
if (c.isCompleted) return;
c.completeError("Failed to read empty dirs, timeout");
});
return c.future;
}
Future<FileDirectory> registerReadTask(bool isLocal, String path) { Future<FileDirectory> registerReadTask(bool isLocal, String path) {
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
final tasks = remoteTasks; // bypass now final tasks = remoteTasks; // bypass now
@@ -948,6 +1190,25 @@ class FileFetcher {
return c.future; return c.future;
} }
tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return;
late final Map<String, Completer<List<FileDirectory>>> tasks;
try {
final map = jsonDecode(msg);
final String path = map["path"];
final List<dynamic> fdJsons = map["empty_dirs"];
final List<FileDirectory> fds =
fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
tasks = remoteEmptyDirsTasks;
final completer = tasks.remove(path);
completer?.complete(fds);
} catch (e) {
debugPrint("tryCompleteJob err: $e");
}
}
tryCompleteTask(String? msg, String? isLocalStr) { tryCompleteTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return; if (msg == null || isLocalStr == null) return;
late final Map<Object, Completer<FileDirectory>> tasks; late final Map<Object, Completer<FileDirectory>> tasks;
@@ -971,6 +1232,28 @@ class FileFetcher {
} }
} }
Future<List<FileDirectory>> readEmptyDirs(
String path, bool isLocal, bool showHidden) async {
try {
if (isLocal) {
final res = await bind.sessionReadLocalEmptyDirsRecursiveSync(
sessionId: sessionId, path: path, includeHidden: showHidden);
final List<dynamic> fdJsons = jsonDecode(res);
final List<FileDirectory> fds =
fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
return fds;
} else {
await bind.sessionReadRemoteEmptyDirsRecursiveSync(
sessionId: sessionId, path: path, includeHidden: showHidden);
return registerReadEmptyDirsTask(isLocal, path);
}
} catch (e) {
return Future.error(e);
}
}
Future<FileDirectory> fetchDirectory( Future<FileDirectory> fetchDirectory(
String path, bool isLocal, bool showHidden) async { String path, bool isLocal, bool showHidden) async {
try { try {
@@ -989,11 +1272,11 @@ class FileFetcher {
} }
} }
Future<FileDirectory> fetchDirectoryRecursive( Future<FileDirectory> fetchDirectoryRecursiveToRemove(
int actID, String path, bool isLocal, bool showHidden) async { int actID, String path, bool isLocal, bool showHidden) async {
// TODO test Recursive is show hidden default? // TODO test Recursive is show hidden default?
try { try {
await bind.sessionReadDirRecursive( await bind.sessionReadDirToRemoveRecursive(
sessionId: sessionId, sessionId: sessionId,
actId: actID, actId: actID,
path: path, path: path,
@@ -1088,8 +1371,12 @@ extension JobStateDisplay on JobState {
} }
} }
enum JobType { none, transfer, deleteFile, deleteDir }
class JobProgress { class JobProgress {
JobType type = JobType.none;
JobState state = JobState.none; JobState state = JobState.none;
var recvJobRes = false;
var id = 0; var id = 0;
var fileNum = 0; var fileNum = 0;
var speed = 0.0; var speed = 0.0;
@@ -1109,7 +1396,9 @@ class JobProgress {
int lastTransferredSize = 0; int lastTransferredSize = 0;
clear() { clear() {
type = JobType.none;
state = JobState.none; state = JobState.none;
recvJobRes = false;
id = 0; id = 0;
fileNum = 0; fileNum = 0;
speed = 0; speed = 0;
@@ -1123,11 +1412,81 @@ class JobProgress {
} }
String display() { String display() {
if (state == JobState.done && err == "skipped") { if (type == JobType.transfer) {
return translate("Skipped"); if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
} else if (type == JobType.deleteFile) {
if (err == "cancel") {
return translate("Cancel");
}
} }
return state.display(); return state.display();
} }
String getStatus() {
int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
if (handledFileCount >= fileCount) {
handledFileCount = fileCount;
}
if (state == JobState.done) {
handledFileCount = fileCount;
finishedSize = totalSize;
}
final filesStr = "$handledFileCount/$fileCount files";
final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
final sizePercentStr = totalSize > 0 && finishedSize > 0
? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
: "";
if (type == JobType.deleteFile) {
return display();
} else if (type == JobType.deleteDir) {
var res = '';
if (state == JobState.done || state == JobState.error) {
res = display();
}
if (filesStr.isNotEmpty) {
if (res.isNotEmpty) {
res += " ";
}
res += filesStr;
}
if (sizeStr.isNotEmpty) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizeStr;
}
return res;
} else if (type == JobType.transfer) {
var res = "";
if (state != JobState.inProgress && state != JobState.none) {
res += display();
}
if (filesStr.isNotEmpty) {
if (res.isNotEmpty) {
res += ", ";
}
res += filesStr;
}
if (sizeStr.isNotEmpty && state != JobState.inProgress) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizeStr;
}
if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizePercentStr;
}
return res;
}
return '';
}
} }
class _PathStat { class _PathStat {
@@ -1141,6 +1500,24 @@ class PathUtil {
static final windowsContext = path.Context(style: path.Style.windows); static final windowsContext = path.Context(style: path.Style.windows);
static final posixContext = path.Context(style: path.Style.posix); static final posixContext = path.Context(style: path.Style.posix);
static String getOtherSidePath(String mainRootPath, String mainPath,
bool isMainWindows, String otherRootPath, bool isOtherWindows) {
final mainPathUtil = isMainWindows ? windowsContext : posixContext;
final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath);
final names = mainPathUtil.split(relativePath);
final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
String path = otherRootPath;
for (var name in names) {
path = otherPathUtil.join(path, name);
}
return path;
}
static String join(String path1, String path2, bool isWindows) { static String join(String path1, String path2, bool isWindows) {
final pathUtil = isWindows ? windowsContext : posixContext; final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.join(path1, path2); return pathUtil.join(path1, path2);
@@ -1151,6 +1528,12 @@ class PathUtil {
return pathUtil.split(path); return pathUtil.split(path);
} }
static String convert(String path, bool isMainWindows, bool isOtherWindows) {
final mainPathUtil = isMainWindows ? windowsContext : posixContext;
final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
return otherPathUtil.joinAll(mainPathUtil.split(path));
}
static String dirname(String path, bool isWindows) { static String dirname(String path, bool isWindows) {
final pathUtil = isWindows ? windowsContext : posixContext; final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.dirname(path); return pathUtil.dirname(path);

View File

@@ -23,7 +23,14 @@ class GroupModel {
bool get emtpy => users.isEmpty && peers.isEmpty; bool get emtpy => users.isEmpty && peers.isEmpty;
GroupModel(this.parent); late final Peers peersModel;
GroupModel(this.parent) {
peersModel = Peers(
name: PeersModelName.group,
getInitPeers: () => peers,
loadEvent: LoadEvent.group);
}
Future<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
if (bind.isDisableGroupPanel()) return; if (bind.isDisableGroupPanel()) return;

View File

@@ -177,7 +177,7 @@ class PointerEventToRust {
} }
} }
class ToReleaseKeys { class ToReleaseRawKeys {
RawKeyEvent? lastLShiftKeyEvent; RawKeyEvent? lastLShiftKeyEvent;
RawKeyEvent? lastRShiftKeyEvent; RawKeyEvent? lastRShiftKeyEvent;
RawKeyEvent? lastLCtrlKeyEvent; RawKeyEvent? lastLCtrlKeyEvent;
@@ -282,6 +282,48 @@ class ToReleaseKeys {
} }
} }
class ToReleaseKeys {
KeyEvent? lastLShiftKeyEvent;
KeyEvent? lastRShiftKeyEvent;
KeyEvent? lastLCtrlKeyEvent;
KeyEvent? lastRCtrlKeyEvent;
KeyEvent? lastLAltKeyEvent;
KeyEvent? lastRAltKeyEvent;
KeyEvent? lastLCommandKeyEvent;
KeyEvent? lastRCommandKeyEvent;
KeyEvent? lastSuperKeyEvent;
reset() {
lastLShiftKeyEvent = null;
lastRShiftKeyEvent = null;
lastLCtrlKeyEvent = null;
lastRCtrlKeyEvent = null;
lastLAltKeyEvent = null;
lastRAltKeyEvent = null;
lastLCommandKeyEvent = null;
lastRCommandKeyEvent = null;
lastSuperKeyEvent = null;
}
release(KeyEventResult Function(KeyEvent e) handleKeyEvent) {
for (final key in [
lastLShiftKeyEvent,
lastRShiftKeyEvent,
lastLCtrlKeyEvent,
lastRCtrlKeyEvent,
lastLAltKeyEvent,
lastRAltKeyEvent,
lastLCommandKeyEvent,
lastRCommandKeyEvent,
lastSuperKeyEvent,
]) {
if (key != null) {
handleKeyEvent(key);
}
}
}
}
class InputModel { class InputModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
String keyboardMode = ''; String keyboardMode = '';
@@ -292,6 +334,7 @@ class InputModel {
var alt = false; var alt = false;
var command = false; var command = false;
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
final ToReleaseKeys toReleaseKeys = ToReleaseKeys(); final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
// trackpad // trackpad
@@ -339,10 +382,99 @@ class InputModel {
} }
} }
void handleKeyDownEventModifiers(KeyEvent e) {
KeyUpEvent upEvent(e) => KeyUpEvent(
physicalKey: e.physicalKey,
logicalKey: e.logicalKey,
timeStamp: e.timeStamp,
);
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
if (!alt) {
alt = true;
}
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
if (!alt) {
alt = true;
}
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
if (!ctrl) {
ctrl = true;
}
toReleaseKeys.lastLCtrlKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
if (!ctrl) {
ctrl = true;
}
toReleaseKeys.lastRCtrlKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
if (!shift) {
shift = true;
}
toReleaseKeys.lastLShiftKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
if (!shift) {
shift = true;
}
toReleaseKeys.lastRShiftKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
if (!command) {
command = true;
}
toReleaseKeys.lastLCommandKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
if (!command) {
command = true;
}
toReleaseKeys.lastRCommandKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
if (!command) {
command = true;
}
toReleaseKeys.lastSuperKeyEvent = upEvent(e);
}
}
void handleKeyUpEventModifiers(KeyEvent e) {
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
alt = false;
toReleaseKeys.lastLAltKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
alt = false;
toReleaseKeys.lastRAltKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
ctrl = false;
toReleaseKeys.lastLCtrlKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
ctrl = false;
toReleaseKeys.lastRCtrlKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
shift = false;
toReleaseKeys.lastLShiftKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
shift = false;
toReleaseKeys.lastRShiftKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
command = false;
toReleaseKeys.lastLCommandKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
command = false;
toReleaseKeys.lastRCommandKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
command = false;
toReleaseKeys.lastSuperKeyEvent = null;
}
}
KeyEventResult handleRawKeyEvent(RawKeyEvent e) { KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled; if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { if (!isInputSourceFlutter) {
return KeyEventResult.handled; if (isDesktop) {
return KeyEventResult.handled;
} else if (isWeb) {
return KeyEventResult.ignored;
}
} }
final key = e.logicalKey; final key = e.logicalKey;
@@ -358,7 +490,7 @@ class InputModel {
command = true; command = true;
} }
} }
toReleaseKeys.updateKeyDown(key, e); toReleaseRawKeys.updateKeyDown(key, e);
} }
if (e is RawKeyUpEvent) { if (e is RawKeyUpEvent) {
if (key == LogicalKeyboardKey.altLeft || if (key == LogicalKeyboardKey.altLeft ||
@@ -376,12 +508,82 @@ class InputModel {
command = false; command = false;
} }
toReleaseKeys.updateKeyUp(key, e); toReleaseRawKeys.updateKeyUp(key, e);
} }
// * Currently mobile does not enable map mode // * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardMode(e); mapKeyboardModeRaw(e);
} else {
legacyKeyboardModeRaw(e);
}
return KeyEventResult.handled;
}
KeyEventResult handleKeyEvent(KeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (!isInputSourceFlutter) {
if (isDesktop) {
return KeyEventResult.handled;
} else if (isWeb) {
return KeyEventResult.ignored;
}
}
if (isWindows || isLinux) {
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
e.physicalKey == PhysicalKeyboardKey.metaRight) {
return KeyEventResult.handled;
}
}
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
handleKeyDownEventModifiers(e);
}
bool isMobileAndMapMode = false;
if (isMobile) {
// Do not use map mode if mobile -> Android. Android does not support map mode for now.
// Because simulating the physical key events(uhid) which requires root permission is not supported.
if (peerPlatform != kPeerPlatformAndroid) {
if (isIOS) {
isMobileAndMapMode = true;
} else {
// The physicalKey.usbHidUsage may be not correct for soft keyboard on Android.
// iOS does not have this issue.
// 1. Open the soft keyboard on Android
// 2. Switch to input method like zh/ko/ja
// 3. Click Backspace and Enter on the soft keyboard or physical keyboard
// 4. The physicalKey.usbHidUsage is not correct.
// PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042")
// LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter")
//
// The correct PhysicalKeyboardKey should be
// PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter")
// https://github.com/flutter/flutter/issues/157771
// We cannot use the debugName to determine the key is correct or not, because it's null in release mode.
// The normal `usbHidUsage` for keyboard shoud be between [0x00000010, 0x000c029f]
// https://github.com/flutter/flutter/blob/c051b69e2a2224300e20d93dbd15f4b91e8844d1/packages/flutter/lib/src/services/keyboard_key.g.dart#L5332 - 5600
final isNormalHsbHidUsage = (e.physicalKey.usbHidUsage >> 20) == 0;
isMobileAndMapMode = isNormalHsbHidUsage &&
// No need to check `!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel)`
// But we still add it for more reliability.
!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel);
}
}
}
final isDesktopAndMapMode =
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
if (isMobileAndMapMode || isDesktopAndMapMode) {
// FIXME: e.character is wrong for dead keys, eg: ^ in de
newKeyboardMode(
e.character ?? '',
e.physicalKey.usbHidUsage & 0xFFFF,
// Show repeat event be converted to "release+press" events?
e is KeyDownEvent || e is KeyRepeatEvent);
} else { } else {
legacyKeyboardMode(e); legacyKeyboardMode(e);
} }
@@ -389,7 +591,33 @@ class InputModel {
return KeyEventResult.handled; return KeyEventResult.handled;
} }
void mapKeyboardMode(RawKeyEvent e) { /// Send Key Event
void newKeyboardMode(String character, int usbHid, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
bind.sessionHandleFlutterKeyEvent(
sessionId: sessionId,
character: character,
usbHid: usbHid,
lockModes: lockModes,
downOrUp: down);
}
void mapKeyboardModeRaw(RawKeyEvent e) {
int positionCode = -1; int positionCode = -1;
int platformCode = -1; int platformCode = -1;
bool down; bool down;
@@ -441,7 +669,7 @@ class InputModel {
.contains(KeyboardLockMode.scrollLock)) { .contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock); lockModes |= (1 << scrolllock);
} }
bind.sessionHandleFlutterKeyEvent( bind.sessionHandleFlutterRawKeyEvent(
sessionId: sessionId, sessionId: sessionId,
name: name, name: name,
platformCode: platformCode, platformCode: platformCode,
@@ -450,7 +678,7 @@ class InputModel {
downOrUp: down); downOrUp: down);
} }
void legacyKeyboardMode(RawKeyEvent e) { void legacyKeyboardModeRaw(RawKeyEvent e) {
if (e is RawKeyDownEvent) { if (e is RawKeyDownEvent) {
if (e.repeat) { if (e.repeat) {
sendRawKey(e, press: true); sendRawKey(e, press: true);
@@ -471,6 +699,24 @@ class InputModel {
inputKey(label, down: down, press: press ?? false); inputKey(label, down: down, press: press ?? false);
} }
void legacyKeyboardMode(KeyEvent e) {
if (e is KeyDownEvent) {
sendKey(e, down: true);
} else if (e is KeyRepeatEvent) {
sendKey(e, press: true);
} else if (e is KeyUpEvent) {
sendKey(e);
}
}
void sendKey(KeyEvent e, {bool? down, bool? press}) {
// for maximum compatibility
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
logicalKeyMap[e.logicalKey.keyId] ??
e.logicalKey.keyLabel;
inputKey(label, down: down, press: press ?? false);
}
/// Send key stroke event. /// Send key stroke event.
/// [down] indicates the key's state(down or up). /// [down] indicates the key's state(down or up).
/// [press] indicates a click event(down and up). /// [press] indicates a click event(down and up).
@@ -522,22 +768,22 @@ class InputModel {
} }
/// Send a mouse tap event(down and up). /// Send a mouse tap event(down and up).
void tap(MouseButtons button) { Future<void> tap(MouseButtons button) async {
sendMouse('down', button); await sendMouse('down', button);
sendMouse('up', button); await sendMouse('up', button);
} }
void tapDown(MouseButtons button) { Future<void> tapDown(MouseButtons button) async {
sendMouse('down', button); await sendMouse('down', button);
} }
void tapUp(MouseButtons button) { Future<void> tapUp(MouseButtons button) async {
sendMouse('up', button); await sendMouse('up', button);
} }
/// Send scroll event with scroll distance [y]. /// Send scroll event with scroll distance [y].
void scroll(int y) { Future<void> scroll(int y) async {
bind.sessionSendMouse( await bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
msg: json msg: json
.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); .encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()})));
@@ -558,15 +804,16 @@ class InputModel {
} }
/// Send mouse press event. /// Send mouse press event.
void sendMouse(String type, MouseButtons button) { Future<void> sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return; if (!keyboardPerm) return;
bind.sessionSendMouse( await bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value}))); msg: json.encode(modify({'type': type, 'buttons': button.value})));
} }
void enterOrLeave(bool enter) { void enterOrLeave(bool enter) {
toReleaseKeys.release(handleRawKeyEvent); toReleaseKeys.release(handleKeyEvent);
toReleaseRawKeys.release(handleRawKeyEvent);
_pointerMovedAfterEnter = false; _pointerMovedAfterEnter = false;
// Fix status // Fix status
@@ -577,14 +824,17 @@ class InputModel {
if (!isInputSourceFlutter) { if (!isInputSourceFlutter) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
} }
if (!isWeb && enter) {
bind.setCurSessionId(sessionId: sessionId);
}
} }
/// Send mouse movement event with distance in [x] and [y]. /// Send mouse movement event with distance in [x] and [y].
void moveMouse(double x, double y) { Future<void> moveMouse(double x, double y) async {
if (!keyboardPerm) return; if (!keyboardPerm) return;
var x2 = x.toInt(); var x2 = x.toInt();
var y2 = y.toInt(); var y2 = y.toInt();
bind.sessionSendMouse( await bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
} }
@@ -606,7 +856,7 @@ class InputModel {
_stopFling = true; _stopFling = true;
if (isViewOnly) return; if (isViewOnly) return;
if (peerPlatform == kPeerPlatformAndroid) { if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position); handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
} }
} }
@@ -649,8 +899,8 @@ class InputModel {
} }
if (x != 0 || y != 0) { if (x != 0 || y != 0) {
if (peerPlatform == kPeerPlatformAndroid) { if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent( handlePointerEvent('touch', kMouseEventTypePanUpdate,
'touch', 'pan_update', Offset(x.toDouble(), y.toDouble())); Offset(x.toDouble(), y.toDouble()));
} else { } else {
bind.sessionSendMouse( bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
@@ -712,7 +962,7 @@ class InputModel {
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (peerPlatform == kPeerPlatformAndroid) { if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_end', e.position); handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
return; return;
} }
@@ -830,7 +1080,7 @@ class InputModel {
onExit: true, onExit: true,
); );
int trySetNearestRange(int v, int min, int max, int n) { static double tryGetNearestRange(double v, double min, double max, double n) {
if (v < min && v >= min - n) { if (v < min && v >= min - n) {
v = min; v = min;
} }
@@ -870,13 +1120,13 @@ class InputModel {
// to-do: handle mouse events // to-do: handle mouse events
late final dynamic evtValue; late final dynamic evtValue;
if (type == 'pan_update') { if (type == kMouseEventTypePanUpdate) {
evtValue = { evtValue = {
'x': x.toInt(), 'x': x.toInt(),
'y': y.toInt(), 'y': y.toInt(),
}; };
} else { } else {
final isMoveTypes = ['pan_start', 'pan_end']; final isMoveTypes = [kMouseEventTypePanStart, kMouseEventTypePanEnd];
final pos = handlePointerDevicePos( final pos = handlePointerDevicePos(
kPointerEventKindTouch, kPointerEventKindTouch,
x, x,
@@ -888,8 +1138,8 @@ class InputModel {
return; return;
} }
evtValue = { evtValue = {
'x': pos.x, 'x': pos.x.toInt(),
'y': pos.y, 'y': pos.y.toInt(),
}; };
} }
@@ -931,14 +1181,14 @@ class InputModel {
return; return;
} }
var type = ''; var type = kMouseEventTypeDefault;
var isMove = false; var isMove = false;
switch (evt['type']) { switch (evt['type']) {
case _kMouseEventDown: case _kMouseEventDown:
type = 'down'; type = kMouseEventTypeDown;
break; break;
case _kMouseEventUp: case _kMouseEventUp:
type = 'up'; type = kMouseEventTypeUp;
break; break;
case _kMouseEventMove: case _kMouseEventMove:
_pointerMovedAfterEnter = true; _pointerMovedAfterEnter = true;
@@ -949,7 +1199,7 @@ class InputModel {
} }
evt['type'] = type; evt['type'] = type;
if (type == 'down' && !_pointerMovedAfterEnter) { if (type == kMouseEventTypeDown && !_pointerMovedAfterEnter) {
// Move mouse to the position of the down event first. // Move mouse to the position of the down event first.
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
refreshMousePos(); refreshMousePos();
@@ -971,8 +1221,8 @@ class InputModel {
evt['x'] = '0'; evt['x'] = '0';
evt['y'] = '0'; evt['y'] = '0';
} else { } else {
evt['x'] = '${pos.x}'; evt['x'] = '${pos.x.toInt()}';
evt['y'] = '${pos.y}'; evt['y'] = '${pos.y.toInt()}';
} }
Map<int, String> mapButtons = { Map<int, String> mapButtons = {
@@ -1112,33 +1362,56 @@ class InputModel {
y = pos.dy; y = pos.dy;
} }
var evtX = 0; return InputModel.getPointInRemoteRect(
var evtY = 0; true, peerPlatform, kind, evtType, x, y, rect,
try { buttons: buttons);
evtX = x.round(); }
evtY = y.round();
} catch (e) {
debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
return null;
}
int minX = rect.left.toInt(); static Point<double>? getPointInRemoteRect(
bool isLocalDesktop,
String? peerPlatform,
String kind,
String evtType,
double evtX,
double evtY,
Rect rect,
{int buttons = kPrimaryMouseButton}) {
double minX = rect.left;
// https://github.com/rustdesk/rustdesk/issues/6678 // https://github.com/rustdesk/rustdesk/issues/6678
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping. // For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
int maxX = (rect.left + rect.width).toInt() - double maxX = (rect.left + rect.width) -
(peerPlatform == kPeerPlatformWindows ? 0 : 1); (peerPlatform == kPeerPlatformWindows ? 0 : 1);
int minY = rect.top.toInt(); double minY = rect.top;
int maxY = (rect.top + rect.height).toInt() - double maxY = (rect.top + rect.height) -
(peerPlatform == kPeerPlatformWindows ? 0 : 1); (peerPlatform == kPeerPlatformWindows ? 0 : 1);
evtX = trySetNearestRange(evtX, minX, maxX, 5); evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5); evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5);
if (kind == kPointerEventKindMouse) { if (isLocalDesktop) {
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { if (kind == kPointerEventKindMouse) {
// If left mouse up, no early return. if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
if (!(buttons == kPrimaryMouseButton && evtType == 'up')) { // If left mouse up, no early return.
return null; if (!(buttons == kPrimaryMouseButton &&
evtType == kMouseEventTypeUp)) {
return null;
}
} }
} }
} else {
bool evtXInRange = evtX >= minX && evtX <= maxX;
bool evtYInRange = evtY >= minY && evtY <= maxY;
if (!(evtXInRange || evtYInRange)) {
return null;
}
if (evtX < minX) {
evtX = minX;
} else if (evtX > maxX) {
evtX = maxX;
}
if (evtY < minY) {
evtY = minY;
} else if (evtY > maxY) {
evtY = maxY;
}
} }
return Point(evtX, evtY); return Point(evtX, evtY);
@@ -1164,15 +1437,15 @@ class InputModel {
// Simulate a key press event. // Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key. // `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async { Future<void> tapHidKey(int usbHidUsage) async {
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true); newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
await Future.delayed(Duration(milliseconds: 100)); await Future.delayed(Duration(milliseconds: 100));
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false); newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
} }
Future<void> onMobileVolumeUp() async => Future<void> onMobileVolumeUp() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
Future<void> onMobileVolumeDown() async => Future<void> onMobileVolumeDown() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
Future<void> onMobilePower() async => Future<void> onMobilePower() async =>
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
} }

View File

@@ -4,15 +4,20 @@ import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart'; import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_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/peer_tab_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/user_model.dart';
@@ -139,6 +144,7 @@ class FfiModel with ChangeNotifier {
bool get touchMode => _touchMode; bool get touchMode => _touchMode;
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
bool get isPeerMobile => isPeerAndroid;
bool get viewOnly => _viewOnly; bool get viewOnly => _viewOnly;
@@ -267,6 +273,8 @@ class FfiModel with ChangeNotifier {
var name = evt['name']; var name = evt['name'];
if (name == 'msgbox') { if (name == 'msgbox') {
handleMsgBox(evt, sessionId, peerId); handleMsgBox(evt, sessionId, peerId);
} else if (name == 'toast') {
handleToast(evt, sessionId, peerId);
} else if (name == 'set_multiple_windows_session') { } else if (name == 'set_multiple_windows_session') {
handleMultipleWindowsSession(evt, sessionId, peerId); handleMultipleWindowsSession(evt, sessionId, peerId);
} else if (name == 'peer_info') { } else if (name == 'peer_info') {
@@ -301,11 +309,18 @@ class FfiModel with ChangeNotifier {
.receive(int.parse(evt['id'] as String), evt['text'] ?? ''); .receive(int.parse(evt['id'] as String), evt['text'] ?? '');
} else if (name == 'file_dir') { } else if (name == 'file_dir') {
parent.target?.fileModel.receiveFileDir(evt); parent.target?.fileModel.receiveFileDir(evt);
} else if (name == 'empty_dirs') {
parent.target?.fileModel.receiveEmptyDirs(evt);
} else if (name == 'job_progress') { } else if (name == 'job_progress') {
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
} else if (name == 'job_done') { } else if (name == 'job_done') {
parent.target?.fileModel.jobController.jobDone(evt); bool? refresh =
parent.target?.fileModel.refreshAll(); await parent.target?.fileModel.jobController.jobDone(evt);
if (refresh == true) {
// many job done for delete directory
// todo: refresh may not work when confirm delete local directory
parent.target?.fileModel.refreshAll();
}
} else if (name == 'job_error') { } else if (name == 'job_error') {
parent.target?.fileModel.jobController.jobError(evt); parent.target?.fileModel.jobController.jobError(evt);
} else if (name == 'override_file_confirm') { } else if (name == 'override_file_confirm') {
@@ -365,7 +380,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'plugin_option') { } else if (name == 'plugin_option') {
handleOption(evt); handleOption(evt);
} else if (name == "sync_peer_hash_password_to_personal_ab") { } else if (name == "sync_peer_hash_password_to_personal_ab") {
if (desktopType == DesktopType.main) { if (desktopType == DesktopType.main || isWeb || isMobile) {
final id = evt['id']; final id = evt['id'];
final hash = evt['hash']; final hash = evt['hash'];
if (id != null && hash != null) { if (id != null && hash != null) {
@@ -383,6 +398,18 @@ class FfiModel with ChangeNotifier {
handleFollowCurrentDisplay(evt, sessionId, peerId); handleFollowCurrentDisplay(evt, sessionId, peerId);
} else if (name == 'use_texture_render') { } else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId); _handleUseTextureRender(evt, sessionId, peerId);
} else if (name == "selected_files") {
if (isWeb) {
parent.target?.fileModel.onSelectedFiles(evt);
}
} else if (name == "send_emptry_dirs") {
if (isWeb) {
parent.target?.fileModel.sendEmptyDirs(evt);
}
} else if (name == "record_status") {
if (desktopType == DesktopType.remote || isMobile) {
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
}
} else { } else {
debugPrint('Event is not handled in the fixed branch: $name'); debugPrint('Event is not handled in the fixed branch: $name');
} }
@@ -492,10 +519,12 @@ class FfiModel with ChangeNotifier {
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
newDisplay.originalWidth = newDisplay.originalWidth = int.tryParse(
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
newDisplay.originalHeight = kInvalidResolutionValue;
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; newDisplay.originalHeight = int.tryParse(
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
kInvalidResolutionValue;
newDisplay._scale = _pi.scaleOfDisplay(display); newDisplay._scale = _pi.scaleOfDisplay(display);
_pi.displays[display] = newDisplay; _pi.displays[display] = newDisplay;
@@ -511,7 +540,6 @@ class FfiModel with ChangeNotifier {
} }
} }
parent.target?.recordingModel.onSwitchDisplay();
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) { if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
handleResolutions(peerId, evt['resolutions']); handleResolutions(peerId, evt['resolutions']);
} }
@@ -582,13 +610,44 @@ class FfiModel with ChangeNotifier {
} }
} }
handleToast(Map<String, dynamic> evt, SessionID sessionId, String peerId) {
final type = evt['type'] ?? 'info';
final text = evt['text'] ?? '';
final durMsc = evt['dur_msec'] ?? 2000;
final duration = Duration(milliseconds: durMsc);
if ((text).isEmpty) {
BotToast.showLoading(
duration: duration,
clickClose: true,
allowClick: true,
);
} else {
if (type.contains('error')) {
BotToast.showText(
contentColor: Colors.red,
text: translate(text),
duration: duration,
clickClose: true,
onlyOne: true,
);
} else {
BotToast.showText(
text: translate(text),
duration: duration,
clickClose: true,
onlyOne: true,
);
}
}
}
/// Show a message box with [type], [title] and [text]. /// Show a message box with [type], [title] and [text].
showMsgBox(SessionID sessionId, String type, String title, String text, showMsgBox(SessionID sessionId, String type, String title, String text,
String link, bool hasRetry, OverlayDialogManager dialogManager, String link, bool hasRetry, OverlayDialogManager dialogManager,
{bool? hasCancel}) { {bool? hasCancel}) {
msgBox(sessionId, type, title, text, link, dialogManager, msgBox(sessionId, type, title, text, link, dialogManager,
hasCancel: hasCancel, hasCancel: hasCancel,
reconnect: reconnect, reconnect: hasRetry ? reconnect : null,
reconnectTimeout: hasRetry ? _reconnects : null); reconnectTimeout: hasRetry ? _reconnects : null);
_timer?.cancel(); _timer?.cancel();
if (hasRetry) { if (hasRetry) {
@@ -788,7 +847,7 @@ class FfiModel with ChangeNotifier {
isRefreshing = false; isRefreshing = false;
} }
Map<String, dynamic> features = json.decode(evt['features']); Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1; _pi.features.privacyMode = features['privacy_mode'] == true;
if (!isCache) { if (!isCache) {
handleResolutions(peerId, evt["resolutions"]); handleResolutions(peerId, evt["resolutions"]);
} }
@@ -832,7 +891,7 @@ class FfiModel with ChangeNotifier {
for (final mode in [kKeyMapMode, kKeyLegacyMode]) { for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
if (bind.sessionIsKeyboardModeSupported( if (bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: mode)) { sessionId: sessionId, mode: mode)) {
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
break; break;
} }
} }
@@ -1088,8 +1147,6 @@ class FfiModel with ChangeNotifier {
// Directly switch to the new display without waiting for the response. // Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId, switchToNewDisplay(int display, SessionID sessionId, String peerId,
{bool updateCursorPos = false}) { {bool updateCursorPos = false}) {
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
parent.target?.recordingModel.onClose();
// no need to wait for the response // no need to wait for the response
pi.currentDisplay = display; pi.currentDisplay = display;
updateCurDisplay(sessionId, updateCursorPos: updateCursorPos); updateCurDisplay(sessionId, updateCursorPos: updateCursorPos);
@@ -1178,6 +1235,27 @@ class ImageModel with ChangeNotifier {
clearImage() => _image = null; clearImage() => _image = null;
bool _webDecodingRgba = false;
final List<Uint8List> _webRgbaList = List.empty(growable: true);
webOnRgba(int display, Uint8List rgba) async {
// deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer"
_webRgbaList.add(Uint8List.fromList(rgba));
if (_webDecodingRgba) {
return;
}
_webDecodingRgba = true;
try {
while (_webRgbaList.isNotEmpty) {
final rgba2 = _webRgbaList.last;
_webRgbaList.clear();
await decodeAndUpdate(display, rgba2);
}
} catch (e) {
debugPrint('onRgba error: $e');
}
_webDecodingRgba = false;
}
onRgba(int display, Uint8List rgba) async { onRgba(int display, Uint8List rgba) async {
try { try {
await decodeAndUpdate(display, rgba); await decodeAndUpdate(display, rgba);
@@ -1194,7 +1272,9 @@ class ImageModel with ChangeNotifier {
rgba, rgba,
rect?.width.toInt() ?? 0, rect?.width.toInt() ?? 0,
rect?.height.toInt() ?? 0, rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, isWeb | isWindows | isLinux
? ui.PixelFormat.rgba8888
: ui.PixelFormat.bgra8888,
); );
if (parent.target?.id != pid) return; if (parent.target?.id != pid) return;
await update(image); await update(image);
@@ -1205,13 +1285,6 @@ class ImageModel with ChangeNotifier {
if (isDesktop || isWebDesktop) { if (isDesktop || isWebDesktop) {
await parent.target?.canvasModel.updateViewStyle(); await parent.target?.canvasModel.updateViewStyle();
await parent.target?.canvasModel.updateScrollStyle(); await parent.target?.canvasModel.updateScrollStyle();
} else {
final size = MediaQueryData.fromWindow(ui.window).size;
final canvasWidth = size.width;
final canvasHeight = size.height;
final xscale = canvasWidth / image.width;
final yscale = canvasHeight / image.height;
parent.target?.canvasModel.scale = min(xscale, yscale);
} }
if (parent.target != null) { if (parent.target != null) {
await initializeCursorAndCanvas(parent.target!); await initializeCursorAndCanvas(parent.target!);
@@ -1223,20 +1296,18 @@ class ImageModel with ChangeNotifier {
} }
// mobile only // mobile only
// for desktop, height should minus tabbar height
double get maxScale { double get maxScale {
if (_image == null) return 1.5; if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size; final size = parent.target!.canvasModel.getSize();
final xscale = size.width / _image!.width; final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return max(1.5, max(xscale, yscale)); return max(1.5, max(xscale, yscale));
} }
// mobile only // mobile only
// for desktop, height should minus tabbar height
double get minScale { double get minScale {
if (_image == null) return 1.5; if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size; final size = parent.target!.canvasModel.getSize();
final xscale = size.width / _image!.width; final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return min(xscale, yscale) / 1.5; return min(xscale, yscale) / 1.5;
@@ -1359,6 +1430,12 @@ class CanvasModel with ChangeNotifier {
ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
Timer? _timerMobileFocusCanvasCursor;
// `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method
// after showing the soft keyboard.
bool isMobileCanvasChanged = false;
final ScrollController _horizontal = ScrollController(); final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController(); final ScrollController _vertical = ScrollController();
@@ -1401,21 +1478,37 @@ class CanvasModel with ChangeNotifier {
static double get bottomToEdge => static double get bottomToEdge =>
isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0; isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0;
updateViewStyle({refreshMousePos = true}) async { Size getSize() {
Size getSize() { final mediaData = MediaQueryData.fromView(ui.window);
final size = MediaQueryData.fromWindow(ui.window).size; final size = mediaData.size;
// If minimized, w or h may be negative here. // If minimized, w or h may be negative here.
double w = size.width - leftToEdge - rightToEdge; double w = size.width - leftToEdge - rightToEdge;
double h = size.height - topToEdge - bottomToEdge; double h = size.height - topToEdge - bottomToEdge;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); if (isMobile) {
h = h -
mediaData.viewInsets.bottom -
(parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ??
0);
} }
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
// mobile only
double getAdjustY() {
final bottom =
parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? 0;
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); final style = await bind.sessionGetViewStyle(sessionId: sessionId);
if (style == null) { if (style == null) {
return; return;
} }
_size = getSize(); updateSize();
final displayWidth = getDisplayWidth(); final displayWidth = getDisplayWidth();
final displayHeight = getDisplayHeight(); final displayHeight = getDisplayHeight();
final viewStyle = ViewStyle( final viewStyle = ViewStyle(
@@ -1438,16 +1531,25 @@ class CanvasModel with ChangeNotifier {
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / _devicePixelRatio; _scale = 1.0 / _devicePixelRatio;
} }
_x = (size.width - displayWidth * _scale) / 2; _resetCanvasOffset(displayWidth, displayHeight);
_y = (size.height - displayHeight * _scale) / 2;
_imageOverflow.value = _x < 0 || y < 0; _imageOverflow.value = _x < 0 || y < 0;
notifyListeners(); if (notify) {
if (refreshMousePos) { notifyListeners();
}
if (!isMobile && refreshMousePos) {
parent.target?.inputModel.refreshMousePos(); parent.target?.inputModel.refreshMousePos();
} }
tryUpdateScrollStyle(Duration.zero, style); tryUpdateScrollStyle(Duration.zero, style);
} }
_resetCanvasOffset(int displayWidth, int displayHeight) {
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
if (isMobile) {
_moveToCenterCursor();
}
}
tryUpdateScrollStyle(Duration duration, String? style) async { tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return; if (_scrollStyle != ScrollStyle.scrollbar) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId); style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
@@ -1551,6 +1653,9 @@ class CanvasModel with ChangeNotifier {
panX(double dx) { panX(double dx) {
_x += dx; _x += dx;
if (isMobile) {
isMobileCanvasChanged = true;
}
notifyListeners(); notifyListeners();
} }
@@ -1558,17 +1663,20 @@ class CanvasModel with ChangeNotifier {
if (isWebDesktop) { if (isWebDesktop) {
updateViewStyle(); updateViewStyle();
} else { } else {
_x = (size.width - getDisplayWidth() * _scale) / 2; _resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
_y = (size.height - getDisplayHeight() * _scale) / 2;
} }
notifyListeners(); notifyListeners();
} }
panY(double dy) { panY(double dy) {
_y += dy; _y += dy;
if (isMobile) {
isMobileCanvasChanged = true;
}
notifyListeners(); notifyListeners();
} }
// mobile only
updateScale(double v, Offset focalPoint) { updateScale(double v, Offset focalPoint) {
if (parent.target?.imageModel.image == null) return; if (parent.target?.imageModel.image == null) return;
final s = _scale; final s = _scale;
@@ -1580,21 +1688,34 @@ class CanvasModel with ChangeNotifier {
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2 // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale; _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
final adjustForKeyboard = final adjust = getAdjustY();
parent.target?.cursorModel.adjustForKeyboard() ?? 0.0; // (focalPoint.dy - _y_1 - adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 - adjust) / s2 + displayOriginY
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY // _y_2 = focalPoint.dy - adjust - (focalPoint.dy - _y_1 - adjust) / s1 * s2
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2 _y = focalPoint.dy - adjust - (focalPoint.dy - _y - adjust) / s * _scale;
_y = focalPoint.dy + if (isMobile) {
adjustForKeyboard - isMobileCanvasChanged = true;
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale; }
notifyListeners(); notifyListeners();
} }
clear([bool notify = false]) { // For reset canvas to the last view style
reset() {
_scale = _lastViewStyle.scale;
_devicePixelRatio = ui.window.devicePixelRatio;
if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) {
_scale = 1.0 / _devicePixelRatio;
}
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style);
notifyListeners();
}
clear() {
_x = 0; _x = 0;
_y = 0; _y = 0;
_scale = 1.0; _scale = 1.0;
if (notify) notifyListeners(); _lastViewStyle = ViewStyle.defaultViewStyle();
_timerMobileFocusCanvasCursor?.cancel();
} }
updateScrollPercent() { updateScrollPercent() {
@@ -1612,6 +1733,43 @@ class CanvasModel with ChangeNotifier {
: 0.0; : 0.0;
setScrollPercent(percentX, percentY); setScrollPercent(percentX, percentY);
} }
void mobileFocusCanvasCursor() {
_timerMobileFocusCanvasCursor?.cancel();
_timerMobileFocusCanvasCursor =
Timer(Duration(milliseconds: 100), () async {
updateSize();
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
notifyListeners();
});
}
// mobile only
// Move the canvas to make the cursor visible(center) on the screen.
void _moveToCenterCursor() {
Rect? imageRect = parent.target?.ffiModel.rect;
if (imageRect == null) {
// unreachable
return;
}
final maxX = 0.0;
final minX = _size.width + (imageRect.left - imageRect.right) * _scale;
final maxY = 0.0;
final minY = _size.height + (imageRect.top - imageRect.bottom) * _scale;
Offset offsetToCenter =
parent.target?.cursorModel.getCanvasOffsetToCenterCursor() ??
Offset.zero;
if (minX < 0) {
_x = min(max(offsetToCenter.dx, minX), maxX);
} else {
// _size.width > (imageRect.right, imageRect.left) * _scale, we should not change _x
}
if (minY < 0) {
_y = min(max(offsetToCenter.dy, minY), maxY);
} else {
// _size.height > (imageRect.bottom - imageRect.top) * _scale, , we should not change _y
}
}
} }
// data for cursor // data for cursor
@@ -1805,9 +1963,13 @@ class CursorModel with ChangeNotifier {
// `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap() // `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap()
// Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position. // Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position.
bool _lastIsBlocked = false; bool _lastIsBlocked = false;
double _yForKeyboardAdjust = 0; bool _lastKeyboardIsVisible = false;
keyHelpToolsVisibilityChanged(Rect? r) { bool get lastKeyboardIsVisible => _lastKeyboardIsVisible;
Rect? get keyHelpToolsRectToAdjustCanvas =>
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
_keyHelpToolsRect = r; _keyHelpToolsRect = r;
if (r == null) { if (r == null) {
_lastIsBlocked = false; _lastIsBlocked = false;
@@ -1817,7 +1979,11 @@ class CursorModel with ChangeNotifier {
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else. // `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
_lastIsBlocked = true; _lastIsBlocked = true;
} }
_yForKeyboardAdjust = _y; if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) {
parent.target?.canvasModel.mobileFocusCanvasCursor();
parent.target?.canvasModel.isMobileCanvasChanged = false;
}
_lastKeyboardIsVisible = keyboardIsVisible;
} }
get lastIsBlocked => _lastIsBlocked; get lastIsBlocked => _lastIsBlocked;
@@ -1856,8 +2022,10 @@ class CursorModel with ChangeNotifier {
addKey(String key) => _cacheKeys.add(key); addKey(String key) => _cacheKeys.add(key);
// remote physical display coordinate // remote physical display coordinate
// For update pan (mobile), onOneFingerPanStart, onOneFingerPanUpdate, onHoldDragUpdate
Rect getVisibleRect() { Rect getVisibleRect() {
final size = MediaQueryData.fromWindow(ui.window).size; final size = parent.target?.canvasModel.getSize() ??
MediaQueryData.fromView(ui.window).size;
final xoffset = parent.target?.canvasModel.x ?? 0; final xoffset = parent.target?.canvasModel.x ?? 0;
final yoffset = parent.target?.canvasModel.y ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0;
final scale = parent.target?.canvasModel.scale ?? 1; final scale = parent.target?.canvasModel.scale ?? 1;
@@ -1866,22 +2034,22 @@ class CursorModel with ChangeNotifier {
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
} }
get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom; Offset getCanvasOffsetToCenterCursor() {
get scale => parent.target?.canvasModel.scale ?? 1.0; // Cursor should be at the center of the visible rect.
// _x = rect.left + rect.width / 2
double adjustForKeyboard() { // _y = rect.right + rect.height / 2
if (keyboardHeight < 100) { // See `getVisibleRect()`
return 0.0; // _x = _displayOriginX - xoffset / scale + size.width / scale * 0.5;
} // _y = _displayOriginY - yoffset / scale + size.height / scale * 0.5;
final size = parent.target?.canvasModel.getSize() ??
final m = MediaQueryData.fromWindow(ui.window); MediaQueryData.fromView(ui.window).size;
final size = m.size; final xoffset = (_displayOriginX - _x) * scale + size.width * 0.5;
final thresh = (size.height - keyboardHeight) / 2; final yoffset = (_displayOriginY - _y) * scale + size.height * 0.5;
final h = (_yForKeyboardAdjust - getVisibleRect().top) * return Offset(xoffset, yoffset);
scale; // local physical display height
return h - thresh;
} }
get scale => parent.target?.canvasModel.scale ?? 1.0;
// mobile Soft keyboard, block touch event from the KeyHelpTools // mobile Soft keyboard, block touch event from the KeyHelpTools
shouldBlock(double x, double y) { shouldBlock(double x, double y) {
if (!(parent.target?.ffiModel.touchMode ?? false)) { if (!(parent.target?.ffiModel.touchMode ?? false)) {
@@ -1896,22 +2064,63 @@ class CursorModel with ChangeNotifier {
return false; return false;
} }
move(double x, double y) { // For touch mode
Future<bool> move(double x, double y) async {
if (shouldBlock(x, y)) { if (shouldBlock(x, y)) {
_lastIsBlocked = true; _lastIsBlocked = true;
return false; return false;
} }
_lastIsBlocked = false; _lastIsBlocked = false;
moveLocal(x, y, adjust: adjustForKeyboard()); if (!_moveLocalIfInRemoteRect(x, y)) {
parent.target?.inputModel.moveMouse(_x, _y); return false;
}
await parent.target?.inputModel.moveMouse(_x, _y);
return true;
}
bool isInRemoteRect(Offset offset) {
return getRemotePosInRect(offset) != null;
}
Offset? getRemotePosInRect(Offset offset) {
final adjust = parent.target?.canvasModel.getAdjustY() ?? 0;
final newPos = _getNewPos(offset.dx, offset.dy, adjust);
final visibleRect = getVisibleRect();
if (!isPointInRect(newPos, visibleRect)) {
return null;
}
final rect = parent.target?.ffiModel.rect;
if (rect != null) {
if (!isPointInRect(newPos, rect)) {
return null;
}
}
return newPos;
}
Offset _getNewPos(double x, double y, double adjust) {
final xoffset = parent.target?.canvasModel.x ?? 0;
final yoffset = parent.target?.canvasModel.y ?? 0;
final newX = (x - xoffset) / scale + _displayOriginX;
final newY = (y - yoffset - adjust) / scale + _displayOriginY;
return Offset(newX, newY);
}
bool _moveLocalIfInRemoteRect(double x, double y) {
final newPos = getRemotePosInRect(Offset(x, y));
if (newPos == null) {
return false;
}
_x = newPos.dx;
_y = newPos.dy;
notifyListeners();
return true; return true;
} }
moveLocal(double x, double y, {double adjust = 0}) { moveLocal(double x, double y, {double adjust = 0}) {
final xoffset = parent.target?.canvasModel.x ?? 0; final newPos = _getNewPos(x, y, adjust);
final yoffset = parent.target?.canvasModel.y ?? 0; _x = newPos.dx;
_x = (x - xoffset) / scale + _displayOriginX; _y = newPos.dy;
_y = (y - yoffset + adjust) / scale + _displayOriginY;
notifyListeners(); notifyListeners();
} }
@@ -1919,13 +2128,13 @@ class CursorModel with ChangeNotifier {
_x = _displayOriginX; _x = _displayOriginX;
_y = _displayOriginY; _y = _displayOriginY;
parent.target?.inputModel.moveMouse(_x, _y); parent.target?.inputModel.moveMouse(_x, _y);
parent.target?.canvasModel.clear(true); parent.target?.canvasModel.reset();
notifyListeners(); notifyListeners();
} }
updatePan(Offset delta, Offset localPosition, bool touchMode) { updatePan(Offset delta, Offset localPosition, bool touchMode) async {
if (touchMode) { if (touchMode) {
_handleTouchMode(delta, localPosition); await _handleTouchMode(delta, localPosition);
return; return;
} }
double dx = delta.dx; double dx = delta.dx;
@@ -1983,13 +2192,34 @@ class CursorModel with ChangeNotifier {
} }
if (dx == 0 && dy == 0) return; if (dx == 0 && dy == 0) return;
_x += dx;
_y += dy; Point<double>? newPos;
final rect = parent.target?.ffiModel.rect;
if (rect == null) {
// unreachable
return;
}
newPos = InputModel.getPointInRemoteRect(
false,
parent.target?.ffiModel.pi.platform,
kPointerEventKindMouse,
kMouseEventTypeDefault,
_x + dx,
_y + dy,
rect,
buttons: kPrimaryButton);
if (newPos == null) {
return;
}
dx = newPos.x - _x;
dy = newPos.y - _y;
_x = newPos.x;
_y = newPos.y;
if (tryMoveCanvasX && dx != 0) { if (tryMoveCanvasX && dx != 0) {
parent.target?.canvasModel.panX(-dx); parent.target?.canvasModel.panX(-dx * scale);
} }
if (tryMoveCanvasY && dy != 0) { if (tryMoveCanvasY && dy != 0) {
parent.target?.canvasModel.panY(-dy); parent.target?.canvasModel.panY(-dy * scale);
} }
parent.target?.inputModel.moveMouse(_x, _y); parent.target?.inputModel.moveMouse(_x, _y);
@@ -2002,7 +2232,7 @@ class CursorModel with ChangeNotifier {
return x >= 0 && y >= 0 && x <= w && y <= h; return x >= 0 && y >= 0 && x <= w && y <= h;
} }
_handleTouchMode(Offset delta, Offset localPosition) { _handleTouchMode(Offset delta, Offset localPosition) async {
bool isMoved = false; bool isMoved = false;
if (_remoteWindowCoords.isNotEmpty && if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null && _windowRect != null &&
@@ -2018,15 +2248,50 @@ class CursorModel with ChangeNotifier {
coords.canvas.scale; coords.canvas.scale;
x2 += coords.cursor.offset.dx; x2 += coords.cursor.offset.dx;
y2 += coords.cursor.offset.dy; y2 += coords.cursor.offset.dy;
parent.target?.inputModel.moveMouse(x2, y2); await parent.target?.inputModel.moveMouse(x2, y2);
isMoved = true; isMoved = true;
} }
} }
if (!isMoved) { if (!isMoved) {
final rect = parent.target?.ffiModel.rect;
if (rect == null) {
// unreachable
return;
}
Offset? movementInRect(double x, double y, Rect r) {
final isXInRect = x >= r.left && x <= r.right;
final isYInRect = y >= r.top && y <= r.bottom;
if (!(isXInRect || isYInRect)) {
return null;
}
if (x < r.left) {
x = r.left;
} else if (x > r.right) {
x = r.right;
}
if (y < r.top) {
y = r.top;
} else if (y > r.bottom) {
y = r.bottom;
}
return Offset(x, y);
}
final scale = parent.target?.canvasModel.scale ?? 1.0; final scale = parent.target?.canvasModel.scale ?? 1.0;
_x += delta.dx / scale; var movement =
_y += delta.dy / scale; movementInRect(_x + delta.dx / scale, _y + delta.dy / scale, rect);
parent.target?.inputModel.moveMouse(_x, _y); if (movement == null) {
return;
}
movement = movementInRect(movement.dx, movement.dy, getVisibleRect());
if (movement == null) {
return;
}
_x = movement.dx;
_y = movement.dy;
await parent.target?.inputModel.moveMouse(_x, _y);
} }
notifyListeners(); notifyListeners();
} }
@@ -2178,6 +2443,7 @@ class CursorModel with ChangeNotifier {
debugPrint("deleting cursor with key $k"); debugPrint("deleting cursor with key $k");
deleteCustomCursor(k); deleteCustomCursor(k);
} }
resetSystemCursor();
} }
trySetRemoteWindowCoords() { trySetRemoteWindowCoords() {
@@ -2224,8 +2490,10 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) { updateQualityStatus(Map<String, dynamic> evt) {
try { try {
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
if ((evt['fps'] as String).isNotEmpty) { _data.speed = evt['speed'];
}
if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) {
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>; final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
final pi = parent.target?.ffiModel.pi; final pi = parent.target?.ffiModel.pi;
if (pi != null) { if (pi != null) {
@@ -2246,14 +2514,18 @@ class QualityMonitorModel with ChangeNotifier {
_data.fps = null; _data.fps = null;
} }
} }
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
if ((evt['target_bitrate'] as String).isNotEmpty) { _data.delay = evt['delay'];
}
if (evt.containsKey('target_bitrate') &&
(evt['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt['target_bitrate']; _data.targetBitrate = evt['target_bitrate'];
} }
if ((evt['codec_format'] as String).isNotEmpty) { if (evt.containsKey('codec_format') &&
(evt['codec_format'] as String).isNotEmpty) {
_data.codecFormat = evt['codec_format']; _data.codecFormat = evt['codec_format'];
} }
if ((evt['chroma'] as String).isNotEmpty) { if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
_data.chroma = evt['chroma']; _data.chroma = evt['chroma'];
} }
notifyListeners(); notifyListeners();
@@ -2267,25 +2539,7 @@ class RecordingModel with ChangeNotifier {
WeakReference<FFI> parent; WeakReference<FFI> parent;
RecordingModel(this.parent); RecordingModel(this.parent);
bool _start = false; bool _start = false;
get start => _start; bool get start => _start;
onSwitchDisplay() {
if (isIOS || !_start) return;
final sessionId = parent.target?.sessionId;
int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayHeight();
if (sessionId == null || width == null || height == null) return;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
bind.sessionRecordScreen(
sessionId: sessionId,
start: true,
display: currentDisplay,
width: width,
height: height);
}
toggle() async { toggle() async {
if (isIOS) return; if (isIOS) return;
@@ -2293,48 +2547,16 @@ class RecordingModel with ChangeNotifier {
if (sessionId == null) return; if (sessionId == null) return;
final pi = parent.target?.ffiModel.pi; final pi = parent.target?.ffiModel.pi;
if (pi == null) return; if (pi == null) return;
final currentDisplay = pi.currentDisplay; bool value = !_start;
if (currentDisplay == kAllDisplayValue) return; if (value) {
_start = !_start; await sessionRefreshVideo(sessionId, pi);
notifyListeners();
await _sendStatusMessage(sessionId, pi, _start);
if (_start) {
sessionRefreshVideo(sessionId, pi);
if (versionCmp(pi.version, '1.2.4') >= 0) {
// will not receive SwitchDisplay since 1.2.4
onSwitchDisplay();
}
} else {
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay,
width: 0,
height: 0);
} }
await bind.sessionRecordScreen(sessionId: sessionId, start: value);
} }
onClose() async { updateStatus(bool status) {
if (isIOS) return; _start = status;
final sessionId = parent.target?.sessionId; notifyListeners();
if (sessionId == null) return;
if (!_start) return;
_start = false;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
await _sendStatusMessage(sessionId, pi, false);
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay,
width: 0,
height: 0);
}
_sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async {
await bind.sessionRecordStatus(sessionId: sessionId, status: status);
} }
} }
@@ -2383,6 +2605,9 @@ class FFI {
late final ElevationModel elevationModel; // session late final ElevationModel elevationModel; // session
late final CmFileModel cmFileModel; // cm late final CmFileModel cmFileModel; // cm
late final TextureModel textureModel; //session late final TextureModel textureModel; //session
late final Peers recentPeersModel; // global
late final Peers favoritePeersModel; // global
late final Peers lanPeersModel; // global
FFI(SessionID? sId) { FFI(SessionID? sId) {
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
@@ -2403,6 +2628,16 @@ class FFI {
elevationModel = ElevationModel(WeakReference(this)); elevationModel = ElevationModel(WeakReference(this));
cmFileModel = CmFileModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this));
textureModel = TextureModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this));
recentPeersModel = Peers(
name: PeersModelName.recent,
loadEvent: LoadEvent.recent,
getInitPeers: null);
favoritePeersModel = Peers(
name: PeersModelName.favorite,
loadEvent: LoadEvent.favorite,
getInitPeers: null);
lanPeersModel = Peers(
name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null);
} }
/// Mobile reuse FFI /// Mobile reuse FFI
@@ -2423,6 +2658,7 @@ class FFI {
String? switchUuid, String? switchUuid,
String? password, String? password,
bool? isSharedPassword, bool? isSharedPassword,
String? connToken,
bool? forceRelay, bool? forceRelay,
int? tabWindowId, int? tabWindowId,
int? display, int? display,
@@ -2459,6 +2695,7 @@ class FFI {
forceRelay: forceRelay ?? false, forceRelay: forceRelay ?? false,
password: password ?? '', password: password ?? '',
isSharedPassword: isSharedPassword ?? false, isSharedPassword: isSharedPassword ?? false,
connToken: connToken,
); );
} else if (display != null) { } else if (display != null) {
if (displays == null) { if (displays == null) {
@@ -2497,6 +2734,7 @@ class FFI {
onEvent2UIRgba(); onEvent2UIRgba();
imageModel.onRgba(display, data); imageModel.onRgba(display, data);
}); });
this.id = id;
return; return;
} }
@@ -2630,6 +2868,7 @@ class FFI {
canvasModel.scale, canvasModel.scale,
ffiModel.pi.currentDisplay); ffiModel.pi.currentDisplay);
} }
imageModel.callbacksOnFirstImage.clear();
await imageModel.update(null); await imageModel.update(null);
cursorModel.clear(); cursorModel.clear();
ffiModel.clear(); ffiModel.clear();

View File

@@ -48,6 +48,12 @@ class PlatformFFI {
static get isMain => instance._appType == kAppTypeMain; static get isMain => instance._appType == kAppTypeMain;
static String getByName(String name, [String arg = '']) {
return '';
}
static void setByName(String name, [String value = '']) {}
static Future<String> getVersion() async { static Future<String> getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version; return packageInfo.version;
@@ -276,4 +282,6 @@ class PlatformFFI {
void syncAndroidServiceAppDirConfigPath() { void syncAndroidServiceAppDirConfigPath() {
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
} }
void setFullscreenCallback(void Function(bool) fun) {}
} }

View File

@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
} }
void _updateOnlineState(Map<String, dynamic> evt) { void _updateOnlineState(Map<String, dynamic> evt) {
int changedCount = 0;
evt['onlines'].split(',').forEach((online) { evt['onlines'].split(',').forEach((online) {
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (peers[i].id == online) { if (peers[i].id == online) {
peers[i].online = true; if (!peers[i].online) {
changedCount += 1;
peers[i].online = true;
}
} }
} }
}); });
@@ -205,13 +209,18 @@ class Peers extends ChangeNotifier {
evt['offlines'].split(',').forEach((offline) { evt['offlines'].split(',').forEach((offline) {
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (peers[i].id == offline) { if (peers[i].id == offline) {
peers[i].online = false; if (peers[i].online) {
changedCount += 1;
peers[i].online = false;
}
} }
} }
}); });
event = UpdateEvent.online; if (changedCount > 0) {
notifyListeners(); event = UpdateEvent.online;
notifyListeners();
}
} }
void _updatePeers(Map<String, dynamic> evt) { void _updatePeers(Map<String, dynamic> evt) {

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