diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 16509a3be..ec23aa7a9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -1,14 +1,6 @@
name: 🐞 Bug report
description: Thanks for taking the time to fill out this bug report! Please fill the form in **English**
-title: "[Bug] "
body:
- - type: checkboxes
- attributes:
- label: Is there an existing issue for this?
- description: Please search to see if an issue related to this already exists.
- options:
- - label: I have searched the existing issues
- required: true
- type: textarea
id: desc
attributes:
@@ -30,13 +22,22 @@ body:
description: A clear and concise description of what you expected to happen
validations:
required: true
+ - type: input
+ id: os
+ attributes:
+ label: Operating system(s) on local side and remote side
+ description: What operating system(s) do you see this bug on? local side -> remote side.
+ placeholder: |
+ Windows 10 -> osx
+ validations:
+ required: true
- type: input
id: version
attributes:
- label: Operating System(s) and RustDesk Version(s) on local side and remote side
- description: What Operatiing System(s) and version(s) of RustDesk do you see this bug on? local side / remote side.
+ label: RustDesk Version(s) on local side and remote side
+ description: What RustDesk version(s) do you see this bug on? local side -> remote side.
placeholder: |
- Windows 10, 1.1.9 / osx 13.1, 1.1.8
+ 1.1.9 -> 1.1.8
validations:
required: true
- type: textarea
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 7b43e397b..2da6bbaf1 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,3 +1,4 @@
+blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/rustdesk/rustdesk/discussions/category_choices
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 50cd6d0cf..29b0d0e0f 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -1,15 +1,6 @@
name: 🛠️ Feature request
description: Suggest an idea for RustDesk
-title: "[FR] "
body:
- - type: checkboxes
- attributes:
- label: Is there an existing issue for this?
- description: Please search to see if an issue related to this already exists.
- options:
- - label: I have searched the existing issues
- required: true
-
- type: textarea
id: desc
attributes:
diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml
index 5d4cf39c9..78c60df37 100644
--- a/.github/workflows/flutter-ci.yml
+++ b/.github/workflows/flutter-ci.yml
@@ -105,7 +105,7 @@ jobs:
- name: Install build runtime
run: |
- brew install llvm create-dmg nasm yasm cmake gcc wget ninja
+ brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml
index 5ca284cee..ffcadd18b 100644
--- a/.github/workflows/flutter-nightly.yml
+++ b/.github/workflows/flutter-nightly.yml
@@ -183,7 +183,7 @@ jobs:
- name: Install build runtime
run: |
- brew install llvm create-dmg nasm yasm cmake gcc wget ninja
+ brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
@@ -242,10 +242,9 @@ jobs:
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm rustdesk-${{ env.VERSION }}.dmg || true
- mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app
- codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v
+ codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v
+ codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
diff --git a/Cargo.lock b/Cargo.lock
index e15641363..48981e169 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -153,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb"
dependencies = [
"clipboard-win",
"core-graphics 0.22.3",
- "image",
+ "image 0.23.14",
"log",
"objc",
"objc-foundation",
@@ -278,24 +278,24 @@ dependencies = [
[[package]]
name = "atk"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd"
+checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf"
dependencies = [
"atk-sys",
"bitflags",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
]
[[package]]
name = "atk-sys"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6"
+checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148"
dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -405,6 +405,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "bit_field"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -508,24 +514,25 @@ dependencies = [
[[package]]
name = "cairo-rs"
-version = "0.15.12"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
+checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d"
dependencies = [
"bitflags",
"cairo-sys-rs",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
+ "once_cell",
"thiserror",
]
[[package]]
name = "cairo-sys-rs"
-version = "0.15.1"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
+checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421"
dependencies = [
- "glib-sys 0.15.10",
+ "glib-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -972,7 +979,7 @@ dependencies = [
"alsa",
"core-foundation-sys 0.8.3",
"coreaudio-rs",
- "jni",
+ "jni 0.19.0",
"js-sys",
"lazy_static",
"libc",
@@ -1059,6 +1066,12 @@ dependencies = [
"cfg-if 1.0.0",
]
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1131,9 +1144,9 @@ dependencies = [
[[package]]
name = "dark-light"
-version = "0.2.3"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a"
+checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645"
dependencies = [
"dconf_rs",
"detect-desktop-environment",
@@ -1334,8 +1347,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]]
name = "default-net"
-version = "0.11.0"
-source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63"
dependencies = [
"libc",
"memalloc",
@@ -1552,7 +1566,6 @@ version = "0.0.14"
dependencies = [
"core-graphics 0.22.3",
"hbb_common",
- "libc",
"log",
"objc",
"pkg-config",
@@ -1711,6 +1724,22 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+[[package]]
+name = "exr"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide 0.6.2",
+ "smallvec",
+ "threadpool",
+ "zune-inflate",
+]
+
[[package]]
name = "extend"
version = "1.1.2"
@@ -1793,6 +1822,19 @@ dependencies = [
"time 0.3.9",
]
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "nanorand",
+ "pin-project",
+ "spin 0.9.5",
+]
+
[[package]]
name = "flutter_rust_bridge"
version = "1.61.1"
@@ -2039,63 +2081,90 @@ dependencies = [
[[package]]
name = "gdk"
-version = "0.15.4"
+version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8"
+checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1"
dependencies = [
"bitflags",
"cairo-rs",
"gdk-pixbuf",
"gdk-sys",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
"pango",
]
[[package]]
name = "gdk-pixbuf"
-version = "0.15.11"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a"
+checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05"
dependencies = [
"bitflags",
"gdk-pixbuf-sys",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
-version = "0.15.10"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7"
+checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016"
dependencies = [
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
[[package]]
name = "gdk-sys"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88"
+checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"pango-sys",
"pkg-config",
"system-deps 6.0.3",
]
+[[package]]
+name = "gdkwayland-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba"
+dependencies = [
+ "gdk-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
+ "libc",
+ "pkg-config",
+ "system-deps 6.0.3",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af"
+dependencies = [
+ "gdk-sys",
+ "glib-sys 0.16.3",
+ "libc",
+ "system-deps 6.0.3",
+ "x11 2.20.1",
+]
+
[[package]]
name = "generic-array"
version = "0.14.6"
@@ -2123,8 +2192,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
+ "js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
]
[[package]]
@@ -2135,34 +2216,24 @@ checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "gio"
-version = "0.15.12"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
+checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-io",
- "gio-sys 0.15.10",
- "glib 0.15.12",
+ "futures-util",
+ "gio-sys",
+ "glib 0.16.5",
"libc",
"once_cell",
+ "pin-project-lite",
+ "smallvec",
"thiserror",
]
-[[package]]
-name = "gio-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d"
-dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
- "libc",
- "system-deps 6.0.3",
- "winapi 0.3.9",
-]
-
[[package]]
name = "gio-sys"
version = "0.16.3"
@@ -2195,26 +2266,6 @@ dependencies = [
"once_cell",
]
-[[package]]
-name = "glib"
-version = "0.15.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
-dependencies = [
- "bitflags",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-task",
- "glib-macros 0.15.11",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
- "libc",
- "once_cell",
- "smallvec",
- "thiserror",
-]
-
[[package]]
name = "glib"
version = "0.16.5"
@@ -2227,7 +2278,7 @@ dependencies = [
"futures-executor",
"futures-task",
"futures-util",
- "gio-sys 0.16.3",
+ "gio-sys",
"glib-macros 0.16.3",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
@@ -2253,21 +2304,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "glib-macros"
-version = "0.15.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64"
-dependencies = [
- "anyhow",
- "heck 0.4.0",
- "proc-macro-crate 1.2.1",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
[[package]]
name = "glib-macros"
version = "0.16.3"
@@ -2293,16 +2329,6 @@ dependencies = [
"system-deps 1.3.2",
]
-[[package]]
-name = "glib-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
-dependencies = [
- "libc",
- "system-deps 6.0.3",
-]
-
[[package]]
name = "glib-sys"
version = "0.16.3"
@@ -2330,17 +2356,6 @@ dependencies = [
"system-deps 1.3.2",
]
-[[package]]
-name = "gobject-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
-dependencies = [
- "glib-sys 0.15.10",
- "libc",
- "system-deps 6.0.3",
-]
-
[[package]]
name = "gobject-sys"
version = "0.16.3"
@@ -2369,7 +2384,7 @@ dependencies = [
"gstreamer-sys",
"libc",
"muldiv",
- "num-rational",
+ "num-rational 0.3.2",
"once_cell",
"paste",
"pretty-hex",
@@ -2487,9 +2502,9 @@ dependencies = [
[[package]]
name = "gtk"
-version = "0.15.5"
+version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0"
+checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6"
dependencies = [
"atk",
"bitflags",
@@ -2499,7 +2514,7 @@ dependencies = [
"gdk",
"gdk-pixbuf",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"gtk-sys",
"gtk3-macros",
"libc",
@@ -2510,17 +2525,17 @@ dependencies = [
[[package]]
name = "gtk-sys"
-version = "0.15.3"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84"
+checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3"
dependencies = [
"atk-sys",
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk-sys",
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"pango-sys",
"system-deps 6.0.3",
@@ -2528,9 +2543,9 @@ dependencies = [
[[package]]
name = "gtk3-macros"
-version = "0.15.4"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9"
+checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff"
dependencies = [
"anyhow",
"proc-macro-crate 1.2.1",
@@ -2559,6 +2574,15 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -2573,6 +2597,7 @@ name = "hbb_common"
version = "0.1.0"
dependencies = [
"anyhow",
+ "backtrace",
"bytes",
"chrono",
"confy",
@@ -2583,9 +2608,11 @@ dependencies = [
"futures",
"futures-util",
"lazy_static",
+ "libc",
"log",
"mac_address",
"machine-uid",
+ "osascript",
"protobuf",
"protobuf-codegen",
"quinn",
@@ -2780,10 +2807,29 @@ dependencies = [
"byteorder",
"color_quant",
"num-iter",
- "num-rational",
+ "num-rational 0.3.2",
"num-traits 0.2.15",
- "png",
- "tiff",
+ "png 0.16.8",
+ "tiff 0.6.1",
+]
+
+[[package]]
+name = "image"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder 0.3.0",
+ "num-rational 0.4.1",
+ "num-traits 0.2.15",
+ "png 0.17.7",
+ "scoped_threadpool",
+ "tiff 0.8.1",
]
[[package]]
@@ -2914,6 +2960,20 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.0"
@@ -2935,6 +2995,15 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
+dependencies = [
+ "rayon",
+]
+
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -2954,6 +3023,17 @@ dependencies = [
"winapi-build",
]
+[[package]]
+name = "keyboard-types"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68"
+dependencies = [
+ "bitflags",
+ "serde 1.0.149",
+ "unicode-segmentation",
+]
+
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -2967,12 +3047,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
-name = "libappindicator"
-version = "0.7.1"
+name = "lebe"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "libappindicator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f"
dependencies = [
- "glib 0.15.12",
+ "glib 0.16.5",
"gtk",
"gtk-sys",
"libappindicator-sys",
@@ -2981,9 +3067,9 @@ dependencies = [
[[package]]
name = "libappindicator-sys"
-version = "0.7.3"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa"
+checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918"
dependencies = [
"gtk-sys",
"libloading",
@@ -3084,6 +3170,25 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "libxdo"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db"
+dependencies = [
+ "libxdo-sys",
+]
+
+[[package]]
+name = "libxdo-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
+dependencies = [
+ "libc",
+ "x11 2.20.1",
+]
+
[[package]]
name = "link-cplusplus"
version = "1.0.7"
@@ -3339,12 +3444,41 @@ dependencies = [
"glob",
]
+[[package]]
+name = "muda"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6"
+dependencies = [
+ "cocoa",
+ "crossbeam-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gtk",
+ "keyboard-types",
+ "libxdo",
+ "objc",
+ "once_cell",
+ "png 0.17.7",
+ "thiserror",
+ "windows-sys 0.45.0",
+]
+
[[package]]
name = "muldiv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204"
+[[package]]
+name = "nanorand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
+dependencies = [
+ "getrandom",
+]
+
[[package]]
name = "ndk"
version = "0.5.0"
@@ -3615,6 +3749,17 @@ dependencies = [
"num-traits 0.2.15",
]
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-integer",
+ "num-traits 0.2.15",
+]
+
[[package]]
name = "num-traits"
version = "0.1.43"
@@ -3727,7 +3872,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1"
dependencies = [
- "jni",
+ "jni 0.19.0",
"ndk 0.6.0",
"ndk-context",
"num-derive",
@@ -3746,9 +3891,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "openssl-probe"
@@ -3783,19 +3928,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
-name = "padlock"
-version = "0.2.0"
+name = "osascript"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f"
+checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc"
+dependencies = [
+ "serde 1.0.149",
+ "serde_derive",
+ "serde_json 1.0.89",
+]
[[package]]
name = "pango"
-version = "0.15.10"
+version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f"
+checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94"
dependencies = [
"bitflags",
- "glib 0.15.12",
+ "gio",
+ "glib 0.16.5",
"libc",
"once_cell",
"pango-sys",
@@ -3803,12 +3954,12 @@ dependencies = [
[[package]]
name = "pango-sys"
-version = "0.15.10"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa"
+checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f"
dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -4004,6 +4155,18 @@ dependencies = [
"miniz_oxide 0.3.7",
]
+[[package]]
+name = "png"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "flate2",
+ "miniz_oxide 0.6.2",
+]
+
[[package]]
name = "polling"
version = "2.5.1"
@@ -4404,12 +4567,13 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
-source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358"
+source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc"
dependencies = [
"cocoa",
"core-foundation 0.9.3",
"core-foundation-sys 0.8.3",
"core-graphics 0.22.3",
+ "dispatch",
"enum-map",
"epoll",
"inotify",
@@ -4546,7 +4710,7 @@ dependencies = [
"cc",
"libc",
"once_cell",
- "spin",
+ "spin 0.5.2",
"untrusted",
"web-sys",
"winapi 0.3.9",
@@ -4662,7 +4826,6 @@ dependencies = [
"arboard",
"async-process",
"async-trait",
- "backtrace",
"base64",
"bytes",
"cc",
@@ -4689,16 +4852,13 @@ dependencies = [
"flutter_rust_bridge",
"flutter_rust_bridge_codegen",
"fruitbasket",
- "glib 0.16.5",
- "gtk",
"hbb_common",
"hound",
+ "image 0.24.5",
"impersonate_system",
"include_dir",
- "jni",
+ "jni 0.19.0",
"lazy_static",
- "libappindicator",
- "libc",
"libpulse-binding",
"libpulse-simple-binding",
"mac_address",
@@ -4729,7 +4889,8 @@ dependencies = [
"sys-locale",
"sysinfo",
"system_shutdown",
- "tray-item",
+ "tao",
+ "tray-icon",
"trayicon",
"url",
"uuid",
@@ -4867,6 +5028,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -4888,9 +5055,8 @@ dependencies = [
"gstreamer-video",
"hbb_common",
"hwcodec",
- "jni",
+ "jni 0.19.0",
"lazy_static",
- "libc",
"log",
"ndk 0.7.0",
"num_cpus",
@@ -5126,6 +5292,12 @@ version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+[[package]]
+name = "simd-adler32"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18"
+
[[package]]
name = "simple_rc"
version = "0.1.0"
@@ -5216,6 +5388,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+[[package]]
+name = "spin"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc"
+dependencies = [
+ "lock_api",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -5398,6 +5579,61 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "tao"
+version = "0.17.0"
+source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "cc",
+ "cocoa",
+ "core-foundation 0.9.3",
+ "core-graphics 0.22.3",
+ "crossbeam-channel",
+ "dispatch",
+ "gdk",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gio",
+ "glib 0.16.5",
+ "glib-sys 0.16.3",
+ "gtk",
+ "image 0.24.5",
+ "instant",
+ "jni 0.20.0",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.6.0",
+ "ndk-context",
+ "ndk-sys 0.3.0",
+ "objc",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "png 0.17.7",
+ "raw-window-handle 0.5.0",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "uuid",
+ "windows 0.44.0",
+ "windows-implement",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.0"
+source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tap"
version = "1.0.1"
@@ -5508,11 +5744,22 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
dependencies = [
- "jpeg-decoder",
+ "jpeg-decoder 0.1.22",
"miniz_oxide 0.4.4",
"weezl",
]
+[[package]]
+name = "tiff"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
+dependencies = [
+ "flate2",
+ "jpeg-decoder 0.3.0",
+ "weezl",
+]
+
[[package]]
name = "time"
version = "0.1.45"
@@ -5697,21 +5944,22 @@ dependencies = [
]
[[package]]
-name = "tray-item"
-version = "0.7.1"
+name = "tray-icon"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f"
+checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3"
dependencies = [
"cocoa",
"core-graphics 0.22.3",
- "gtk",
+ "crossbeam-channel",
+ "dirs-next",
"libappindicator",
- "libc",
+ "muda",
"objc",
- "objc-foundation",
- "objc_id",
- "padlock",
- "winapi 0.3.9",
+ "once_cell",
+ "png 0.17.7",
+ "thiserror",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -5810,9 +6058,9 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.2.2"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
+checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom",
]
@@ -6241,6 +6489,39 @@ dependencies = [
"windows_x86_64_msvc 0.34.0",
]
+[[package]]
+name = "windows"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "windows-service"
version = "0.4.0"
@@ -6286,19 +6567,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
- "windows_aarch64_msvc 0.42.0",
- "windows_i686_gnu 0.42.0",
- "windows_i686_msvc 0.42.0",
- "windows_x86_64_gnu 0.42.0",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm",
- "windows_x86_64_msvc 0.42.0",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
@@ -6326,9 +6631,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
@@ -6356,9 +6661,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
@@ -6386,9 +6691,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
@@ -6416,15 +6721,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
@@ -6452,9 +6757,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winit"
@@ -6565,12 +6870,12 @@ dependencies = [
[[package]]
name = "x11-dl"
-version = "2.20.1"
+version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
- "lazy_static",
"libc",
+ "once_cell",
"pkg-config",
]
@@ -6702,6 +7007,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "zune-inflate"
+version = "0.2.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d"
+dependencies = [
+ "simd-adler32",
+]
+
[[package]]
name = "zvariant"
version = "3.9.0"
diff --git a/Cargo.toml b/Cargo.toml
index 936b9e349..0ebe49fdf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,7 +43,6 @@ cfg-if = "1.0"
lazy_static = "1.4"
sha2 = "0.10"
repng = "0.2"
-libc = "0.2"
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
runas = "0.2"
@@ -59,7 +58,7 @@ base64 = "0.13"
sysinfo = "0.24"
num_cpus = "1.13"
bytes = { version = "1.2", features = ["serde"] }
-default-net = { git = "https://github.com/Kingtous/default-net" }
+default-net = "0.12.0"
wol-rs = "0.9.1"
flutter_rust_bridge = { version = "1.61.1", optional = true }
errno = "0.2.8"
@@ -86,7 +85,6 @@ arboard = "2.0"
system_shutdown = "3.0.0"
[target.'cfg(target_os = "windows")'.dependencies]
-#systray = { git = "https://github.com/open-trade/systray-rs" }
trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
winit = "0.26"
winapi = { version = "0.3", features = ["winuser"] }
@@ -104,11 +102,15 @@ dispatch = "0.2"
core-foundation = "0.9"
core-graphics = "0.22"
include_dir = "0.7.2"
-tray-item = "0.7" # looks better than trayicon
-dark-light = "0.2"
+dark-light = "1.0"
fruitbasket = "0.10.0"
objc_id = "0.1.1"
+[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
+tray-icon = "0.4"
+tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }
+image = "0.24"
+
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.25" }
pulse = { package = "libpulse-binding", version = "2.26" }
@@ -118,10 +120,6 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
-gtk = "0.15"
-libappindicator = "0.7"
-glib = "0.16.5"
-backtrace = "0.3"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11"
@@ -157,7 +155,6 @@ identifier = "com.carriez.rustdesk"
icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"]
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"]
osx_minimum_system_version = "10.14"
-resources = ["res/mac-tray-light.png","res/mac-tray-dark.png", "res/mac-tray-light-x2.png","res/mac-tray-dark-x2.png"]
#https://github.com/johnthagen/min-sized-rust
[profile.release]
diff --git a/README.md b/README.md
index 866063726..df0ca8328 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Servers •
Build •
Docker •
diff --git a/build.py b/build.py
index 6b107ff4b..9e490166f 100755
--- a/build.py
+++ b/build.py
@@ -323,7 +323,7 @@ def build_flutter_dmg(version, features):
os.chdir('flutter')
os.system('flutter build macos --release')
os.system(
- "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app")
+ "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
os.chdir("..")
diff --git a/docs/README-DE.md b/docs/README-DE.md
index 0b51d8fdd..8ee4a51fa 100644
--- a/docs/README-DE.md
+++ b/docs/README-DE.md
@@ -1,63 +1,84 @@
- Server •
- Kompilieren •
+ Server •
+ Kompilieren •
Docker •
Dateistruktur •
Screenshots
- [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ]
- Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren
+ [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ]
+ Wir brauchen deine Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in deine Muttersprache zu übersetzen.
-Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[](https://ko-fi.com/I2I04VU09)
-Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
+RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
-RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start.
+
-[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst.
-## Kostenlose öffentliche Server
+[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
-Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
+[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases)
-| Standort | Serverart | Spezifikationen | Kommentare |
-| --------- | ------------- | ------------------ | ---------- |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | |
-| Germany | Codext | 2 vCPU / 4GB RAM |
-| Germany | Hetzner | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Freie öffentliche Server
+
+Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
+| Standort | Anbieter | Spezifikation |
+| --------- | ------------- | ------------------ |
+| Südkorea (Seoul) | AWS lightsail | 1 vCPU / 0,5 GB RAM |
+| Deutschland | Hetzner | 2 vCPU / 4 GB RAM |
+| Deutschland | Codext | 4 vCPU / 8 GB RAM |
+| Finnland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
+| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
+| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM |
## Abhängigkeiten
-Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter.
+Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.
+
+Bitte lade die dynamische Bibliothek Sciter selbst herunter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
-[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-## Die groben Schritte zum Kompilieren
+## Grobe Schritte zum Kompilieren
-- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor
+- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor
-- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu
+- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die Systemumgebungsvariable `VCPKG_ROOT` hinzu
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- - Linux/MacOS: `vcpkg install libvpx libyuv opus`
+ - Linux/macOS: `vcpkg install libvpx libyuv opus`
- Nutze `cargo run`
+## [Erstellen](https://rustdesk.com/docs/de/dev/build/)
+
## Kompilieren auf Linux
### Ubuntu 18 (Debian 10)
```sh
-sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
+```
### Fedora 28 (CentOS 8)
```sh
@@ -82,7 +103,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus
```
-### libvpx reparieren (Für Fedora)
+### libvpx reparieren (für Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -105,16 +126,40 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
-cargo run
+VCPKG_ROOT=$HOME/vcpkg cargo run
```
-### Ändere Wayland zu X11 (Xorg)
+### Wayland zu X11 (Xorg) ändern
-RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen.
+RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen.
-## Auf Docker Kompilieren
+## Wayland-Unterstützung
-Beginne damit das Repository zu klonen und den Docker Container zu bauen:
+Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene).
+
+Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen:
+```bash
+# Dienst uinput starten
+$ sudo rustdesk --service
+$ rustdesk
+```
+**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast.
+```bash
+$ dbus-send --session --print-reply \
+ --dest=org.freedesktop.portal.Desktop \
+ /org/freedesktop/portal/desktop \
+ org.freedesktop.DBus.Properties.Get \
+ string:org.freedesktop.portal.ScreenCast string:version
+# Keine Unterstützung
+Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
+# Unterstützung
+method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
+ variant uint32 4
+```
+
+## Auf Docker kompilieren
+
+Beginne damit, das Repository zu klonen und den Docker-Container zu bauen:
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -122,13 +167,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
-Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl:
+Führe jedes Mal, wenn du das Programm kompilieren musst, folgenden Befehl aus:
```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
```
-Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen:
+Bedenke, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn du verschiedene Argumente für den Kompilierbefehl angeben musst, kannst du dies am Ende des Befehls an der Position `` tun. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm findest du im Zielordner auf deinem System. Du kannst es mit folgendem Befehl ausführen:
```sh
target/debug/rustdesk
@@ -140,18 +185,20 @@ Oder, wenn du eine Releaseversion benutzt:
target/release/rustdesk
```
-Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
+Bitte stelle sicher, dass du diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
## Dateistruktur
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme
-- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
-- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient
## Screenshots
diff --git a/docs/README-JP.md b/docs/README-JP.md
index 6d3b6d380..36c74dfed 100644
--- a/docs/README-JP.md
+++ b/docs/README-JP.md
@@ -14,7 +14,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[](https://ko-fi.com/I2I04VU09)
-Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo).
+Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。

@@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
-## [Build](https://rustdesk.com/docs/en/dev/build/)
+## [ビルド](https://rustdesk.com/docs/en/dev/build/)
## Linuxでのビルド手順
@@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
-### Build
+### ビルド
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -154,7 +154,7 @@ target/release/rustdesk
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
-## File Structure
+## ファイル構造
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
@@ -165,7 +165,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
-## Snapshot
+## スナップショット

diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index 04b2ccc9a..9b25f4973 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -16,6 +16,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..65291b96e
--- /dev/null
+++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index eac2fe724..d05404d3a 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..3742f241f
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..964c5faa0
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png
new file mode 100644
index 000000000..79a814f59
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
new file mode 100644
index 000000000..814ba4549
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 8c01e98de..f16b3d61d 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..de17ccbda
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..2136a2f3c
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png
new file mode 100644
index 000000000..c179bf053
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index d32c8f8e8..d9bd8fdfe 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..f8ced45f1
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..415eca622
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..d82d1a81b
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index a2f07afb4..eba179347 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0f46fafaf
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..87889c953
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..2cbe6eaf1
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index e8c754f4a..a8d80d2a2 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..88eafe8dd
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..00709a815
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..209c5f977
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..ab9832824
--- /dev/null
+++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #ffffff
+
\ No newline at end of file
diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg
new file mode 100644
index 000000000..ef0bb12a7
--- /dev/null
+++ b/flutter/assets/GitHub.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/Github.svg b/flutter/assets/Github.svg
deleted file mode 100644
index a5bd1de81..000000000
--- a/flutter/assets/Github.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg
index b7bb2f42f..df394a84f 100644
--- a/flutter/assets/Google.svg
+++ b/flutter/assets/Google.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg
index 0fa45b93d..931e72844 100644
--- a/flutter/assets/Okta.svg
+++ b/flutter/assets/Okta.svg
@@ -1,30 +1 @@
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg
new file mode 100644
index 000000000..3049f3b89
--- /dev/null
+++ b/flutter/assets/actions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg
new file mode 100644
index 000000000..4185945e1
--- /dev/null
+++ b/flutter/assets/actions_mobile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg
index e46dab11e..6fd89c9ab 100644
--- a/flutter/assets/android.svg
+++ b/flutter/assets/android.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg
new file mode 100644
index 000000000..7c07ee25d
--- /dev/null
+++ b/flutter/assets/call_end.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg
new file mode 100644
index 000000000..530f12a97
--- /dev/null
+++ b/flutter/assets/call_wait.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg
new file mode 100644
index 000000000..c4ab3c92d
--- /dev/null
+++ b/flutter/assets/chat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg
new file mode 100644
index 000000000..fb18eabd2
--- /dev/null
+++ b/flutter/assets/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg
new file mode 100644
index 000000000..9d107d699
--- /dev/null
+++ b/flutter/assets/display.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg
new file mode 100644
index 000000000..93f27bf7b
--- /dev/null
+++ b/flutter/assets/fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg
new file mode 100644
index 000000000..f244631fe
--- /dev/null
+++ b/flutter/assets/fullscreen_exit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg
index 37bb196e3..5a344dd04 100644
--- a/flutter/assets/insecure.svg
+++ b/flutter/assets/insecure.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg
index f08bee6a6..17b474e6e 100644
--- a/flutter/assets/insecure_relay.svg
+++ b/flutter/assets/insecure_relay.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg
index 69f0c96cb..163e045e1 100644
--- a/flutter/assets/kb_layout_iso.svg
+++ b/flutter/assets/kb_layout_iso.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg
index 09a055be3..cfbb046ca 100644
--- a/flutter/assets/kb_layout_not_iso.svg
+++ b/flutter/assets/kb_layout_not_iso.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg
new file mode 100644
index 000000000..d72033f6d
--- /dev/null
+++ b/flutter/assets/keyboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg
index 74248b5f0..2c3697be9 100644
--- a/flutter/assets/linux.svg
+++ b/flutter/assets/linux.svg
@@ -1,6 +1 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico
deleted file mode 100644
index d5080c1f7..000000000
Binary files a/flutter/assets/logo.ico and /dev/null differ
diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png
deleted file mode 100644
index ede0e00c4..000000000
Binary files a/flutter/assets/logo.png and /dev/null differ
diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg
index 0001d0762..4d43f8bcd 100644
--- a/flutter/assets/logo.svg
+++ b/flutter/assets/logo.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg
index 8092b3af3..ccf9c7aab 100644
--- a/flutter/assets/mac.svg
+++ b/flutter/assets/mac.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg
new file mode 100644
index 000000000..a8715011b
--- /dev/null
+++ b/flutter/assets/pinned.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg
new file mode 100644
index 000000000..09aa55e2a
--- /dev/null
+++ b/flutter/assets/rec.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg
new file mode 100644
index 000000000..bbd948c73
--- /dev/null
+++ b/flutter/assets/record_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg
index 29e1d3c4f..fcd99f2f5 100644
--- a/flutter/assets/secure.svg
+++ b/flutter/assets/secure.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg
index 8ecbdb47b..af54808a8 100644
--- a/flutter/assets/secure_relay.svg
+++ b/flutter/assets/secure_relay.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg
new file mode 100644
index 000000000..7e93a7a35
--- /dev/null
+++ b/flutter/assets/unpinned.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg
new file mode 100644
index 000000000..bf90ec958
--- /dev/null
+++ b/flutter/assets/voice_call.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg
new file mode 100644
index 000000000..f1771c3fd
--- /dev/null
+++ b/flutter/assets/voice_call_waiting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg
index 326f7829d..a0f7e3def 100644
--- a/flutter/assets/win.svg
+++ b/flutter/assets/win.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 30d38b8db..e1dd1a1f8 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -49,6 +49,11 @@ int androidVersion = 0;
int windowsBuildNumber = 0;
DesktopType? desktopType;
+/// Check if the app is running with single view mode.
+bool isSingleViewApp() {
+ return desktopType == DesktopType.cm;
+}
+
/// * debug or test only, DO NOT enable in release build
bool isTest = false;
@@ -212,6 +217,9 @@ class MyTheme {
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)
: null,
+ checkboxTheme: const CheckboxThemeData(
+ checkColor: MaterialStatePropertyAll(dark)
+ ),
).copyWith(
extensions: >[
ColorThemeExtension.dark,
@@ -331,6 +339,9 @@ closeConnection({String? id}) {
}
void window_on_top(int? id) {
+ if (!isDesktop) {
+ return;
+ }
if (id == null) {
// main window
windowManager.restore();
@@ -367,20 +378,25 @@ class Dialog {
}
}
+class OverlayKeyState {
+ final _overlayKey = GlobalKey();
+
+ /// use global overlay by default
+ OverlayState? get state =>
+ _overlayKey.currentState ?? globalKey.currentState?.overlay;
+
+ GlobalKey? get key => _overlayKey;
+}
+
class OverlayDialogManager {
- OverlayState? _overlayState;
final Map _dialogs = {};
+ var _overlayKeyState = OverlayKeyState();
int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry;
- /// By default OverlayDialogManager use global overlay
- OverlayDialogManager() {
- _overlayState = globalKey.currentState?.overlay;
- }
-
- void setOverlayState(OverlayState? overlayState) {
- _overlayState = overlayState;
+ void setOverlayState(OverlayKeyState overlayKeyState) {
+ _overlayKeyState = overlayKeyState;
}
void dismissAll() {
@@ -404,7 +420,7 @@ class OverlayDialogManager {
bool useAnimation = true,
bool forceGlobal = false}) {
final overlayState =
- forceGlobal ? globalKey.currentState?.overlay : _overlayState;
+ forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state;
if (overlayState == null) {
return Future.error(
@@ -487,12 +503,14 @@ class OverlayDialogManager {
Offstage(
offstage: !showCancel,
child: Center(
- child: TextButton(
- style: flatButtonStyle,
- onPressed: cancel,
- child: Text(translate('Cancel'),
- style:
- const TextStyle(color: MyTheme.accent)))))
+ child: isDesktop
+ ? dialogButton('Cancel', onPressed: cancel)
+ : TextButton(
+ style: flatButtonStyle,
+ onPressed: cancel,
+ child: Text(translate('Cancel'),
+ style: const TextStyle(
+ color: MyTheme.accent)))))
])),
onCancel: showCancel ? cancel : null,
);
@@ -508,7 +526,8 @@ class OverlayDialogManager {
void showMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry != null) return;
- if (_overlayState == null) return;
+ final overlayState = _overlayKeyState.state;
+ if (overlayState == null) return;
// compute overlay position
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
@@ -534,7 +553,7 @@ class OverlayDialogManager {
onHidePressed: () => hideMobileActionsOverlay(),
);
});
- _overlayState!.insert(overlay);
+ overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay;
}
@@ -618,6 +637,7 @@ class CustomAlertDialog extends StatelessWidget {
if (!scopeNode.hasFocus) scopeNode.requestFocus();
});
const double padding = 16;
+ bool tabTapped = false;
return FocusScope(
node: scopeNode,
autofocus: true,
@@ -627,13 +647,15 @@ class CustomAlertDialog extends StatelessWidget {
onCancel?.call();
}
return KeyEventResult.handled; // avoid TextField exception on escape
- } else if (onSubmit != null &&
+ } else if (!tabTapped &&
+ onSubmit != null &&
key.logicalKey == LogicalKeyboardKey.enter) {
if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
if (key is RawKeyDownEvent) {
scopeNode.nextFocus();
+ tabTapped = true;
}
return KeyEventResult.handled;
}
@@ -642,8 +664,9 @@ class CustomAlertDialog extends StatelessWidget {
child: AlertDialog(
scrollable: true,
title: title,
- contentPadding: EdgeInsets.fromLTRB(
- contentPadding ?? padding, 25, contentPadding ?? padding, 10),
+ titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0),
+ contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25,
+ contentPadding ?? padding, actions is List ? 10 : padding),
content: ConstrainedBox(
constraints: contentBoxConstraints,
child: Theme(
@@ -653,7 +676,7 @@ class CustomAlertDialog extends StatelessWidget {
child: content),
),
actions: actions,
- actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding),
+ actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding),
),
);
}
@@ -661,7 +684,7 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox(String id, String type, String title, String text, String link,
OverlayDialogManager dialogManager,
- {bool? hasCancel}) {
+ {bool? hasCancel, ReconnectHandle? reconnect}) {
dialogManager.dismissAll();
List buttons = [];
bool hasOk = false;
@@ -701,6 +724,13 @@ void msgBox(String id, String type, String title, String text, String link,
dialogManager.dismissAll();
}));
}
+ if (reconnect != null && title == "Connection Error") {
+ buttons.insert(
+ 0,
+ dialogButton('Reconnect', isOutline: true, onPressed: () {
+ reconnect(dialogManager, id, false);
+ }));
+ }
if (link.isNotEmpty) {
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
}
@@ -1393,13 +1423,14 @@ bool callUniLinksUriHandler(Uri uri) {
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
- required bool isRDP}) async {
+ required bool isRDP,
+ bool? forceRelay}) async {
if (isFileTransfer) {
- await rustDeskWinManager.newFileTransfer(id);
+ await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
- await rustDeskWinManager.newPortForward(id, isRDP);
+ await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
} else {
- await rustDeskWinManager.newRemoteDesktop(id);
+ await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
}
}
@@ -1410,7 +1441,8 @@ connectMainDesktop(String id,
connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
- bool isRDP = false}) async {
+ bool isRDP = false,
+ bool forceRelay = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
assert(!(isFileTransfer && isTcpTunneling && isRDP),
@@ -1418,18 +1450,18 @@ connect(BuildContext context, String id,
if (isDesktop) {
if (desktopType == DesktopType.main) {
- await connectMainDesktop(
- id,
- isFileTransfer: isFileTransfer,
- isTcpTunneling: isTcpTunneling,
- isRDP: isRDP,
- );
+ await connectMainDesktop(id,
+ isFileTransfer: isFileTransfer,
+ isTcpTunneling: isTcpTunneling,
+ isRDP: isRDP,
+ forceRelay: forceRelay);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
+ "forceRelay": forceRelay,
});
}
} else {
@@ -1723,3 +1755,55 @@ Future updateSystemWindowTheme() async {
}
}
}
+
+/// macOS only
+///
+/// Note: not found a general solution for rust based AVFoundation bingding.
+/// [AVFoundation] crate has compile error.
+const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
+
+enum PermissionAuthorizeType {
+ undetermined,
+ authorized,
+ denied, // and restricted
+}
+
+Future osxCanRecordAudio() async {
+ int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
+ print(res);
+ if (res > 0) {
+ return PermissionAuthorizeType.authorized;
+ } else if (res == 0) {
+ return PermissionAuthorizeType.undetermined;
+ } else {
+ return PermissionAuthorizeType.denied;
+ }
+}
+
+Future osxRequestAudio() async {
+ 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;
+}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 5c1e1218c..bd2a01296 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -43,13 +43,10 @@ class _AddressBookState extends State {
return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
return Center(
- child: InkWell(
- onTap: loginDialog,
- child: Text(
- translate("Login"),
- style: const TextStyle(decoration: TextDecoration.underline),
- ),
- ),
+ child: ElevatedButton(
+ onPressed: loginDialog,
+ child: Text(translate("Login"))
+ )
);
} else {
if (gFFI.abModel.abLoading.value) {
@@ -389,7 +386,7 @@ class _AddressBookState extends State {
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index d1d96199a..62f81b797 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -95,10 +95,31 @@ class ChatPage extends StatelessWidget implements PageShape {
color: Theme.of(context).colorScheme.primary)),
messageOptions: MessageOptions(
showOtherUsersAvatar: false,
- showTime: true,
- currentUserTextColor: Colors.white,
textColor: Colors.white,
maxWidth: constraints.maxWidth * 0.7,
+ messageTextBuilder: (message, _, __) {
+ final isOwnMessage =
+ message.user.id == currentUser.id;
+ return Column(
+ crossAxisAlignment: isOwnMessage
+ ? CrossAxisAlignment.end
+ : CrossAxisAlignment.start,
+ children: [
+ Text(message.text,
+ style: TextStyle(color: Colors.white)),
+ Padding(
+ padding: const EdgeInsets.only(top: 5),
+ child: Text(
+ "${message.createdAt.hour}:${message.createdAt.minute}",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 10,
+ ),
+ ),
+ ),
+ ],
+ );
+ },
messageDecorationBuilder: (_, __, ___) =>
defaultMessageDecoration(
color: MyTheme.accent80,
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index 837a197dc..cdce6f12a 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -1,18 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
+abstract class ValidationRule {
+ String get name;
+ bool validate(String value);
+}
+
+class LengthRangeValidationRule extends ValidationRule {
+ final int _min;
+ final int _max;
+
+ LengthRangeValidationRule(this._min, this._max);
+
+ @override
+ String get name => translate('length %min% to %max%')
+ .replaceAll('%min%', _min.toString())
+ .replaceAll('%max%', _max.toString());
+
+ @override
+ bool validate(String value) {
+ return value.length >= _min && value.length <= _max;
+ }
+}
+
+class RegexValidationRule extends ValidationRule {
+ final String _name;
+ final RegExp _regex;
+
+ RegexValidationRule(this._name, this._regex);
+
+ @override
+ String get name => translate(_name);
+
+ @override
+ bool validate(String value) {
+ return value.isNotEmpty ? value.contains(_regex) : false;
+ }
+}
+
void changeIdDialog() {
var newId = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController();
+ final RxString rxId = controller.text.trim().obs;
+
+ final rules = [
+ RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
+ LengthRangeValidationRule(6, 16),
+ RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
+ ];
+
gFFI.dialogManager.show((setState, close) {
submit() async {
debugPrint("onSubmit");
newId = controller.text.trim();
+
+ final Iterable violations = rules.where((r) => !r.validate(newId));
+ if (violations.isNotEmpty) {
+ setState(() {
+ msg =
+ '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
+ });
+ return;
+ }
+
setState(() {
msg = "";
isInProgress = true;
@@ -31,7 +87,7 @@ void changeIdDialog() {
}
setState(() {
isInProgress = false;
- msg = translate(status);
+ msg = '${translate('Prompt')}: ${translate(status)}';
});
}
@@ -46,18 +102,47 @@ void changeIdDialog() {
),
TextField(
decoration: InputDecoration(
+ labelText: translate('Your new ID'),
border: const OutlineInputBorder(),
- errorText: msg.isEmpty ? null : translate(msg)),
+ errorText: msg.isEmpty ? null : translate(msg),
+ suffixText: '${rxId.value.length}/16',
+ suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)),
inputFormatters: [
LengthLimitingTextInputFormatter(16),
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
],
- maxLength: 16,
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
+ onChanged: (value) {
+ setState(() {
+ rxId.value = value.trim();
+ msg = '';
+ });
+ },
),
const SizedBox(
- height: 4.0,
+ height: 8.0,
+ ),
+ Obx(() => Wrap(
+ runSpacing: 8,
+ spacing: 4,
+ children: rules.map((e) {
+ var checked = e.validate(rxId.value);
+ return Chip(
+ label: Text(
+ e.name,
+ style: TextStyle(
+ color: checked
+ ? const Color(0xFF0A9471)
+ : Color.fromARGB(255, 198, 86, 157)),
+ ),
+ backgroundColor: checked
+ ? const Color(0xFFD0F7ED)
+ : Color.fromARGB(255, 247, 205, 232));
+ }).toList(),
+ )),
+ const SizedBox(
+ height: 8.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
@@ -99,7 +184,7 @@ void changeWhiteList({Function()? callback}) async {
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
- focusNode: FocusNode()..requestFocus()),
+ autofocus: true),
),
],
),
@@ -186,7 +271,7 @@ Future changeDirectAccessPort(
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,
- focusNode: FocusNode()..requestFocus()),
+ autofocus: true),
),
],
),
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index 05fc1fc5c..43dc3a658 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -197,24 +197,25 @@ class _WidgetOPState extends State {
_failedMsg = '';
}
return Offstage(
- offstage:
- _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
- child: Row(
- children: [
- Text(
- _stateMsg,
- style: TextStyle(fontSize: 12),
- ),
- SizedBox(width: 8),
- Text(
- _failedMsg,
- style: TextStyle(
- fontSize: 14,
- color: Colors.red,
- ),
+ offstage:
+ _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
+ child: RichText(
+ text: TextSpan(
+ text: '$_stateMsg ',
+ style:
+ DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
+ children: [
+ TextSpan(
+ text: _failedMsg,
+ style: DefaultTextStyle.of(context).style.copyWith(
+ fontSize: 14,
+ color: Colors.red,
+ ),
),
],
- ));
+ ),
+ ),
+ );
}),
Obx(
() => Offstage(
@@ -323,13 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget {
children: [
const SizedBox(height: 8.0),
DialogTextField(
- title: '${translate("Username")}:',
+ title: translate("Username"),
controller: username,
focusNode: userFocusNode,
prefixIcon: Icon(Icons.account_circle_outlined),
errorText: usernameMsg),
DialogTextField(
- title: '${translate("Password")}:',
+ title: translate("Password"),
obscureText: true,
controller: pass,
prefixIcon: Icon(Icons.lock_outline),
diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart
index 4b4172ffd..ba7b8a059 100644
--- a/flutter/lib/common/widgets/overlay.dart
+++ b/flutter/lib/common/widgets/overlay.dart
@@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
+import 'package:get/get.dart';
import 'package:provider/provider.dart';
import '../../consts.dart';
@@ -96,12 +97,14 @@ class DraggableChatWindow extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
- child: Row(children: [
- Icon(Icons.chat_bubble_outline,
- size: 20, color: Theme.of(context).colorScheme.primary),
- SizedBox(width: 6),
- Text(translate("Chat"))
- ])),
+ child: Obx(() => Opacity(
+ opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4,
+ child: Row(children: [
+ Icon(Icons.chat_bubble_outline,
+ size: 20, color: Theme.of(context).colorScheme.primary),
+ SizedBox(width: 6),
+ Text(translate("Chat"))
+ ])))),
Padding(
padding: EdgeInsets.all(2),
child: ActionIcon(
@@ -304,15 +307,17 @@ class _DraggableState extends State {
if (widget.checkKeyboard) {
checkKeyboard();
}
- if (widget.checkKeyboard) {
+ if (widget.checkScreenSize) {
checkScreenSize();
}
- return Positioned(
- top: _position.dy,
- left: _position.dx,
- width: widget.width,
- height: widget.height,
- child: widget.builder(context, onPanUpdate));
+ return Stack(children: [
+ Positioned(
+ top: _position.dy,
+ left: _position.dx,
+ width: widget.width,
+ height: widget.height,
+ child: widget.builder(context, onPanUpdate))
+ ]);
}
}
@@ -366,3 +371,55 @@ class QualityMonitor extends StatelessWidget {
)
: const SizedBox.shrink()));
}
+
+class BlockableOverlayState extends OverlayKeyState {
+ final _middleBlocked = false.obs;
+
+ VoidCallback? onMiddleBlockedClick; // to-do use listener
+
+ RxBool get middleBlocked => _middleBlocked;
+
+ void addMiddleBlockedListener(void Function(bool) cb) {
+ _middleBlocked.listen(cb);
+ }
+
+ void setMiddleBlocked(bool blocked) {
+ if (blocked != _middleBlocked.value) {
+ _middleBlocked.value = blocked;
+ }
+ }
+}
+
+class BlockableOverlay extends StatelessWidget {
+ final Widget underlying;
+ final List? upperLayer;
+
+ final BlockableOverlayState state;
+
+ BlockableOverlay(
+ {required this.underlying, required this.state, this.upperLayer});
+
+ @override
+ Widget build(BuildContext context) {
+ final initialEntries = [
+ OverlayEntry(builder: (_) => underlying),
+
+ /// middle layer
+ OverlayEntry(
+ builder: (context) => Obx(() => Listener(
+ onPointerDown: (_) {
+ state.onMiddleBlockedClick?.call();
+ },
+ child: Container(
+ color:
+ state.middleBlocked.value ? Colors.transparent : null)))),
+ ];
+
+ if (upperLayer != null) {
+ initialEntries.addAll(upperLayer!);
+ }
+
+ /// set key
+ return Overlay(key: state.key, initialEntries: initialEntries);
+ }
+}
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index c9af6328c..f1b94ecdf 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -641,7 +641,7 @@ abstract class BasePeerCard extends StatelessWidget {
child: Form(
child: TextFormField(
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
decoration:
const InputDecoration(border: OutlineInputBorder()),
),
@@ -996,14 +996,11 @@ void _rdpDialog(String id) async {
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
+ constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Port')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
inputFormatters: [
@@ -1013,25 +1010,19 @@ void _rdpDialog(String id) async {
decoration: const InputDecoration(
border: OutlineInputBorder(), hintText: '3389'),
controller: portController,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
+ constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Username')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration:
@@ -1040,19 +1031,15 @@ void _rdpDialog(String id) async {
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text("${translate('Password')}:")
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ "${translate('Password')}:",
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
@@ -1067,7 +1054,7 @@ void _rdpDialog(String id) async {
)),
),
],
- ),
+ ).marginOnly(bottom: 8),
],
),
),
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index 2fb409970..5833e760d 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/state_model.dart';
+import '../../common.dart';
import '../../models/input_model.dart';
class RawKeyFocusScope extends StatelessWidget {
@@ -19,6 +20,13 @@ class RawKeyFocusScope extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final FocusOnKeyCallback? onKey;
+ if (isAndroid) {
+ onKey = inputModel.handleRawKeyEvent;
+ } else {
+ onKey = stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null;
+ }
+
return FocusScope(
autofocus: true,
child: Focus(
@@ -26,8 +34,7 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true,
focusNode: focusNode,
onFocusChange: onFocusChange,
- onKey:
- stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null,
+ onKey: onKey,
child: child));
}
}
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index c95c62fcc..2b4bc7f32 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -50,6 +50,18 @@ const int kMobileMaxDisplayHeight = 1280;
const int kDesktopMaxDisplayWidth = 1920;
const int kDesktopMaxDisplayHeight = 1080;
+const double kDesktopFileTransferNameColWidth = 200;
+const double kDesktopFileTransferModifiedColWidth = 120;
+const double kDesktopFileTransferRowHeight = 25.0;
+const double kDesktopFileTransferHeaderHeight = 25.0;
+
+// https://en.wikipedia.org/wiki/Non-breaking_space
+const int $nbsp = 0x00A0;
+
+extension StringExtension on String {
+ String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp));
+}
+
const Size kConnectionManagerWindowSize = Size(300, 400);
// Tabbar transition duration, now we remove the duration
const Duration kTabTransitionDuration = Duration.zero;
@@ -106,6 +118,12 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom';
+/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
+const kRemoteAudioGuestToHost = 'guest-to-host';
+
+/// [kRemoteAudioDualWay] dual-way audio mode(default).
+const kRemoteAudioDualWay = 'dual-way';
+
const kIgnoreDpi = true;
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index eee4c6a20..646ee2a8d 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -66,7 +66,8 @@ class _ConnectionPageState extends State
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
// select all to faciliate removing text, just following the behavior of address input of chrome
- _idController.selection = TextSelection(baseOffset: 0, extentOffset: _idController.value.text.length);
+ _idController.selection = TextSelection(
+ baseOffset: 0, extentOffset: _idController.value.text.length);
});
windowManager.addListener(this);
}
@@ -120,7 +121,7 @@ class _ConnectionPageState extends State
scrollController: _scrollController,
child: CustomScrollView(
controller: _scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
@@ -149,8 +150,11 @@ class _ConnectionPageState extends State
/// Callback for the connect button.
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
- final id = _idController.id;
- connect(context, id, isFileTransfer: isFileTransfer);
+ var id = _idController.id;
+ var forceRelay = id.endsWith(r'/r');
+ if (forceRelay) id = id.substring(0, id.length - 2);
+ connect(context, id,
+ isFileTransfer: isFileTransfer, forceRelay: forceRelay);
}
/// UI for the remote ID TextField.
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index 0501c298a..b5cadbcdf 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -44,6 +44,7 @@ class _DesktopHomePageState extends State
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
+ var watchIsCanRecordAudio = false;
Timer? _updateTimer;
@override
@@ -74,12 +75,22 @@ class _DesktopHomePageState extends State
scrollController: _leftPaneScrollController,
child: SingleChildScrollView(
controller: _leftPaneScrollController,
+ physics: DraggableNeverScrollableScrollPhysics(),
child: Column(
children: [
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
- buildHelpCards(),
+ FutureBuilder(
+ future: buildHelpCards(),
+ builder: (_, data) {
+ if (data.hasData) {
+ return data.data!;
+ } else {
+ return const Offstage();
+ }
+ },
+ ),
],
),
),
@@ -302,7 +313,7 @@ class _DesktopHomePageState extends State
);
}
- Widget buildHelpCards() {
+ Future buildHelpCards() async {
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@@ -349,6 +360,15 @@ class _DesktopHomePageState extends State
bind.mainIsInstalledDaemon(prompt: true);
});
}
+ //// Disable microphone configuration for macOS. We will request the permission when needed.
+ // else if ((await osxCanRecordAudio() !=
+ // PermissionAuthorizeType.authorized)) {
+ // return buildInstallCard("Permissions", "config_microphone", "Configure",
+ // () async {
+ // osxRequestAudio();
+ // watchIsCanRecordAudio = true;
+ // });
+ // }
} else if (Platform.isLinux) {
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
@@ -481,6 +501,20 @@ class _DesktopHomePageState extends State
setState(() {});
}
}
+ if (watchIsCanRecordAudio) {
+ if (Platform.isMacOS) {
+ Future.microtask(() async {
+ if ((await osxCanRecordAudio() ==
+ PermissionAuthorizeType.authorized)) {
+ watchIsCanRecordAudio = false;
+ setState(() {});
+ }
+ });
+ } else {
+ watchIsCanRecordAudio = false;
+ setState(() {});
+ }
+ }
});
Get.put(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
@@ -523,6 +557,7 @@ class _DesktopHomePageState extends State
isFileTransfer: call.arguments['isFileTransfer'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
+ forceRelay: call.arguments['forceRelay'],
);
}
});
@@ -561,13 +596,13 @@ void setPasswordDialog() async {
});
final pass = p0.text.trim();
if (pass.isNotEmpty) {
- for (var r in rules) {
- if (!r.validate(pass)) {
- setState(() {
- errMsg0 = '${translate('Prompt')}: ${r.name}';
- });
- return;
- }
+ final Iterable violations = rules.where((r) => !r.validate(pass));
+ if (violations.isNotEmpty) {
+ setState(() {
+ errMsg0 =
+ '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
+ });
+ return;
}
}
if (p1.text.trim() != pass) {
@@ -601,9 +636,12 @@ void setPasswordDialog() async {
border: const OutlineInputBorder(),
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
controller: p0,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
onChanged: (value) {
rxPass.value = value.trim();
+ setState(() {
+ errMsg0 = '';
+ });
},
),
),
@@ -627,6 +665,11 @@ void setPasswordDialog() async {
labelText: translate('Confirmation'),
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
controller: p1,
+ onChanged: (value) {
+ setState(() {
+ errMsg1 = '';
+ });
+ },
),
),
],
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index 4b6cf2a62..971c713ce 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -128,7 +128,7 @@ class _DesktopSettingPageState extends State
scrollController: controller,
child: PageView(
controller: controller,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: const [
_General(),
_Safety(),
@@ -170,7 +170,7 @@ class _DesktopSettingPageState extends State
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: tabs
.asMap()
@@ -234,7 +234,7 @@ class _GeneralState extends State<_General> {
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
theme(),
@@ -456,7 +456,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return DesktopScrollWrapper(
scrollController: scrollController,
child: SingleChildScrollView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
child: Column(
children: [
@@ -650,7 +650,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
context, onChanged != null)),
),
],
- ).paddingSymmetric(horizontal: 10),
+ ).paddingOnly(right: 10),
onTap: () => onChanged?.call(value),
))
.toList();
@@ -675,6 +675,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword) radios[0],
if (usePassword)
_SubLabeledWidget(
+ context,
'One-time password length',
Row(
children: [
@@ -701,6 +702,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
enabled: enabled),
),
+ shareRdp(context, enabled),
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled),
...directIp(context),
@@ -708,6 +710,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
]);
}
+ shareRdp(BuildContext context, bool enabled) {
+ onChanged(bool b) async {
+ await bind.mainSetShareRdp(enable: b);
+ setState(() {});
+ }
+
+ bool value = bind.mainIsShareRdp();
+ return Offstage(
+ offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()),
+ child: GestureDetector(
+ child: Row(
+ children: [
+ Checkbox(
+ value: value,
+ onChanged: enabled ? (_) => onChanged(!value) : null)
+ .marginOnly(right: 5),
+ Expanded(
+ child: Text(translate('Enable RDP session sharing'),
+ style:
+ TextStyle(color: _disabledTextColor(context, enabled))),
+ )
+ ],
+ ).marginOnly(left: _kCheckBoxLeftMargin),
+ onTap: enabled ? () => onChanged(!value) : null),
+ );
+ }
+
List directIp(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
@@ -728,9 +757,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
controller.text = data['port'].toString();
return Offstage(
offstage: !enabled,
- child: Row(children: [
- _SubLabeledWidget(
- 'Port',
+ child: _SubLabeledWidget(
+ context,
+ 'Port',
+ Row(children: [
SizedBox(
width: 80,
child: TextField(
@@ -744,28 +774,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
textAlign: TextAlign.end,
decoration: const InputDecoration(
hintText: '21118',
- border: InputBorder.none,
- contentPadding: EdgeInsets.only(right: 5),
+ border: OutlineInputBorder(),
+ contentPadding:
+ EdgeInsets.only(bottom: 10, top: 10, right: 10),
isCollapsed: true,
),
- ),
+ ).marginOnly(right: 15),
),
- enabled: enabled && !locked,
- ).marginOnly(left: 5),
- Obx(() => ElevatedButton(
- onPressed: applyEnabled.value && enabled && !locked
- ? () async {
- applyEnabled.value = false;
- await bind.mainSetOption(
- key: 'direct-access-port',
- value: controller.text);
- }
- : null,
- child: Text(
- translate('Apply'),
- ),
- ).marginOnly(left: 20))
- ]),
+ Obx(() => ElevatedButton(
+ onPressed: applyEnabled.value && enabled && !locked
+ ? () async {
+ applyEnabled.value = false;
+ await bind.mainSetOption(
+ key: 'direct-access-port',
+ value: controller.text);
+ }
+ : null,
+ child: Text(
+ translate('Apply'),
+ ),
+ ))
+ ]),
+ enabled: enabled && !locked,
+ ),
);
},
),
@@ -880,7 +911,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
scrollController: scrollController,
child: ListView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: [
_lock(locked, 'Unlock Network Settings', () {
locked = false;
@@ -1043,7 +1074,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
- ).marginOnly(top: 15),
+ ).marginOnly(top: 10),
],
)
]);
@@ -1066,7 +1097,7 @@ class _DisplayState extends State<_Display> {
scrollController: scrollController,
child: ListView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: [
viewStyle(context),
scrollStyle(context),
@@ -1306,7 +1337,7 @@ class _AccountState extends State<_Account> {
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
_Card(title: 'Account', children: [accountAction()]),
@@ -1350,7 +1381,7 @@ class _AboutState extends State<_About> {
scrollController: scrollController,
child: SingleChildScrollView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
child: _Card(title: '${translate('About')} RustDesk', children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1586,43 +1617,18 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) {
}
// ignore: non_constant_identifier_names
-Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) {
- RxBool hover = false.obs;
+Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
+ {bool enabled = true}) {
return Row(
children: [
- MouseRegion(
- onEnter: (_) => hover.value = true,
- onExit: (_) => hover.value = false,
- child: Obx(
- () {
- return Container(
- height: 32,
- decoration: BoxDecoration(
- border: Border.all(
- color: hover.value && enabled
- ? const Color(0xFFD7D7D7)
- : const Color(0xFFCBCBCB),
- width: hover.value && enabled ? 2 : 1)),
- child: Row(
- children: [
- Container(
- height: 28,
- color: (hover.value && enabled)
- ? const Color(0xFFD7D7D7)
- : const Color(0xFFCBCBCB),
- alignment: Alignment.center,
- padding: const EdgeInsets.symmetric(
- horizontal: 5, vertical: 2),
- child: Text(
- '${translate(label)}: ',
- style: const TextStyle(fontWeight: FontWeight.w300),
- ),
- ).paddingAll(2),
- child,
- ],
- ));
- },
- )),
+ Text(
+ '${translate(label)}: ',
+ style: TextStyle(color: _disabledTextColor(context, enabled)),
+ ),
+ SizedBox(
+ width: 10,
+ ),
+ child,
],
).marginOnly(left: _kContentHSubMargin);
}
@@ -1691,33 +1697,30 @@ _LabeledTextField(
bool secure) {
return Row(
children: [
- Spacer(flex: 1),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate(label)}:',
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ fontSize: 16, color: _disabledTextColor(context, enabled)),
+ ).marginOnly(right: 10)),
Expanded(
- flex: 4,
- child: Text(
- '${translate(label)}:',
- textAlign: TextAlign.right,
- style: TextStyle(color: _disabledTextColor(context, enabled)),
- ),
- ),
- Spacer(flex: 1),
- Expanded(
- flex: 10,
child: TextField(
controller: controller,
enabled: enabled,
obscureText: secure,
decoration: InputDecoration(
isDense: true,
- contentPadding: EdgeInsets.symmetric(vertical: 15),
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(14, 15, 14, 15),
errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle(
color: _disabledTextColor(context, enabled),
)),
),
- Spacer(flex: 1),
],
- );
+ ).marginOnly(bottom: 8);
}
// ignore: must_be_immutable
@@ -1804,6 +1807,7 @@ void changeSocks5Proxy() async {
var proxyController = TextEditingController(text: proxy);
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: password);
+ RxBool obscure = true.obs;
var isInProgress = false;
gFFI.dialogManager.show((setState, close) {
@@ -1849,35 +1853,30 @@ void changeSocks5Proxy() async {
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Hostname")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Hostname")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
controller: proxyController,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Username")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Username")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration: const InputDecoration(
@@ -1887,32 +1886,30 @@ void changeSocks5Proxy() async {
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Password")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Password")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
- child: TextField(
- decoration: const InputDecoration(
- border: OutlineInputBorder(),
- ),
- controller: pwdController,
- ),
+ child: Obx(() => TextField(
+ obscureText: obscure.value,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ suffixIcon: IconButton(
+ onPressed: () => obscure.value = !obscure.value,
+ icon: Icon(obscure.value
+ ? Icons.visibility_off
+ : Icons.visibility))),
+ controller: pwdController,
+ )),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart
index c1965921c..35d5a61ef 100644
--- a/flutter/lib/desktop/pages/desktop_tab_page.dart
+++ b/flutter/lib/desktop/pages/desktop_tab_page.dart
@@ -64,23 +64,17 @@ class _DesktopTabPageState extends State {
@override
Widget build(BuildContext context) {
final tabWidget = Container(
- child: Overlay(initialEntries: [
- OverlayEntry(builder: (context) {
- gFFI.dialogManager.setOverlayState(Overlay.of(context));
- return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
- body: DesktopTab(
- controller: tabController,
- tail: ActionIcon(
- message: 'Settings',
- icon: IconFont.menu,
- onTap: DesktopTabPage.onAddSetting,
- isClose: false,
- ),
- ));
- })
- ]),
- );
+ child: Scaffold(
+ backgroundColor: Theme.of(context).backgroundColor,
+ body: DesktopTab(
+ controller: tabController,
+ tail: ActionIcon(
+ message: 'Settings',
+ icon: IconFont.menu,
+ onTap: DesktopTabPage.onAddSetting,
+ isClose: false,
+ ),
+ )));
return Platform.isMacOS
? tabWidget
: Obx(
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index b6a9e5fed..4edffb3b6 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -46,8 +46,10 @@ enum MouseFocusScope {
}
class FileManagerPage extends StatefulWidget {
- const FileManagerPage({Key? key, required this.id}) : super(key: key);
+ const FileManagerPage({Key? key, required this.id, this.forceRelay})
+ : super(key: key);
final String id;
+ final bool? forceRelay;
@override
State createState() => _FileManagerPageState();
@@ -80,6 +82,7 @@ class _FileManagerPageState extends State
Entry? _lastClickEntry;
final _dropMaskVisible = false.obs; // TODO impl drop mask
+ final _overlayKeyState = OverlayKeyState();
ScrollController getBreadCrumbScrollController(bool isLocal) {
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
@@ -101,7 +104,7 @@ class _FileManagerPageState extends State
void initState() {
super.initState();
_ffi = FFI();
- _ffi.start(widget.id, isFileTransfer: true);
+ _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
@@ -115,6 +118,7 @@ class _FileManagerPageState extends State
// register location listener
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
+ _ffi.dialogManager.setOverlayState(_overlayKeyState);
}
@override
@@ -137,9 +141,8 @@ class _FileManagerPageState extends State
@override
Widget build(BuildContext context) {
super.build(context);
- return Overlay(initialEntries: [
- OverlayEntry(builder: (context) {
- _ffi.dialogManager.setOverlayState(Overlay.of(context));
+ return Overlay(key: _overlayKeyState.key, initialEntries: [
+ OverlayEntry(builder: (_) {
return ChangeNotifierProvider.value(
value: _ffi.fileModel,
child: Consumer(builder: (context, model, child) {
@@ -235,10 +238,7 @@ class _FileManagerPageState extends State
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
- child: SingleChildScrollView(
- controller: scrollController,
- child: _buildDataTable(context, isLocal, scrollController),
- ),
+ child: _buildFileList(context, isLocal, scrollController),
)
],
)),
@@ -247,25 +247,11 @@ class _FileManagerPageState extends State
);
}
- Widget _buildDataTable(
+ Widget _buildFileList(
BuildContext context, bool isLocal, ScrollController scrollController) {
- const rowHeight = 25.0;
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
- final sortIndex = (SortBy style) {
- switch (style) {
- case SortBy.name:
- return 0;
- case SortBy.type:
- return 0;
- case SortBy.modified:
- return 1;
- case SortBy.size:
- return 2;
- }
- }(model.getSortStyle(isLocal));
- final sortAscending =
- isLocal ? model.localSortAscending : model.remoteSortAscending;
+ final selectedEntries = getSelectedItems(isLocal);
return MouseRegion(
onEnter: (evt) {
@@ -286,7 +272,6 @@ class _FileManagerPageState extends State
onNext: (buffer) {
debugPrint("searching next for $buffer");
assert(buffer.length == 1);
- final selectedEntries = getSelectedItems(isLocal);
assert(selectedEntries.length <= 1);
var skipCount = 0;
if (selectedEntries.items.isNotEmpty) {
@@ -311,7 +296,8 @@ class _FileManagerPageState extends State
return;
}
_jumpToEntry(
- isLocal, searchResult.first, scrollController, rowHeight, buffer);
+ isLocal, searchResult.first, scrollController,
+ kDesktopFileTransferRowHeight, buffer);
},
onSearch: (buffer) {
debugPrint("searching for $buffer");
@@ -326,7 +312,8 @@ class _FileManagerPageState extends State
return;
}
_jumpToEntry(
- isLocal, searchResult.first, scrollController, rowHeight, buffer);
+ isLocal, searchResult.first, scrollController,
+ kDesktopFileTransferRowHeight, buffer);
},
child: ObxValue(
(searchText) {
@@ -335,118 +322,120 @@ class _FileManagerPageState extends State
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
- return DataTable(
- key: ValueKey(isLocal ? 0 : 1),
- showCheckboxColumn: false,
- dataRowHeight: rowHeight,
- headingRowHeight: 30,
- horizontalMargin: 8,
- columnSpacing: 8,
- showBottomBorder: true,
- sortColumnIndex: sortIndex,
- sortAscending: sortAscending,
- columns: [
- DataColumn(
- label: Text(
- translate("Name"),
- ).marginSymmetric(horizontal: 4),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.name,
- isLocal: isLocal, ascending: ascending);
- }),
- DataColumn(
- label: Text(
- translate("Modified"),
- ),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.modified,
- isLocal: isLocal, ascending: ascending);
- }),
- DataColumn(
- label: Text(translate("Size")),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.size,
- isLocal: isLocal, ascending: ascending);
- }),
- ],
- rows: filteredEntries.map((entry) {
+ final rows = filteredEntries.map((entry) {
final sizeStr =
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
final lastModifiedStr = entry.isDrive
? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
- return DataRow(
- key: ValueKey(entry.name),
- onSelectChanged: (s) {
- _onSelectedChanged(getSelectedItems(isLocal),
- filteredEntries, entry, isLocal);
- },
- selected: getSelectedItems(isLocal).contains(entry),
- cells: [
- DataCell(
- Container(
- width: 200,
- child: Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: entry.name,
- child: Row(children: [
- entry.isDrive
- ? Image(
- image: iconHardDrive,
- fit: BoxFit.scaleDown,
- color: Theme.of(context)
- .iconTheme
- .color
- ?.withOpacity(0.7))
- .paddingAll(4)
- : Icon(
- entry.isFile
- ? Icons.feed_outlined
- : Icons.folder,
- size: 20,
- color: Theme.of(context)
- .iconTheme
- .color
- ?.withOpacity(0.7),
- ).marginSymmetric(horizontal: 2),
- Expanded(
- child: Text(entry.name,
- overflow: TextOverflow.ellipsis))
- ]),
+ final isSelected = selectedEntries.contains(entry);
+ return SizedBox(
+ key: ValueKey(entry.name),
+ height: kDesktopFileTransferRowHeight,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ const Divider(
+ height: 1,
+ ),
+ Expanded(
+ child: Ink(
+ decoration: isSelected
+ ? BoxDecoration(color: Theme.of(context).hoverColor)
+ : null,
+ child: InkWell(
+ child: Row(children: [
+ GestureDetector(
+ child: Container(
+ width: kDesktopFileTransferNameColWidth,
+ child: Tooltip(
+ waitDuration: Duration(milliseconds: 500),
+ message: entry.name,
+ child: Row(children: [
+ entry.isDrive
+ ? Image(
+ image: iconHardDrive,
+ fit: BoxFit.scaleDown,
+ color: Theme.of(context)
+ .iconTheme
+ .color
+ ?.withOpacity(0.7))
+ .paddingAll(4)
+ : Icon(
+ entry.isFile
+ ? Icons.feed_outlined
+ : Icons.folder,
+ size: 20,
+ color: Theme.of(context)
+ .iconTheme
+ .color
+ ?.withOpacity(0.7),
+ ).marginSymmetric(horizontal: 2),
+ Expanded(
+ child: Text(entry.name.nonBreaking,
+ overflow: TextOverflow.ellipsis))
+ ]),
+ )),
+ onTap: () {
+ final items = getSelectedItems(isLocal);
+ // handle double click
+ if (_checkDoubleClick(entry)) {
+ openDirectory(entry.path, isLocal: isLocal);
+ items.clear();
+ return;
+ }
+ _onSelectedChanged(
+ items, filteredEntries, entry, isLocal);
+ },
+ ),
+ GestureDetector(
+ child: SizedBox(
+ width: kDesktopFileTransferModifiedColWidth,
+ child: Tooltip(
+ waitDuration: Duration(milliseconds: 500),
+ message: lastModifiedStr,
+ child: Text(
+ lastModifiedStr,
+ style: TextStyle(
+ fontSize: 12, color: MyTheme.darkGray),
+ )),
)),
- onTap: () {
- final items = getSelectedItems(isLocal);
-
- // handle double click
- if (_checkDoubleClick(entry)) {
- openDirectory(entry.path, isLocal: isLocal);
- items.clear();
- return;
- }
- _onSelectedChanged(
- items, filteredEntries, entry, isLocal);
- },
+ GestureDetector(
+ child: Tooltip(
+ waitDuration: Duration(milliseconds: 500),
+ message: sizeStr,
+ child: Text(
+ sizeStr,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ fontSize: 10,
+ color: MyTheme.darkGray),
+ ))),
+ ]),
+ ),
),
- DataCell(FittedBox(
- child: Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: lastModifiedStr,
- child: Text(
- lastModifiedStr,
- style: TextStyle(
- fontSize: 12, color: MyTheme.darkGray),
- )))),
- DataCell(Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: sizeStr,
- child: Text(
- sizeStr,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- fontSize: 10, color: MyTheme.darkGray),
- ))),
- ]);
- }).toList(growable: false),
+ ),
+ ],
+ ),
+ );
+ }).toList(growable: false);
+
+ return Column(
+ children: [
+ // Header
+ _buildFileBrowserHeader(context, isLocal),
+ // Body
+ Expanded(
+ child: ListView.builder(
+ controller: scrollController,
+ itemExtent: kDesktopFileTransferRowHeight,
+ itemBuilder: (context, index) {
+ return rows[index];
+ },
+ itemCount: rows.length,
+ ),
+ ),
+ ],
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
@@ -797,7 +786,7 @@ class _FileManagerPageState extends State
"Please enter the folder name"),
),
controller: name,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
],
),
@@ -1132,4 +1121,60 @@ class _FileManagerPageState extends State
}
});
}
+
+ Widget headerItemFunc(
+ double? width, SortBy sortBy, String name, bool isLocal) {
+ final headerTextStyle =
+ Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle();
+ return ObxValue>(
+ (ascending) => InkWell(
+ onTap: () {
+ if (ascending.value == null) {
+ ascending.value = true;
+ } else {
+ ascending.value = !ascending.value!;
+ }
+ model.changeSortStyle(sortBy,
+ isLocal: isLocal, ascending: ascending.value!);
+ },
+ child: SizedBox(
+ width: width,
+ height: kDesktopFileTransferHeaderHeight,
+ child: Row(
+ children: [
+ Text(
+ name,
+ style: headerTextStyle,
+ ).marginSymmetric(
+ horizontal: sortBy == SortBy.name ? 4 : 0.0),
+ ascending.value != null
+ ? Icon(ascending.value!
+ ? Icons.arrow_upward
+ : Icons.arrow_downward)
+ : const Offstage()
+ ],
+ ),
+ ),
+ ), () {
+ if (model.getSortStyle(isLocal) == sortBy) {
+ return model.getSortAscending(isLocal).obs;
+ } else {
+ return Rx(null);
+ }
+ }());
+ }
+
+ Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) {
+ return Row(
+ children: [
+ headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name,
+ translate("Name"), isLocal),
+ headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified,
+ translate("Modified"), isLocal),
+ Expanded(
+ child:
+ headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
+ ],
+ );
+ }
}
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index b2566e267..7540f7662 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -41,7 +41,11 @@ class _FileManagerTabPageState extends State {
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => () => tabController.closeBy(params['id']),
- page: FileManagerPage(key: ValueKey(params['id']), id: params['id'])));
+ page: FileManagerPage(
+ key: ValueKey(params['id']),
+ id: params['id'],
+ forceRelay: params['forceRelay'],
+ )));
}
@override
@@ -64,7 +68,11 @@ class _FileManagerTabPageState extends State {
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
- page: FileManagerPage(key: ValueKey(id), id: id)));
+ page: FileManagerPage(
+ key: ValueKey(id),
+ id: id,
+ forceRelay: args['forceRelay'],
+ )));
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index f513a1c6a..2ac6bf23a 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -26,10 +26,12 @@ class _PortForward {
}
class PortForwardPage extends StatefulWidget {
- const PortForwardPage({Key? key, required this.id, required this.isRDP})
+ const PortForwardPage(
+ {Key? key, required this.id, required this.isRDP, this.forceRelay})
: super(key: key);
final String id;
final bool isRDP;
+ final bool? forceRelay;
@override
State createState() => _PortForwardPageState();
@@ -47,7 +49,7 @@ class _PortForwardPageState extends State
void initState() {
super.initState();
_ffi = FFI();
- _ffi.start(widget.id, isPortForward: true);
+ _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay);
Get.put(_ffi, tag: 'pf_${widget.id}');
if (!Platform.isLinux) {
Wakelock.enable();
@@ -179,36 +181,33 @@ class _PortForwardPageState extends State
buildTunnelInputCell(context,
controller: remotePortController,
inputFormatters: portInputFormatter),
- SizedBox(
- width: _kColumn4Width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0, side: const BorderSide(color: MyTheme.border)),
- onPressed: () async {
- int? localPort = int.tryParse(localPortController.text);
- int? remotePort = int.tryParse(remotePortController.text);
- if (localPort != null &&
- remotePort != null &&
- (remoteHostController.text.isEmpty ||
- remoteHostController.text.trim().isNotEmpty)) {
- await bind.sessionAddPortForward(
- id: 'pf_${widget.id}',
- localPort: localPort,
- remoteHost: remoteHostController.text.trim().isEmpty
- ? 'localhost'
- : remoteHostController.text.trim(),
- remotePort: remotePort);
- localPortController.clear();
- remoteHostController.clear();
- remotePortController.clear();
- refreshTunnelConfig();
- }
- },
- child: Text(
- translate('Add'),
- ),
- ).marginAll(10),
- ),
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ elevation: 0, side: const BorderSide(color: MyTheme.border)),
+ onPressed: () async {
+ int? localPort = int.tryParse(localPortController.text);
+ int? remotePort = int.tryParse(remotePortController.text);
+ if (localPort != null &&
+ remotePort != null &&
+ (remoteHostController.text.isEmpty ||
+ remoteHostController.text.trim().isNotEmpty)) {
+ await bind.sessionAddPortForward(
+ id: 'pf_${widget.id}',
+ localPort: localPort,
+ remoteHost: remoteHostController.text.trim().isEmpty
+ ? 'localhost'
+ : remoteHostController.text.trim(),
+ remotePort: remotePort);
+ localPortController.clear();
+ remoteHostController.clear();
+ remotePortController.clear();
+ refreshTunnelConfig();
+ }
+ },
+ child: Text(
+ translate('Add'),
+ ),
+ ).marginAll(10),
]),
);
}
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index ca354f297..ee5dd9b53 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State {
key: ValueKey(params['id']),
id: params['id'],
isRDP: isRDP,
+ forceRelay: params['forceRelay'],
)));
}
@@ -72,7 +73,12 @@ class _PortForwardTabPageState extends State {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
- page: PortForwardPage(id: id, isRDP: isRDP)));
+ page: PortForwardPage(
+ key: ValueKey(args['id']),
+ id: id,
+ isRDP: isRDP,
+ forceRelay: args['forceRelay'],
+ )));
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index 0e0127312..f9db985d9 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -21,6 +21,7 @@ import '../../mobile/widgets/dialog.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
+import '../../utils/image.dart';
import '../widgets/remote_menubar.dart';
import '../widgets/kb_layout_type_chooser.dart';
@@ -33,11 +34,13 @@ class RemotePage extends StatefulWidget {
required this.id,
required this.menubarState,
this.switchUuid,
+ this.forceRelay,
}) : super(key: key);
final String id;
final MenubarState menubarState;
final String? switchUuid;
+ final bool? forceRelay;
final SimpleWrapper?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
@@ -61,6 +64,8 @@ class _RemotePageState extends State
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
+ final _blockableOverlayState = BlockableOverlayState();
+
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
Function(bool)? _onEnterOrLeaveImage4Menubar;
@@ -104,6 +109,7 @@ class _RemotePageState extends State
_ffi.start(
widget.id,
switchUuid: widget.switchUuid,
+ forceRelay: widget.forceRelay,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -132,6 +138,13 @@ class _RemotePageState extends State
// });
// _isCustomCursorInited = true;
// }
+
+ _ffi.dialogManager.setOverlayState(_blockableOverlayState);
+ _ffi.chatModel.setOverlayState(_blockableOverlayState);
+ // make remote page penetrable automatically, effective for chat over remote
+ _blockableOverlayState.onMiddleBlockedClick = () {
+ _blockableOverlayState.setMiddleBlocked(false);
+ };
}
@override
@@ -191,39 +204,50 @@ class _RemotePageState extends State
Widget buildBody(BuildContext context) {
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
- body: Overlay(
- initialEntries: [
- OverlayEntry(builder: (context) {
- _ffi.chatModel.setOverlayState(Overlay.of(context));
- _ffi.dialogManager.setOverlayState(Overlay.of(context));
- return Container(
- color: Colors.black,
- child: RawKeyFocusScope(
- focusNode: _rawKeyFocusNode,
- onFocusChange: (bool imageFocused) {
- debugPrint(
- "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
- // See [onWindowBlur].
- if (Platform.isWindows) {
- if (_isWindowBlur) {
- imageFocused = false;
- Future.delayed(Duration.zero, () {
- _rawKeyFocusNode.unfocus();
- });
- }
- if (imageFocused) {
- _ffi.inputModel.enterOrLeave(true);
- } else {
- _ffi.inputModel.enterOrLeave(false);
- }
- }
- },
- inputModel: _ffi.inputModel,
- child: getBodyForDesktop(context)));
- })
- ],
- ));
+ backgroundColor: Theme.of(context).backgroundColor,
+
+ /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
+ /// see override build() in [BlockableOverlay]
+ body: BlockableOverlay(
+ state: _blockableOverlayState,
+ underlying: Container(
+ color: Colors.black,
+ child: RawKeyFocusScope(
+ focusNode: _rawKeyFocusNode,
+ onFocusChange: (bool imageFocused) {
+ debugPrint(
+ "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
+ // See [onWindowBlur].
+ if (Platform.isWindows) {
+ if (_isWindowBlur) {
+ imageFocused = false;
+ Future.delayed(Duration.zero, () {
+ _rawKeyFocusNode.unfocus();
+ });
+ }
+ if (imageFocused) {
+ _ffi.inputModel.enterOrLeave(true);
+ } else {
+ _ffi.inputModel.enterOrLeave(false);
+ }
+ }
+ },
+ inputModel: _ffi.inputModel,
+ child: getBodyForDesktop(context))),
+ upperLayer: [
+ OverlayEntry(
+ builder: (context) => RemoteMenubar(
+ id: widget.id,
+ ffi: _ffi,
+ state: widget.menubarState,
+ onEnterOrLeaveImageSetter: (func) =>
+ _onEnterOrLeaveImage4Menubar = func,
+ onEnterOrLeaveImageCleaner: () =>
+ _onEnterOrLeaveImage4Menubar = null,
+ ))
+ ],
+ ),
+ );
}
@override
@@ -344,13 +368,6 @@ class _RemotePageState extends State
QualityMonitor(_ffi.qualityMonitorModel), null, null),
),
);
- paints.add(RemoteMenubar(
- id: widget.id,
- ffi: _ffi,
- state: widget.menubarState,
- onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func,
- onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null,
- ));
return Stack(
children: paints,
);
@@ -672,40 +689,3 @@ class CursorPaint extends StatelessWidget {
);
}
}
-
-class ImagePainter extends CustomPainter {
- ImagePainter({
- required this.image,
- required this.x,
- required this.y,
- required this.scale,
- });
-
- ui.Image? image;
- double x;
- double y;
- double scale;
-
- @override
- void paint(Canvas canvas, Size size) {
- if (image == null) return;
- if (x.isNaN || y.isNaN) return;
- canvas.scale(scale, scale);
- // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
- // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
- var paint = Paint();
- if ((scale - 1.0).abs() > 0.001) {
- paint.filterQuality = FilterQuality.medium;
- if (scale > 10.00000) {
- paint.filterQuality = FilterQuality.high;
- }
- }
- canvas.drawImage(
- image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
- }
-
- @override
- bool shouldRepaint(CustomPainter oldDelegate) {
- return oldDelegate != this;
- }
-}
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 9b00b481f..64c78f24d 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -22,7 +22,10 @@ import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
- static const Color commonColor = MyTheme.accent;
+ static const Color blueColor = MyTheme.button;
+ static const Color hoverBlueColor = MyTheme.accent;
+ static const Color redColor = Colors.redAccent;
+ static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
@@ -70,6 +73,7 @@ class _ConnectionTabPageState extends State {
id: peerId,
menubarState: _menubarState,
switchUuid: params['switch_uuid'],
+ forceRelay: params['forceRelay'],
),
));
_update_remote_count();
@@ -104,6 +108,7 @@ class _ConnectionTabPageState extends State {
id: id,
menubarState: _menubarState,
switchUuid: switchUuid,
+ forceRelay: args['forceRelay'],
),
));
} else if (call.method == "onDestroy") {
@@ -280,7 +285,7 @@ class _ConnectionTabPageState extends State {
.map((entry) => entry.build(
context,
const MenuConfig(
- commonColor: _MenuTheme.commonColor,
+ commonColor: _MenuTheme.blueColor,
height: _MenuTheme.height,
dividerHeight: _MenuTheme.dividerHeight,
)))
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index 521413647..252e1cd12 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -1,11 +1,13 @@
// original cm window in Sciter version.
import 'dart:async';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
+import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
@@ -47,8 +49,17 @@ class _DesktopServerPageState extends State
@override
void onWindowClose() {
- gFFI.serverModel.closeAll();
- gFFI.close();
+ Future.wait([
+ gFFI.serverModel.closeAll(),
+ gFFI.close()
+ ]).then((_) {
+ if (Platform.isMacOS) {
+ RdPlatformChannel.instance.terminate();
+ } else {
+ windowManager.setPreventClose(false);
+ windowManager.close();
+ }
+ });
super.onWindowClose();
}
@@ -68,26 +79,19 @@ class _DesktopServerPageState extends State
],
child: Consumer(
builder: (context, serverModel, child) => Container(
- decoration: BoxDecoration(
- border:
- Border.all(color: MyTheme.color(context).border!)),
- child: Overlay(initialEntries: [
- OverlayEntry(builder: (context) {
- gFFI.dialogManager.setOverlayState(Overlay.of(context));
- return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- Expanded(child: ConnectionManager()),
- ],
- ),
- ),
- );
- })
- ]),
- )));
+ decoration: BoxDecoration(
+ border: Border.all(color: MyTheme.color(context).border!)),
+ child: Scaffold(
+ backgroundColor: Theme.of(context).backgroundColor,
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Expanded(child: ConnectionManager()),
+ ],
+ ),
+ ),
+ ))));
}
@override
@@ -521,6 +525,39 @@ class _CmControlPanel extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
+ Offstage(
+ offstage: !client.inVoiceCall,
+ child: buildButton(context,
+ color: Colors.red,
+ onClick: () => closeVoiceCall(),
+ icon: Icon(Icons.phone_disabled_rounded, color: Colors.white),
+ text: "Stop voice call",
+ textColor: Colors.white),
+ ),
+ Offstage(
+ offstage: !client.incomingVoiceCall,
+ child: Row(
+ children: [
+ Expanded(
+ child: buildButton(context,
+ color: MyTheme.accent,
+ onClick: () => handleVoiceCall(true),
+ icon: Icon(Icons.phone_enabled, color: Colors.white),
+ text: "Accept",
+ textColor: Colors.white),
+ ),
+ Expanded(
+ child: buildButton(context,
+ color: Colors.red,
+ onClick: () => handleVoiceCall(false),
+ icon:
+ Icon(Icons.phone_disabled_rounded, color: Colors.white),
+ text: "Dismiss",
+ textColor: Colors.white),
+ )
+ ],
+ ),
+ ),
Offstage(
offstage: !client.fromSwitch,
child: buildButton(context,
@@ -626,7 +663,7 @@ class _CmControlPanel extends StatelessWidget {
.marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
}
- buildButton(
+ Widget buildButton(
BuildContext context, {
required Color? color,
required Function() onClick,
@@ -692,6 +729,14 @@ class _CmControlPanel extends StatelessWidget {
void handleSwitchBack(BuildContext context) {
bind.cmSwitchBack(connId: client.id);
}
+
+ void handleVoiceCall(bool accept) {
+ bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
+ }
+
+ void closeVoiceCall() {
+ bind.cmCloseVoiceCall(id: client.id);
+ }
}
void checkClickTime(int id, Function() callback) async {
diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
index 666c9a6e2..3e85cb296 100644
--- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
+++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
@@ -5,6 +5,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
// Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame }
@@ -1391,22 +1393,20 @@ class PopupMenuButtonState extends State> {
onTap: widget.enabled ? showButtonMenu : null,
onHover: widget.onHover,
canRequestFocus: _canRequestFocus,
- radius: widget.splashRadius,
enableFeedback: enableFeedback,
child: widget.child,
),
);
}
- return IconButton(
- icon: widget.icon ?? Icon(Icons.adaptive.more),
- padding: widget.padding,
- splashRadius: widget.splashRadius,
- iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize,
+ return MenuButton(
+ child: widget.icon ?? Icon(Icons.adaptive.more),
tooltip:
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
onPressed: widget.enabled ? showButtonMenu : null,
enableFeedback: enableFeedback,
+ color: MyTheme.button,
+ hoverColor: MyTheme.accent,
);
}
}
diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart
new file mode 100644
index 000000000..96cc9fa9b
--- /dev/null
+++ b/flutter/lib/desktop/widgets/menu_button.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+
+class MenuButton extends StatefulWidget {
+ final GestureTapCallback? onPressed;
+ final Color color;
+ final Color hoverColor;
+ final Color? splashColor;
+ final Widget child;
+ final String? tooltip;
+ final EdgeInsetsGeometry padding;
+ final bool enableFeedback;
+ const MenuButton({
+ super.key,
+ required this.onPressed,
+ required this.color,
+ required this.hoverColor,
+ required this.child,
+ this.splashColor,
+ this.tooltip = "",
+ this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6),
+ this.enableFeedback = true,
+ });
+
+ @override
+ State createState() => _MenuButtonState();
+}
+
+class _MenuButtonState extends State {
+ bool _isHover = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: widget.padding,
+ child: Tooltip(
+ message: widget.tooltip,
+ child: Material(
+ type: MaterialType.transparency,
+ child: Ink(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(5),
+ color: _isHover ? widget.hoverColor : widget.color,
+ ),
+ child: InkWell(
+ onHover: (val) {
+ setState(() {
+ _isHover = val;
+ });
+ },
+ borderRadius: BorderRadius.circular(5),
+ splashColor: widget.splashColor,
+ enableFeedback: widget.enableFeedback,
+ onTap: widget.onPressed,
+ child: widget.child,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index 36b9504c0..e82e9d26e 100644
--- a/flutter/lib/desktop/widgets/remote_menubar.dart
+++ b/flutter/lib/desktop/widgets/remote_menubar.dart
@@ -5,10 +5,12 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
@@ -93,7 +95,10 @@ class MenubarState {
}
class _MenubarTheme {
- static const Color commonColor = MyTheme.accent;
+ static const Color blueColor = MyTheme.button;
+ static const Color hoverBlueColor = MyTheme.accent;
+ static const Color redColor = Colors.redAccent;
+ static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
@@ -408,14 +413,18 @@ class _RemoteMenubarState extends State {
menubarItems.add(_buildPinMenubar(context));
menubarItems.add(_buildFullscreen(context));
if (widget.ffi.ffiModel.isPeerAndroid) {
- menubarItems.add(IconButton(
+ menubarItems.add(MenuButton(
tooltip: translate('Mobile Actions'),
- color: _MenubarTheme.commonColor,
- icon: const Icon(Icons.build),
+ child: SvgPicture.asset(
+ "assets/actions_mobile.svg",
+ color: Colors.white,
+ ),
onPressed: () {
widget.ffi.dialogManager
.toggleMobileActionsOverlay(ffi: widget.ffi);
},
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
));
}
}
@@ -425,85 +434,84 @@ class _RemoteMenubarState extends State {
menubarItems.add(_buildKeyboard(context));
if (!isWeb) {
menubarItems.add(_buildChat(context));
+ menubarItems.add(_buildVoiceCall(context));
}
menubarItems.add(_buildRecording(context));
menubarItems.add(_buildClose(context));
return PopupMenuTheme(
- data: const PopupMenuThemeData(
- textStyle: TextStyle(color: _MenubarTheme.commonColor)),
- child: Column(mainAxisSize: MainAxisSize.min, children: [
+ data: const PopupMenuThemeData(
+ textStyle: TextStyle(color: _MenubarTheme.blueColor)),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
Container(
- decoration: BoxDecoration(
- color: Colors.white,
- border: Border.all(color: MyTheme.border),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(
+ bottom: Radius.circular(10),
),
+ ),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
- children: menubarItems,
- )),
+ children: [
+ SizedBox(width: 3),
+ ...menubarItems,
+ SizedBox(width: 3)
+ ],
+ ),
+ ),
+ ),
_buildDraggableShowHide(context),
- ]));
+ ],
+ ),
+ );
}
Widget _buildPinMenubar(BuildContext context) {
- return Obx(() => IconButton(
- tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'),
- onPressed: () {
- widget.state.switchPin();
- },
- icon: Obx(() => Transform.rotate(
- angle: pin ? math.pi / 4 : 0,
- child: Icon(
- Icons.push_pin,
- color: pin ? _MenubarTheme.commonColor : Colors.grey,
- ))),
- ));
+ return Obx(
+ () => MenuButton(
+ tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'),
+ onPressed: () {
+ widget.state.switchPin();
+ },
+ child: SvgPicture.asset(
+ pin ? "assets/pinned.svg" : "assets/unpinned.svg",
+ color: Colors.white,
+ ),
+ color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!,
+ hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!,
+ ),
+ );
}
Widget _buildFullscreen(BuildContext context) {
- return IconButton(
+ return MenuButton(
tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
onPressed: () {
_setFullscreen(!isFullscreen);
},
- icon: isFullscreen
- ? const Icon(
- Icons.fullscreen_exit,
- color: _MenubarTheme.commonColor,
- )
- : const Icon(
- Icons.fullscreen,
- color: _MenubarTheme.commonColor,
- ),
- );
- }
-
- Widget _buildChat(BuildContext context) {
- return IconButton(
- tooltip: translate('Chat'),
- onPressed: () {
- widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
- widget.ffi.chatModel.toggleChatOverlay();
- },
- icon: const Icon(
- Icons.message,
- color: _MenubarTheme.commonColor,
+ child: SvgPicture.asset(
+ isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg",
+ color: Colors.white,
),
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
);
}
Widget _buildMonitor(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
- return mod_menu.PopupMenuButton(
+ final monitor = mod_menu.PopupMenuButton(
tooltip: translate('Select Monitor'),
- padding: EdgeInsets.zero,
position: mod_menu.PopupMenuPosition.under,
icon: Stack(
alignment: Alignment.center,
children: [
- const Icon(
- Icons.personal_video,
- color: _MenubarTheme.commonColor,
+ SvgPicture.asset(
+ "assets/display.svg",
+ color: Colors.white,
),
Padding(
padding: const EdgeInsets.only(bottom: 3.9),
@@ -511,8 +519,7 @@ class _RemoteMenubarState extends State {
RxInt display = CurrentDisplayState.find(widget.id);
return Text(
'${display.value + 1}/${pi.displays.length}',
- style: const TextStyle(
- color: _MenubarTheme.commonColor, fontSize: 8),
+ style: const TextStyle(color: Colors.white, fontSize: 8),
);
}),
)
@@ -521,41 +528,44 @@ class _RemoteMenubarState extends State {
itemBuilder: (BuildContext context) {
final List rowChildren = [];
for (int i = 0; i < pi.displays.length; i++) {
- rowChildren.add(
- Stack(
- alignment: Alignment.center,
- children: [
- const Icon(
- Icons.personal_video,
- color: _MenubarTheme.commonColor,
- ),
- TextButton(
- child: Container(
- alignment: AlignmentDirectional.center,
- constraints:
- const BoxConstraints(minHeight: _MenubarTheme.height),
- child: Padding(
- padding: const EdgeInsets.only(bottom: 2.5),
- child: Text(
- (i + 1).toString(),
- style:
- const TextStyle(color: _MenubarTheme.commonColor),
- ),
- )),
- onPressed: () {
- if (Navigator.canPop(context)) {
- Navigator.pop(context);
- _menuDismissCallback();
- }
- RxInt display = CurrentDisplayState.find(widget.id);
- if (display.value != i) {
- bind.sessionSwitchDisplay(id: widget.id, value: i);
- }
- },
- )
- ],
+ rowChildren.add(MenuButton(
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ child: Container(
+ alignment: AlignmentDirectional.center,
+ constraints:
+ const BoxConstraints(minHeight: _MenubarTheme.height),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ SvgPicture.asset(
+ "assets/display.svg",
+ color: Colors.white,
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 2.5),
+ child: Text(
+ (i + 1).toString(),
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ )
+ ],
+ ),
),
- );
+ onPressed: () {
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ _menuDismissCallback();
+ }
+ RxInt display = CurrentDisplayState.find(widget.id);
+ if (display.value != i) {
+ bind.sessionSwitchDisplay(id: widget.id, value: i);
+ }
+ },
+ ));
}
return >[
mod_menu.PopupMenuItem(
@@ -571,14 +581,19 @@ class _RemoteMenubarState extends State {
];
},
);
+
+ return Obx(() => Offstage(
+ offstage: stateGlobal.displaysCount.value < 2,
+ child: monitor,
+ ));
}
Widget _buildControl(BuildContext context) {
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
- icon: const Icon(
- Icons.bolt,
- color: _MenubarTheme.commonColor,
+ icon: SvgPicture.asset(
+ "assets/actions.svg",
+ color: Colors.white,
),
tooltip: translate('Control Actions'),
position: mod_menu.PopupMenuPosition.under,
@@ -586,7 +601,7 @@ class _RemoteMenubarState extends State {
.map((entry) => entry.build(
context,
const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
+ commonColor: _MenubarTheme.blueColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
@@ -608,9 +623,9 @@ class _RemoteMenubarState extends State {
final remoteCount = RemoteCountState.find().value;
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
- icon: const Icon(
- Icons.tv,
- color: _MenubarTheme.commonColor,
+ icon: SvgPicture.asset(
+ "assets/display.svg",
+ color: Colors.white,
),
tooltip: translate('Display Settings'),
position: mod_menu.PopupMenuPosition.under,
@@ -620,7 +635,7 @@ class _RemoteMenubarState extends State {
.map((entry) => entry.build(
context,
const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
+ commonColor: _MenubarTheme.blueColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
@@ -641,9 +656,9 @@ class _RemoteMenubarState extends State {
}
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
- icon: const Icon(
- Icons.keyboard,
- color: _MenubarTheme.commonColor,
+ icon: SvgPicture.asset(
+ "assets/keyboard.svg",
+ color: Colors.white,
),
tooltip: translate('Keyboard Settings'),
position: mod_menu.PopupMenuPosition.under,
@@ -651,7 +666,7 @@ class _RemoteMenubarState extends State {
.map((entry) => entry.build(
context,
const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
+ commonColor: _MenubarTheme.blueColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
@@ -664,18 +679,22 @@ class _RemoteMenubarState extends State {
return Consumer(builder: ((context, value, child) {
if (value.permissions['recording'] != false) {
return Consumer(
- builder: (context, value, child) => IconButton(
- tooltip: value.start
- ? translate('Stop session recording')
- : translate('Start session recording'),
- onPressed: () => value.toggle(),
- icon: Icon(
- value.start
- ? Icons.pause_circle_filled
- : Icons.videocam_outlined,
- color: _MenubarTheme.commonColor,
- ),
- ));
+ builder: (context, value, child) => MenuButton(
+ tooltip: value.start
+ ? translate('Stop session recording')
+ : translate('Start session recording'),
+ onPressed: () => value.toggle(),
+ child: SvgPicture.asset(
+ "assets/rec.svg",
+ color: Colors.white,
+ ),
+ color:
+ value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor,
+ hoverColor: value.start
+ ? _MenubarTheme.hoverRedColor
+ : _MenubarTheme.hoverBlueColor,
+ ),
+ );
} else {
return Offstage();
}
@@ -683,18 +702,132 @@ class _RemoteMenubarState extends State {
}
Widget _buildClose(BuildContext context) {
- return IconButton(
+ return MenuButton(
tooltip: translate('Close'),
onPressed: () {
clientClose(widget.id, widget.ffi.dialogManager);
},
- icon: const Icon(
- Icons.close,
- color: _MenubarTheme.commonColor,
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Colors.white,
),
+ color: _MenubarTheme.redColor,
+ hoverColor: _MenubarTheme.hoverRedColor,
);
}
+ final _chatButtonKey = GlobalKey();
+ Widget _buildChat(BuildContext context) {
+ FfiModel ffiModel = Provider.of(context);
+ return mod_menu.PopupMenuButton(
+ key: _chatButtonKey,
+ padding: EdgeInsets.zero,
+ icon: SvgPicture.asset(
+ "assets/chat.svg",
+ color: Colors.white,
+ ),
+ tooltip: translate('Chat'),
+ position: mod_menu.PopupMenuPosition.under,
+ itemBuilder: (BuildContext context) => _getChatMenu(context)
+ .map((entry) => entry.build(
+ context,
+ const MenuConfig(
+ commonColor: _MenubarTheme.blueColor,
+ height: _MenubarTheme.height,
+ dividerHeight: _MenubarTheme.dividerHeight,
+ )))
+ .expand((i) => i)
+ .toList(),
+ );
+ }
+
+ Widget _getVoiceCallIcon() {
+ switch (widget.ffi.chatModel.voiceCallStatus.value) {
+ case VoiceCallStatus.waitingForResponse:
+ return SvgPicture.asset(
+ "assets/call_wait.svg",
+ color: Colors.white,
+ );
+
+ case VoiceCallStatus.connected:
+ return SvgPicture.asset(
+ "assets/call_end.svg",
+ color: Colors.white,
+ );
+ default:
+ return const Offstage();
+ }
+ }
+
+ String? _getVoiceCallTooltip() {
+ switch (widget.ffi.chatModel.voiceCallStatus.value) {
+ case VoiceCallStatus.waitingForResponse:
+ return "Waiting";
+ case VoiceCallStatus.connected:
+ return "Disconnect";
+ default:
+ return null;
+ }
+ }
+
+ Widget _buildVoiceCall(BuildContext context) {
+ return Obx(
+ () {
+ final tooltipText = _getVoiceCallTooltip();
+ return tooltipText == null
+ ? const Offstage()
+ : MenuButton(
+ child: _getVoiceCallIcon(),
+ tooltip: translate(tooltipText),
+ onPressed: () => bind.sessionCloseVoiceCall(id: widget.id),
+ color: _MenubarTheme.redColor,
+ hoverColor: _MenubarTheme.hoverRedColor,
+ );
+ },
+ );
+ }
+
+ List> _getChatMenu(BuildContext context) {
+ final List> chatMenu = [];
+ const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
+ chatMenu.addAll([
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Text chat'),
+ style: style,
+ ),
+ proc: () {
+ RenderBox? renderBox =
+ _chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
+
+ Offset? initPos;
+ if (renderBox != null) {
+ final pos = renderBox.localToGlobal(Offset.zero);
+ initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
+ }
+
+ widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
+ widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
+ },
+ padding: padding,
+ dismissOnClicked: true,
+ ),
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Voice call'),
+ style: style,
+ ),
+ proc: () {
+ // Request a voice call.
+ bind.sessionRequestVoiceCall(id: widget.id);
+ },
+ padding: padding,
+ dismissOnClicked: true,
+ ),
+ ]);
+ return chatMenu;
+ }
+
List> _getControlMenu(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
final perms = widget.ffi.ffiModel.permissions;
@@ -884,7 +1017,6 @@ class _RemoteMenubarState extends State {
// ));
// }
}
-
return displayMenu;
}
@@ -1382,25 +1514,32 @@ class _RemoteMenubarState extends State {
text: translate('Ratio'),
optionsGetter: () {
List list = [];
- List modes = ["legacy"];
+ List modes = [
+ KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'),
+ KeyboardModeMenu(key: 'map', menu: 'Map mode'),
+ KeyboardModeMenu(key: 'translate', menu: 'Translate mode'),
+ ];
- if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) {
- modes.add("map");
- }
-
- for (String mode in modes) {
- if (mode == "legacy") {
- list.add(MenuEntryRadioOption(
- text: translate('Legacy mode'), value: 'legacy'));
- } else if (mode == "map") {
- list.add(MenuEntryRadioOption(
- text: translate('Map mode'), value: 'map'));
+ for (KeyboardModeMenu mode in modes) {
+ if (bind.sessionIsKeyboardModeSupported(
+ id: widget.id, mode: mode.key)) {
+ if (mode.key == 'translate') {
+ if (Platform.isLinux ||
+ widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) {
+ continue;
+ }
+ }
+ var text = translate(mode.menu);
+ if (mode.key == 'translate') {
+ text = '$text beta';
+ }
+ list.add(MenuEntryRadioOption(text: text, value: mode.key));
}
}
return list;
},
curOptionGetter: () async {
- return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
+ return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy';
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
@@ -1626,7 +1765,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
child: Icon(
Icons.drag_indicator,
size: 20,
- color: Colors.grey,
+ color: Colors.grey[800],
),
feedback: widget,
onDragStarted: (() {
@@ -1679,7 +1818,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
child: Container(
decoration: BoxDecoration(
color: Colors.white,
- border: Border.all(color: MyTheme.border),
+ borderRadius: BorderRadius.vertical(
+ bottom: Radius.circular(5),
+ ),
),
child: SizedBox(
height: 20,
@@ -1689,3 +1830,10 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
);
}
}
+
+class KeyboardModeMenu {
+ final String key;
+ final String menu;
+
+ KeyboardModeMenu({required this.key, required this.menu});
+}
diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart
index cfbddbafb..9ba7a6315 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -234,7 +234,7 @@ class DesktopTab extends StatelessWidget {
Key? key,
required this.controller,
this.showLogo = true,
- this.showTitle = true,
+ this.showTitle = false,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
@@ -327,14 +327,32 @@ class DesktopTab extends StatelessWidget {
));
}
+ List _tabWidgets = [];
Widget _buildPageView() {
return _buildBlock(
child: Obx(() => PageView(
controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(),
- children: state.value.tabs
- .map((tab) => tab.page)
- .toList(growable: false))));
+ children: () {
+ /// 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
+ final tabLen = state.value.tabs.length;
+ if (tabLen == _tabWidgets.length) {
+ return _tabWidgets;
+ } else if (_tabWidgets.isNotEmpty &&
+ tabLen == _tabWidgets.length + 1) {
+ /// On add. Use the previous list(pointer) to prevent item's state init twice.
+ /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built.
+ _tabWidgets.add(state.value.tabs.last.page);
+ return _tabWidgets;
+ } else {
+ /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal.
+ /// the Widgets in list must enable [AutomaticKeepAliveClientMixin]
+ final newList = state.value.tabs.map((v) => v.page).toList();
+ _tabWidgets = newList;
+ return newList;
+ }
+ }())));
}
/// Check whether to show ListView
@@ -767,7 +785,8 @@ class _ListView extends StatelessWidget {
tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
maxLabelWidth: maxLabelWidth,
- selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor,
+ selectedTabBackgroundColor: selectedTabBackgroundColor ??
+ MyTheme.tabbar(context).selectedTabBackgroundColor,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
);
}).toList()));
@@ -1121,7 +1140,8 @@ class TabbarTheme extends ThemeExtension {
dividerColor: dividerColor ?? this.dividerColor,
hoverColor: hoverColor ?? this.hoverColor,
closeHoverColor: closeHoverColor ?? this.closeHoverColor,
- selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor,
+ selectedTabBackgroundColor:
+ selectedTabBackgroundColor ?? this.selectedTabBackgroundColor,
);
}
@@ -1147,7 +1167,8 @@ class TabbarTheme extends ThemeExtension {
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t),
- selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t),
+ selectedTabBackgroundColor: Color.lerp(
+ selectedTabBackgroundColor, other.selectedTabBackgroundColor, t),
);
}
diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart
index 475b4cb86..38e4d917b 100644
--- a/flutter/lib/desktop/widgets/titlebar_widget.dart
+++ b/flutter/lib/desktop/widgets/titlebar_widget.dart
@@ -24,47 +24,8 @@ class DesktopTitleBar extends StatelessWidget {
Expanded(
child: child ?? Offstage(),
)
- // const WindowButtons()
],
),
);
}
-}
-
-// final buttonColors = WindowButtonColors(
-// iconNormal: const Color(0xFF805306),
-// mouseOver: const Color(0xFFF6A00C),
-// mouseDown: const Color(0xFF805306),
-// iconMouseOver: const Color(0xFF805306),
-// iconMouseDown: const Color(0xFFFFD500));
-//
-// final closeButtonColors = WindowButtonColors(
-// mouseOver: const Color(0xFFD32F2F),
-// mouseDown: const Color(0xFFB71C1C),
-// iconNormal: const Color(0xFF805306),
-// iconMouseOver: Colors.white);
-//
-// class WindowButtons extends StatelessWidget {
-// const WindowButtons({Key? key}) : super(key: key);
-//
-// @override
-// Widget build(BuildContext context) {
-// return Row(
-// children: [
-// MinimizeWindowButton(colors: buttonColors, onPressed: () {
-// windowManager.minimize();
-// },),
-// MaximizeWindowButton(colors: buttonColors, onPressed: () async {
-// if (await windowManager.isMaximized()) {
-// windowManager.restore();
-// } else {
-// windowManager.maximize();
-// }
-// },),
-// CloseWindowButton(colors: closeButtonColors, onPressed: () {
-// windowManager.close();
-// },),
-// ],
-// );
-// }
-// }
+}
\ No newline at end of file
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index c19adf753..c61287d4f 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -216,6 +216,7 @@ void runMultiWindow(
void runConnectionManagerScreen(bool hide) async {
await initEnv(kAppTypeConnectionManager);
+ await bind.cmStartListenIpcThread();
_runApp(
'',
const DesktopServerPage(),
diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart
index c4b07b375..951d63faf 100644
--- a/flutter/lib/mobile/pages/remote_page.dart
+++ b/flutter/lib/mobile/pages/remote_page.dart
@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
import 'package:flutter_hbb/models/chat_model.dart';
+import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
@@ -17,6 +18,7 @@ import '../../common/widgets/remote_input.dart';
import '../../models/input_model.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
+import '../../utils/image.dart';
import '../widgets/dialog.dart';
import '../widgets/gestures.dart';
@@ -32,17 +34,16 @@ class RemotePage extends StatefulWidget {
}
class _RemotePageState extends State {
- Timer? _interval;
Timer? _timer;
bool _showBar = !isWebDesktop;
- double _bottom = 0;
+ bool _showGestureHelp = false;
String _value = '';
double _scale = 1;
double _mouseScrollIntegral = 0; // mouse scroll speed controller
Orientation? _currentOrientation;
- var _more = true;
- var _fn = false;
+ final keyboardVisibilityController = KeyboardVisibilityController();
+ late final StreamSubscription keyboardSubscription;
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
@@ -57,14 +58,14 @@ class _RemotePageState extends State {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
- _interval =
- Timer.periodic(Duration(milliseconds: 30), (timer) => interval());
});
Wakelock.enable();
_physicalFocusNode.requestFocus();
gFFI.ffiModel.updateEventListener(widget.id);
gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id);
+ keyboardSubscription =
+ keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
}
@override
@@ -75,47 +76,26 @@ class _RemotePageState extends State {
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
gFFI.close();
- _interval?.cancel();
_timer?.cancel();
gFFI.dialogManager.dismissAll();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
Wakelock.disable();
+ keyboardSubscription.cancel();
super.dispose();
}
- void resetTool() {
- inputModel.resetModifiers();
- }
-
- bool isKeyboardShown() {
- return _bottom >= 100;
- }
-
- // crash on web before widget initiated.
- void intervalUnsafe() {
- var v = MediaQuery.of(context).viewInsets.bottom;
- if (v != _bottom) {
- resetTool();
- setState(() {
- _bottom = v;
- if (v < 100) {
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
- overlays: []);
- // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
- if (gFFI.chatModel.chatWindowOverlayEntry == null &&
- gFFI.ffiModel.pi.version.isNotEmpty) {
- gFFI.invokeMethod("enable_soft_keyboard", false);
- }
- }
- });
+ void onSoftKeyboardChanged(bool visible) {
+ if (!visible) {
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
+ // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
+ if (gFFI.chatModel.chatWindowOverlayEntry == null &&
+ gFFI.ffiModel.pi.version.isNotEmpty) {
+ gFFI.invokeMethod("enable_soft_keyboard", false);
+ }
}
- }
-
- void interval() {
- try {
- intervalUnsafe();
- } catch (e) {}
+ // update for Scaffold
+ setState(() {});
}
// handle mobile virtual keyboard
@@ -218,8 +198,9 @@ class _RemotePageState extends State {
@override
Widget build(BuildContext context) {
final pi = Provider.of(context).pi;
- final hideKeyboard = isKeyboardShown() && _showEdit;
- final showActionButton = !_showBar || hideKeyboard;
+ final keyboardIsVisible =
+ keyboardVisibilityController.isVisible && _showEdit;
+ final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
return WillPopScope(
@@ -228,29 +209,40 @@ class _RemotePageState extends State {
return false;
},
child: getRawPointerAndKeyBody(Scaffold(
- // resizeToAvoidBottomInset: true,
+ // workaround for https://github.com/rustdesk/rustdesk/issues/3131
+ floatingActionButtonLocation: keyboardIsVisible
+ ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
+ : null,
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
- mini: !hideKeyboard,
+ mini: !keyboardIsVisible,
child: Icon(
- hideKeyboard ? Icons.expand_more : Icons.expand_less),
+ (keyboardIsVisible || _showGestureHelp)
+ ? Icons.expand_more
+ : Icons.expand_less,
+ color: Colors.white,
+ ),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
- if (hideKeyboard) {
+ if (keyboardIsVisible) {
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
+ } else if (_showGestureHelp) {
+ _showGestureHelp = false;
} else {
_showBar = !_showBar;
}
});
}),
- bottomNavigationBar: _showBar && pi.displays.isNotEmpty
- ? getBottomAppBar(keyboard)
- : null,
+ bottomNavigationBar: _showGestureHelp
+ ? getGestureHelp()
+ : (_showBar && pi.displays.isNotEmpty
+ ? getBottomAppBar(keyboard)
+ : null),
body: Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
@@ -340,7 +332,8 @@ class _RemotePageState extends State {
icon: Icon(gFFI.ffiModel.touchMode
? Icons.touch_app
: Icons.mouse),
- onPressed: changeTouchMode,
+ onPressed: () => setState(
+ () => _showGestureHelp = !_showGestureHelp),
),
]) +
(isWeb
@@ -492,6 +485,7 @@ class _RemotePageState extends State {
}
Widget getBodyForMobile() {
+ final keyboardIsVisible = keyboardVisibilityController.isVisible;
return Container(
color: MyTheme.canvasColor,
child: Stack(children: () {
@@ -502,7 +496,7 @@ class _RemotePageState extends State {
right: 10,
child: QualityMonitor(gFFI.qualityMonitorModel),
),
- getHelpTools(),
+ KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)),
SizedBox(
width: 0,
height: 0,
@@ -575,9 +569,10 @@ class _RemotePageState extends State {
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
}
if (perms['keyboard'] != false) {
- more.add(PopupMenuItem(
- child: Text(translate('Physical Keyboard Input Mode')),
- value: 'input-mode'));
+ // * Currently mobile does not enable map mode
+ // more.add(PopupMenuItem(
+ // child: Text(translate('Physical Keyboard Input Mode')),
+ // value: 'input-mode'));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
more.add(PopupMenuItem(
child: Text('${translate('Insert')} Ctrl + Alt + Del'),
@@ -632,8 +627,9 @@ class _RemotePageState extends State {
);
if (value == 'cad') {
bind.sessionCtrlAltDel(id: widget.id);
- } else if (value == 'input-mode') {
- changePhysicalKeyboardInputMode();
+ // * Currently mobile does not enable map mode
+ // } else if (value == 'input-mode') {
+ // changePhysicalKeyboardInputMode();
} else if (value == 'lock') {
bind.sessionLockScreen(id: widget.id);
} else if (value == 'block-input') {
@@ -670,94 +666,110 @@ class _RemotePageState extends State {
}();
}
- void changeTouchMode() {
- setState(() => _showEdit = false);
- showModalBottomSheet(
- // backgroundColor: MyTheme.grayBg,
- isScrollControlled: true,
- context: context,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(5))),
- builder: (context) => DraggableScrollableSheet(
- expand: false,
- builder: (context, scrollController) {
- return SingleChildScrollView(
- controller: ScrollController(),
- padding: EdgeInsets.symmetric(vertical: 10),
- child: GestureHelp(
- touchMode: gFFI.ffiModel.touchMode,
- onTouchModeChange: (t) {
- gFFI.ffiModel.toggleTouchMode();
- final v = gFFI.ffiModel.touchMode ? 'Y' : '';
- bind.sessionPeerOption(
- id: widget.id, name: "touch", value: v);
- }));
- }));
+ /// aka changeTouchMode
+ BottomAppBar getGestureHelp() {
+ return BottomAppBar(
+ child: SingleChildScrollView(
+ controller: ScrollController(),
+ padding: EdgeInsets.symmetric(vertical: 10),
+ child: GestureHelp(
+ touchMode: gFFI.ffiModel.touchMode,
+ onTouchModeChange: (t) {
+ gFFI.ffiModel.toggleTouchMode();
+ final v = gFFI.ffiModel.touchMode ? 'Y' : '';
+ bind.sessionPeerOption(
+ id: widget.id, name: "touch", value: v);
+ })));
}
- void changePhysicalKeyboardInputMode() async {
- var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
- gFFI.dialogManager.show((setState, close) {
- void setMode(String? v) async {
- await bind.sessionPeerOption(
- id: widget.id, name: "keyboard-mode", value: v ?? "");
- setState(() => current = v ?? '');
- Future.delayed(Duration(milliseconds: 300), close);
- }
+ // * Currently mobile does not enable map mode
+ // void changePhysicalKeyboardInputMode() async {
+ // var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
+ // gFFI.dialogManager.show((setState, close) {
+ // void setMode(String? v) async {
+ // await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? "");
+ // setState(() => current = v ?? '');
+ // Future.delayed(Duration(milliseconds: 300), close);
+ // }
+ //
+ // return CustomAlertDialog(
+ // title: Text(translate('Physical Keyboard Input Mode')),
+ // content: Column(mainAxisSize: MainAxisSize.min, children: [
+ // getRadio('Legacy mode', 'legacy', current, setMode,
+ // contentPadding: EdgeInsets.zero),
+ // getRadio('Map mode', 'map', current, setMode,
+ // contentPadding: EdgeInsets.zero),
+ // ]));
+ // }, clickMaskDismiss: true);
+ // }
+}
- return CustomAlertDialog(
- title: Text(translate('Physical Keyboard Input Mode')),
- content: Column(mainAxisSize: MainAxisSize.min, children: [
- getRadio('Legacy mode', 'legacy', current, setMode,
- contentPadding: EdgeInsets.zero),
- getRadio('Map mode', 'map', current, setMode,
- contentPadding: EdgeInsets.zero),
- ]));
- }, clickMaskDismiss: true);
+class KeyHelpTools extends StatefulWidget {
+ /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
+ final bool requestShow;
+
+ KeyHelpTools({required this.requestShow});
+
+ @override
+ State createState() => _KeyHelpToolsState();
+}
+
+class _KeyHelpToolsState extends State {
+ var _more = true;
+ var _fn = false;
+ var _pin = false;
+ final _keyboardVisibilityController = KeyboardVisibilityController();
+
+ InputModel get inputModel => gFFI.inputModel;
+
+ Widget wrap(String text, void Function() onPressed,
+ {bool? active, IconData? icon}) {
+ return TextButton(
+ style: TextButton.styleFrom(
+ minimumSize: Size(0, 0),
+ padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75),
+ //adds padding inside the button
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ //limits the touch area to the button area
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(5.0),
+ ),
+ backgroundColor: active == true ? MyTheme.accent80 : null,
+ ),
+ child: icon != null
+ ? Icon(icon, size: 14, color: Colors.white)
+ : Text(translate(text),
+ style: TextStyle(color: Colors.white, fontSize: 11)),
+ onPressed: onPressed);
}
- Widget getHelpTools() {
- final keyboard = isKeyboardShown();
- if (!keyboard) {
- return SizedBox();
+ @override
+ Widget build(BuildContext context) {
+ final hasModifierOn = inputModel.ctrl ||
+ inputModel.alt ||
+ inputModel.shift ||
+ inputModel.command;
+
+ if (!_pin && !hasModifierOn && !widget.requestShow) {
+ return Offstage();
}
final size = MediaQuery.of(context).size;
- wrap(String text, void Function() onPressed,
- [bool? active, IconData? icon]) {
- return TextButton(
- style: TextButton.styleFrom(
- minimumSize: Size(0, 0),
- padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75),
- //adds padding inside the button
- tapTargetSize: MaterialTapTargetSize.shrinkWrap,
- //limits the touch area to the button area
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(5.0),
- ),
- backgroundColor: active == true ? MyTheme.accent80 : null,
- ),
- child: icon != null
- ? Icon(icon, size: 17, color: Colors.white)
- : Text(translate(text),
- style: TextStyle(color: Colors.white, fontSize: 11)),
- onPressed: onPressed);
- }
final pi = gFFI.ffiModel.pi;
final isMac = pi.platform == kPeerPlatformMacOS;
final modifiers = [
wrap('Ctrl ', () {
setState(() => inputModel.ctrl = !inputModel.ctrl);
- }, inputModel.ctrl),
+ }, active: inputModel.ctrl),
wrap(' Alt ', () {
setState(() => inputModel.alt = !inputModel.alt);
- }, inputModel.alt),
+ }, active: inputModel.alt),
wrap('Shift', () {
setState(() => inputModel.shift = !inputModel.shift);
- }, inputModel.shift),
+ }, active: inputModel.shift),
wrap(isMac ? ' Cmd ' : ' Win ', () {
setState(() => inputModel.command = !inputModel.command);
- }, inputModel.command),
+ }, active: inputModel.command),
];
final keys = [
wrap(
@@ -770,7 +782,14 @@ class _RemotePageState extends State {
}
},
),
- _fn),
+ active: _fn),
+ wrap(
+ '',
+ () => setState(
+ () => _pin = !_pin,
+ ),
+ active: _pin,
+ icon: Icons.push_pin),
wrap(
' ... ',
() => setState(
@@ -781,7 +800,7 @@ class _RemotePageState extends State {
}
},
),
- _more),
+ active: _more),
];
final fn = [
SizedBox(width: 9999),
@@ -806,6 +825,9 @@ class _RemotePageState extends State {
wrap('End', () {
inputModel.inputKey('VK_END');
}),
+ wrap('Ins', () {
+ inputModel.inputKey('VK_INSERT');
+ }),
wrap('Del', () {
inputModel.inputKey('VK_DELETE');
}),
@@ -818,16 +840,16 @@ class _RemotePageState extends State {
SizedBox(width: 9999),
wrap('', () {
inputModel.inputKey('VK_LEFT');
- }, false, Icons.keyboard_arrow_left),
+ }, icon: Icons.keyboard_arrow_left),
wrap('', () {
inputModel.inputKey('VK_UP');
- }, false, Icons.keyboard_arrow_up),
+ }, icon: Icons.keyboard_arrow_up),
wrap('', () {
inputModel.inputKey('VK_DOWN');
- }, false, Icons.keyboard_arrow_down),
+ }, icon: Icons.keyboard_arrow_down),
wrap('', () {
inputModel.inputKey('VK_RIGHT');
- }, false, Icons.keyboard_arrow_right),
+ }, icon: Icons.keyboard_arrow_right),
wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () {
sendPrompt(isMac, 'VK_C');
}),
@@ -842,14 +864,15 @@ class _RemotePageState extends State {
return Container(
color: Color(0xAA000000),
padding: EdgeInsets.only(
- top: keyboard ? 24 : 4, left: 0, right: 0, bottom: 8),
+ top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
child: Wrap(
spacing: space,
runSpacing: space,
children: [SizedBox(width: 9999)] +
- (keyboard
- ? modifiers + keys + (_fn ? fn : []) + (_more ? more : [])
- : modifiers),
+ modifiers +
+ keys +
+ (_fn ? fn : []) +
+ (_more ? more : []),
));
}
}
@@ -893,32 +916,6 @@ class CursorPaint extends StatelessWidget {
}
}
-class ImagePainter extends CustomPainter {
- ImagePainter({
- required this.image,
- required this.x,
- required this.y,
- required this.scale,
- });
-
- ui.Image? image;
- double x;
- double y;
- double scale;
-
- @override
- void paint(Canvas canvas, Size size) {
- if (image == null) return;
- canvas.scale(scale, scale);
- canvas.drawImage(image!, Offset(x, y), Paint());
- }
-
- @override
- bool shouldRepaint(CustomPainter oldDelegate) {
- return oldDelegate != this;
- }
-}
-
void showOptions(
BuildContext context, String id, OverlayDialogManager dialogManager) async {
String quality =
@@ -1134,3 +1131,16 @@ void sendPrompt(bool isMac, String key) {
gFFI.inputModel.ctrl = old;
}
}
+
+class FABLocation extends FloatingActionButtonLocation {
+ FloatingActionButtonLocation location;
+ double offsetX;
+ double offsetY;
+ FABLocation(this.location, this.offsetX, this.offsetY);
+
+ @override
+ Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
+ final offset = location.getOffset(scaffoldGeometry);
+ return Offset(offset.dx + offsetX, offset.dy + offsetY);
+ }
+}
diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart
index 37cc77c8f..bc31ae2c4 100644
--- a/flutter/lib/mobile/widgets/gesture_help.dart
+++ b/flutter/lib/mobile/widgets/gesture_help.dart
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:toggle_switch/toggle_switch.dart';
-import '../../models/model.dart';
-
class GestureIcons {
static const String _family = 'gestureicons';
@@ -79,7 +77,10 @@ class _GestureHelpState extends State {
children: [
ToggleSwitch(
initialLabelIndex: _selectedIndex,
- inactiveBgColor: MyTheme.darkGray,
+ activeFgColor: Colors.white,
+ inactiveFgColor: Colors.white60,
+ activeBgColor: [MyTheme.accent],
+ inactiveBgColor: Theme.of(context).hintColor,
totalSwitches: 2,
minWidth: 150,
fontSize: 15,
@@ -188,7 +189,7 @@ class GestureInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- width: this.width,
+ width: width,
child: Column(
children: [
Icon(
@@ -199,11 +200,14 @@ class GestureInfo extends StatelessWidget {
SizedBox(height: 6),
Text(fromText,
textAlign: TextAlign.center,
- style: TextStyle(fontSize: 9, color: Colors.grey)),
+ style:
+ TextStyle(fontSize: 9, color: Theme.of(context).hintColor)),
SizedBox(height: 3),
Text(toText,
textAlign: TextAlign.center,
- style: TextStyle(fontSize: 12, color: Colors.black))
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).textTheme.bodySmall?.color))
],
));
}
diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart
index 18a0be279..8666e13e4 100644
--- a/flutter/lib/models/chat_model.dart
+++ b/flutter/lib/models/chat_model.dart
@@ -1,7 +1,11 @@
+import 'dart:async';
+
import 'package:dash_chat_2/dash_chat_2.dart';
import 'package:draggable_float_widget/draggable_float_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import '../consts.dart';
@@ -26,15 +30,17 @@ class MessageBody {
class ChatModel with ChangeNotifier {
static final clientModeID = -1;
- /// _overlayState:
- /// Desktop: store session overlay by using [setOverlayState].
- /// Mobile: always null, use global overlay.
- /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay]
- OverlayState? _overlayState;
OverlayEntry? chatIconOverlayEntry;
OverlayEntry? chatWindowOverlayEntry;
+
bool isConnManager = false;
+ RxBool isWindowFocus = true.obs;
+ BlockableOverlayState? _blockableOverlayState;
+ final Rx _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
+
+ Rx get voiceCallStatus => _voiceCallStatus;
+
final ChatUser me = ChatUser(
id: "",
firstName: "Me",
@@ -52,6 +58,19 @@ class ChatModel with ChangeNotifier {
bool get isShowCMChatPage => _isShowCMChatPage;
+ void setOverlayState(BlockableOverlayState blockableOverlayState) {
+ _blockableOverlayState = blockableOverlayState;
+
+ _blockableOverlayState!.addMiddleBlockedListener((v) {
+ if (!v) {
+ isWindowFocus.value = false;
+ if (isWindowFocus.value) {
+ isWindowFocus.toggle();
+ }
+ }
+ });
+ }
+
final WeakReference parent;
ChatModel(this.parent);
@@ -68,20 +87,6 @@ class ChatModel with ChangeNotifier {
}
}
- setOverlayState(OverlayState? os) {
- _overlayState = os;
- }
-
- OverlayState? _getOverlayState() {
- if (_overlayState == null) {
- if (globalKey.currentState == null ||
- globalKey.currentState!.overlay == null) return null;
- return globalKey.currentState!.overlay;
- } else {
- return _overlayState;
- }
- }
-
showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
if (chatIconOverlayEntry != null) {
chatIconOverlayEntry!.remove();
@@ -94,7 +99,7 @@ class ChatModel with ChangeNotifier {
}
}
- final overlayState = _getOverlayState();
+ final overlayState = _blockableOverlayState?.state;
if (overlayState == null) return;
final overlay = OverlayEntry(builder: (context) {
@@ -126,23 +131,35 @@ class ChatModel with ChangeNotifier {
}
}
- showChatWindowOverlay() {
+ showChatWindowOverlay({Offset? chatInitPos}) {
if (chatWindowOverlayEntry != null) return;
- final overlayState = _getOverlayState();
+ isWindowFocus.value = true;
+ _blockableOverlayState?.setMiddleBlocked(true);
+
+ final overlayState = _blockableOverlayState?.state;
if (overlayState == null) return;
final overlay = OverlayEntry(builder: (context) {
- return DraggableChatWindow(
- position: const Offset(20, 80),
- width: 250,
- height: 350,
- chatModel: this);
+ return Listener(
+ onPointerDown: (_) {
+ if (!isWindowFocus.value) {
+ isWindowFocus.value = true;
+ _blockableOverlayState?.setMiddleBlocked(true);
+ }
+ },
+ child: DraggableChatWindow(
+ position: chatInitPos ?? Offset(20, 80),
+ width: 250,
+ height: 350,
+ chatModel: this));
});
overlayState.insert(overlay);
chatWindowOverlayEntry = overlay;
+ requestChatInputFocus();
}
hideChatWindowOverlay() {
if (chatWindowOverlayEntry != null) {
+ _blockableOverlayState?.setMiddleBlocked(false);
chatWindowOverlayEntry!.remove();
chatWindowOverlayEntry = null;
return;
@@ -152,13 +169,13 @@ class ChatModel with ChangeNotifier {
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
chatWindowOverlayEntry == null);
- toggleChatOverlay() {
+ toggleChatOverlay({Offset? chatInitPos}) {
if (_isChatOverlayHide()) {
gFFI.invokeMethod("enable_soft_keyboard", true);
if (!isDesktop) {
showChatIconOverlay();
}
- showChatWindowOverlay();
+ showChatWindowOverlay(chatInitPos: chatInitPos);
} else {
hideChatIconOverlay();
hideChatWindowOverlay();
@@ -188,6 +205,7 @@ class ChatModel with ChangeNotifier {
await windowManager.setSizeAlignment(
kConnectionManagerWindowSize, Alignment.topRight);
} else {
+ requestChatInputFocus();
await windowManager.show();
await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight);
_isShowCMChatPage = !_isShowCMChatPage;
@@ -285,11 +303,48 @@ class ChatModel with ChangeNotifier {
close() {
hideChatIconOverlay();
hideChatWindowOverlay();
- _overlayState = null;
notifyListeners();
}
resetClientMode() {
_messages[clientModeID]?.clear();
}
+
+ void requestChatInputFocus() {
+ Timer(Duration(milliseconds: 100), () {
+ if (inputNode.hasListeners && inputNode.canRequestFocus) {
+ inputNode.requestFocus();
+ }
+ });
+ }
+
+ void onVoiceCallWaiting() {
+ _voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
+ }
+
+ void onVoiceCallStarted() {
+ _voiceCallStatus.value = VoiceCallStatus.connected;
+ }
+
+ void onVoiceCallClosed(String reason) {
+ _voiceCallStatus.value = VoiceCallStatus.notStarted;
+ }
+
+ void onVoiceCallIncoming() {
+ if (isConnManager) {
+ _voiceCallStatus.value = VoiceCallStatus.incoming;
+ }
+ }
+
+ void closeVoiceCall(String id) {
+ bind.sessionCloseVoiceCall(id: id);
+ }
+}
+
+enum VoiceCallStatus {
+ notStarted,
+ waitingForResponse,
+ connected,
+ // Connection manager only.
+ incoming
}
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
index 18d42d143..5817e54fe 100644
--- a/flutter/lib/models/file_model.dart
+++ b/flutter/lib/models/file_model.dart
@@ -75,6 +75,10 @@ class FileModel extends ChangeNotifier {
return isLocal ? _localSortStyle : _remoteSortStyle;
}
+ bool getSortAscending(bool isLocal) {
+ return isLocal ? _localSortAscending : _remoteSortAscending;
+ }
+
FileDirectory _currentLocalDir = FileDirectory();
FileDirectory get currentLocalDir => _currentLocalDir;
diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart
index 8c37f50bd..b1491d526 100644
--- a/flutter/lib/models/input_model.dart
+++ b/flutter/lib/models/input_model.dart
@@ -58,9 +58,12 @@ class InputModel {
InputModel(this.parent);
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
- bind.sessionGetKeyboardMode(id: id).then((result) {
- keyboardMode = result.toString();
- });
+ // * Currently mobile does not enable map mode
+ if (isDesktop) {
+ bind.sessionGetKeyboardMode(id: id).then((result) {
+ keyboardMode = result.toString();
+ });
+ }
final key = e.logicalKey;
if (e is RawKeyDownEvent) {
@@ -93,10 +96,9 @@ class InputModel {
}
}
- if (keyboardMode == 'map') {
+ // * Currently mobile does not enable map mode
+ if (isDesktop && keyboardMode == 'map') {
mapKeyboardMode(e);
- } else if (keyboardMode == 'translate') {
- legacyKeyboardMode(e);
} else {
legacyKeyboardMode(e);
}
@@ -483,10 +485,19 @@ class InputModel {
y /= canvasModel.scale;
x += d.x;
y += d.y;
+
+ if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) {
+ // If left mouse up, no early return.
+ if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
+ return;
+ }
+ }
+
if (type != '') {
x = 0;
y = 0;
}
+
evt['x'] = '${x.round()}';
evt['y'] = '${y.round()}';
var buttons = '';
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index daf7bfe34..1afb5b147 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
+import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
@@ -18,7 +20,6 @@ import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/common/shared_state.dart';
-import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:tuple/tuple.dart';
import 'package:image/image.dart' as img2;
import 'package:flutter_custom_cursor/cursor_manager.dart';
@@ -26,13 +27,13 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import '../common.dart';
-import '../common/shared_state.dart';
import '../utils/image.dart' as img;
import '../mobile/widgets/dialog.dart';
import 'input_model.dart';
import 'platform_model.dart';
typedef HandleMsgBox = Function(Map evt, String id);
+typedef ReconnectHandle = Function(OverlayDialogManager, String, bool);
final _waitForImage = {};
class FfiModel with ChangeNotifier {
@@ -139,6 +140,8 @@ class FfiModel with ChangeNotifier {
handleMsgBox(evt, peerId);
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
+ } else if (name == 'sync_peer_info') {
+ handleSyncPeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
@@ -203,6 +206,23 @@ class FfiModel with ChangeNotifier {
} else if (name == "on_url_scheme_received") {
final url = evt['url'].toString();
parseRustdeskUri(url);
+ } else if (name == "on_voice_call_waiting") {
+ // Waiting for the response from the peer.
+ parent.target?.chatModel.onVoiceCallWaiting();
+ } else if (name == "on_voice_call_started") {
+ // Voice call is connected.
+ parent.target?.chatModel.onVoiceCallStarted();
+ } else if (name == "on_voice_call_closed") {
+ // Voice call is closed with reason.
+ final reason = evt['reason'].toString();
+ parent.target?.chatModel.onVoiceCallClosed(reason);
+ } else if (name == "on_voice_call_incoming") {
+ // Voice call is requested by the peer.
+ parent.target?.chatModel.onVoiceCallIncoming();
+ } else if (name == "update_voice_call_state") {
+ parent.target?.serverModel.updateVoiceCallState(evt);
+ } else {
+ debugPrint("Unknown event name: $name");
}
};
}
@@ -281,6 +301,8 @@ class FfiModel with ChangeNotifier {
showWaitUacDialog(id, dialogManager, type);
} else if (type == 'elevation-error') {
showElevationError(id, type, title, text, dialogManager);
+ } else if (type == "relay-hint") {
+ showRelayHintDialog(id, type, title, text, dialogManager);
} else {
var hasRetry = evt['hasRetry'] == 'true';
showMsgBox(id, type, title, text, link, hasRetry, dialogManager);
@@ -291,14 +313,12 @@ class FfiModel with ChangeNotifier {
showMsgBox(String id, String type, String title, String text, String link,
bool hasRetry, OverlayDialogManager dialogManager,
{bool? hasCancel}) {
- msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel);
+ msgBox(id, type, title, text, link, dialogManager,
+ hasCancel: hasCancel, reconnect: reconnect);
_timer?.cancel();
if (hasRetry) {
_timer = Timer(Duration(seconds: _reconnects), () {
- bind.sessionReconnect(id: id);
- clearPermissions();
- dialogManager.showLoading(translate('Connecting...'),
- onCancel: closeConnection);
+ reconnect(dialogManager, id, false);
});
_reconnects *= 2;
} else {
@@ -306,6 +326,47 @@ class FfiModel with ChangeNotifier {
}
}
+ void reconnect(
+ OverlayDialogManager dialogManager, String id, bool forceRelay) {
+ bind.sessionReconnect(id: id, forceRelay: forceRelay);
+ clearPermissions();
+ dialogManager.showLoading(translate('Connecting...'),
+ onCancel: closeConnection);
+ }
+
+ void showRelayHintDialog(String id, String type, String title, String text,
+ OverlayDialogManager dialogManager) {
+ dialogManager.show(tag: '$id-$type', (setState, close) {
+ onClose() {
+ closeConnection();
+ close();
+ }
+
+ final style =
+ ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
+ return CustomAlertDialog(
+ title: null,
+ content: msgboxContent(type, title,
+ "${translate(text)}\n\n${translate('relay_hint_tip')}"),
+ actions: [
+ dialogButton('Close', onPressed: onClose, isOutline: true),
+ dialogButton('Retry',
+ onPressed: () => reconnect(dialogManager, id, false)),
+ dialogButton('Connect via relay',
+ onPressed: () => reconnect(dialogManager, id, true),
+ buttonStyle: style),
+ dialogButton('Always connect via relay', onPressed: () {
+ const option = 'force-always-relay';
+ bind.sessionPeerOption(
+ id: id, name: option, value: bool2option(option, true));
+ reconnect(dialogManager, id, true);
+ }, buttonStyle: style),
+ ],
+ onCancel: onClose,
+ );
+ });
+ }
+
/// Handle the peer info event based on [evt].
handlePeerInfo(Map evt, String peerId) async {
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
@@ -356,6 +417,7 @@ class FfiModel with ChangeNotifier {
d.cursorEmbedded = d0['cursor_embedded'] == 1;
_pi.displays.add(d);
}
+ stateGlobal.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay < _pi.displays.length) {
_display = _pi.displays[_pi.currentDisplay];
}
@@ -372,6 +434,27 @@ class FfiModel with ChangeNotifier {
notifyListeners();
}
+ /// Handle the peer info synchronization event based on [evt].
+ handleSyncPeerInfo(Map evt, String peerId) async {
+ if (evt['displays'] != null) {
+ List displays = json.decode(evt['displays']);
+ List newDisplays = [];
+ for (int i = 0; i < displays.length; ++i) {
+ Map d0 = displays[i];
+ var d = Display();
+ d.x = d0['x'].toDouble();
+ d.y = d0['y'].toDouble();
+ d.width = d0['width'];
+ d.height = d0['height'];
+ d.cursorEmbedded = d0['cursor_embedded'] == 1;
+ newDisplays.add(d);
+ }
+ _pi.displays = newDisplays;
+ stateGlobal.displaysCount.value = _pi.displays.length;
+ }
+ notifyListeners();
+ }
+
updateBlockInputState(Map evt, String peerId) {
_inputBlocked = evt['input_state'] == 'on';
notifyListeners();
@@ -419,12 +502,17 @@ class ImageModel with ChangeNotifier {
}
}
}
+
final pid = parent.target?.id;
- ui.decodeImageFromPixels(
+ img.decodeImageFromPixels(
rgba,
parent.target?.ffiModel.display.width ?? 0,
parent.target?.ffiModel.display.height ?? 0,
- isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) {
+ isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
+ onPixelsCopied: () {
+ // Unlock the rgba memory from rust codes.
+ platformFFI.nextRgba(id);
+ }).then((image) {
if (parent.target?.id != pid) return;
try {
// my throw exception, because the listener maybe already dispose
@@ -1319,7 +1407,8 @@ class FFI {
void start(String id,
{bool isFileTransfer = false,
bool isPortForward = false,
- String? switchUuid}) {
+ String? switchUuid,
+ bool? forceRelay}) {
assert(!(isFileTransfer && isPortForward), 'more than one connect type');
if (isFileTransfer) {
connType = ConnType.fileTransfer;
@@ -1335,16 +1424,20 @@ class FFI {
}
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
- id: id,
- isFileTransfer: isFileTransfer,
- isPortForward: isPortForward,
- switchUuid: switchUuid ?? "",
- );
+ id: id,
+ isFileTransfer: isFileTransfer,
+ isPortForward: isPortForward,
+ switchUuid: switchUuid ?? "",
+ forceRelay: forceRelay ?? false);
final stream = bind.sessionStart(id: id);
final cb = ffiModel.startEventListener(id);
() async {
+ // Preserved for the rgba data.
await for (final message in stream) {
if (message is EventToUI_Event) {
+ if (message.field0 == "close") {
+ break;
+ }
try {
Map event = json.decode(message.field0);
await cb(event);
@@ -1352,9 +1445,18 @@ class FFI {
debugPrint('json.decode fail1(): $e, ${message.field0}');
}
} else if (message is EventToUI_Rgba) {
- imageModel.onRgba(message.field0);
+ // Fetch the image buffer from rust codes.
+ final sz = platformFFI.getRgbaSize(id);
+ if (sz == null || sz == 0) {
+ return;
+ }
+ final rgba = platformFFI.getRgba(id, sz);
+ if (rgba != null) {
+ imageModel.onRgba(rgba);
+ }
}
}
+ debugPrint('Exit session event loop');
}();
// every instance will bind a stream
this.id = id;
@@ -1375,14 +1477,14 @@ class FFI {
await setCanvasConfig(id, cursorModel.x, cursorModel.y, canvasModel.x,
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
}
- bind.sessionClose(id: id);
- id = '';
imageModel.update(null);
cursorModel.clear();
ffiModel.clear();
canvasModel.clear();
inputModel.resetModifiers();
+ await bind.sessionClose(id: id);
debugPrint('model $id closed');
+ id = '';
}
void setMethodCallHandler(FMethod callback) {
diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart
index 628bf502d..ba62b775e 100644
--- a/flutter/lib/models/native_model.dart
+++ b/flutter/lib/models/native_model.dart
@@ -9,6 +9,7 @@ import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
+import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win32/win32.dart' as win32;
@@ -23,7 +24,11 @@ class RgbaFrame extends Struct {
}
typedef F2 = Pointer Function(Pointer, Pointer);
-typedef F3 = void Function(Pointer, Pointer);
+typedef F3 = Pointer Function(Pointer);
+typedef F4 = Uint64 Function(Pointer);
+typedef F4Dart = int Function(Pointer);
+typedef F5 = Void Function(Pointer);
+typedef F5Dart = void Function(Pointer);
typedef HandleEvent = Future Function(Map evt);
/// FFI wrapper around the native Rust core.
@@ -44,6 +49,9 @@ class PlatformFFI {
final _toAndroidChannel = const MethodChannel('mChannel');
RustdeskImpl get ffiBind => _ffiBind;
+ F3? _session_get_rgba;
+ F4Dart? _session_get_rgba_size;
+ F5Dart? _session_next_rgba;
static get localeName => Platform.localeName;
@@ -92,6 +100,36 @@ class PlatformFFI {
return res;
}
+ Uint8List? getRgba(String id, int bufSize) {
+ if (_session_get_rgba == null) return null;
+ var a = id.toNativeUtf8();
+ try {
+ final buffer = _session_get_rgba!(a);
+ if (buffer == nullptr) {
+ return null;
+ }
+ final data = buffer.asTypedList(bufSize);
+ return data;
+ } finally {
+ malloc.free(a);
+ }
+ }
+
+ int? getRgbaSize(String id) {
+ if (_session_get_rgba_size == null) return null;
+ var a = id.toNativeUtf8();
+ final bufferSize = _session_get_rgba_size!(a);
+ malloc.free(a);
+ return bufferSize;
+ }
+
+ void nextRgba(String id) {
+ if (_session_next_rgba == null) return;
+ final a = id.toNativeUtf8();
+ _session_next_rgba!(a);
+ malloc.free(a);
+ }
+
/// Init the FFI class, loads the native Rust core library.
Future init(String appType) async {
_appType = appType;
@@ -107,6 +145,11 @@ class PlatformFFI {
debugPrint('initializing FFI $_appType');
try {
_translate = dylib.lookupFunction('translate');
+ _session_get_rgba = dylib.lookupFunction("session_get_rgba");
+ _session_get_rgba_size =
+ dylib.lookupFunction("session_get_rgba_size");
+ _session_next_rgba =
+ dylib.lookupFunction("session_next_rgba");
try {
// SYSTEM user failed
_dir = (await getApplicationDocumentsDirectory()).path;
@@ -118,8 +161,12 @@ class PlatformFFI {
// Start a dbus service, no need to await
_ffiBind.mainStartDbusServer();
} else if (Platform.isMacOS && isMain) {
- // Start an ipc server for handling url schemes.
- _ffiBind.mainStartIpcUrlServer();
+ Future.wait([
+ // Start dbus service.
+ _ffiBind.mainStartDbusServer(),
+ // Start local audio pulseaudio server.
+ _ffiBind.mainStartPa()
+ ]);
}
_startListenEvent(_ffiBind); // global event
try {
diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart
index 56dca4cdf..b2043f3c2 100644
--- a/flutter/lib/models/server_model.dart
+++ b/flutter/lib/models/server_model.dart
@@ -560,10 +560,8 @@ class ServerModel with ChangeNotifier {
}
}
- closeAll() {
- for (var client in _clients) {
- bind.cmCloseConnection(connId: client.id);
- }
+ Future closeAll() async {
+ await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
_clients.clear();
tabController.state.value.tabs.clear();
}
@@ -579,6 +577,26 @@ class ServerModel with ChangeNotifier {
notifyListeners();
}
}
+
+ void updateVoiceCallState(Map evt) {
+ try {
+ final client = Client.fromJson(jsonDecode(evt["client"]));
+ final index = _clients.indexWhere((element) => element.id == client.id);
+ if (index != -1) {
+ _clients[index].inVoiceCall = client.inVoiceCall;
+ _clients[index].incomingVoiceCall = client.incomingVoiceCall;
+ if (client.incomingVoiceCall) {
+ // Has incoming phone call, let's set the window on top.
+ Future.delayed(Duration.zero, () {
+ window_on_top(null);
+ });
+ }
+ notifyListeners();
+ }
+ } catch (e) {
+ debugPrint("updateVoiceCallState failed: $e");
+ }
+ }
}
enum ClientType {
@@ -602,6 +620,8 @@ class Client {
bool recording = false;
bool disconnected = false;
bool fromSwitch = false;
+ bool inVoiceCall = false;
+ bool incomingVoiceCall = false;
RxBool hasUnreadChatMessage = false.obs;
@@ -623,6 +643,8 @@ class Client {
recording = json['recording'];
disconnected = json['disconnected'];
fromSwitch = json['from_switch'];
+ inVoiceCall = json['in_voice_call'];
+ incomingVoiceCall = json['incoming_voice_call'];
}
Map toJson() {
diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart
index e4c9fa03f..761c95ded 100644
--- a/flutter/lib/models/state_model.dart
+++ b/flutter/lib/models/state_model.dart
@@ -14,6 +14,7 @@ class StateGlobal {
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteMenuBar = false.obs;
+ final RxInt displaysCount = 0.obs;
int get windowId => _windowId;
bool get fullscreen => _fullscreen;
diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart
index 1f0d5b0cd..a153dbc63 100644
--- a/flutter/lib/utils/image.dart
+++ b/flutter/lib/utils/image.dart
@@ -1,6 +1,8 @@
import 'dart:typed_data';
import 'dart:ui' as ui;
+import 'package:flutter/widgets.dart';
+
Future decodeImageFromPixels(
Uint8List pixels,
int width,
@@ -9,6 +11,7 @@ Future decodeImageFromPixels(
int? rowBytes,
int? targetWidth,
int? targetHeight,
+ VoidCallback? onPixelsCopied,
bool allowUpscaling = true,
}) async {
if (targetWidth != null) {
@@ -20,6 +23,7 @@ Future decodeImageFromPixels(
final ui.ImmutableBuffer buffer =
await ui.ImmutableBuffer.fromUint8List(pixels);
+ onPixelsCopied?.call();
final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
buffer,
width: width,
@@ -47,3 +51,40 @@ Future decodeImageFromPixels(
descriptor.dispose();
return frameInfo.image;
}
+
+class ImagePainter extends CustomPainter {
+ ImagePainter({
+ required this.image,
+ required this.x,
+ required this.y,
+ required this.scale,
+ });
+
+ ui.Image? image;
+ double x;
+ double y;
+ double scale;
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ if (image == null) return;
+ if (x.isNaN || y.isNaN) return;
+ canvas.scale(scale, scale);
+ // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
+ // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
+ var paint = Paint();
+ if ((scale - 1.0).abs() > 0.001) {
+ paint.filterQuality = FilterQuality.medium;
+ if (scale > 10.00000) {
+ paint.filterQuality = FilterQuality.high;
+ }
+ }
+ canvas.drawImage(
+ image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
+ }
+
+ @override
+ bool shouldRepaint(CustomPainter oldDelegate) {
+ return oldDelegate != this;
+ }
+}
diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart
index 3af189ef6..864659a66 100644
--- a/flutter/lib/utils/multi_window_manager.dart
+++ b/flutter/lib/utils/multi_window_manager.dart
@@ -41,11 +41,15 @@ class RustDeskMultiWindowManager {
int? _fileTransferWindowId;
int? _portForwardWindowId;
- Future newRemoteDesktop(String remoteId,
- {String? switch_uuid}) async {
+ Future newRemoteDesktop(
+ String remoteId, {
+ String? switch_uuid,
+ bool? forceRelay,
+ }) async {
var params = {
"type": WindowType.RemoteDesktop.index,
"id": remoteId,
+ "forceRelay": forceRelay
};
if (switch_uuid != null) {
params['switch_uuid'] = switch_uuid;
@@ -78,9 +82,12 @@ class RustDeskMultiWindowManager {
}
}
- Future newFileTransfer(String remoteId) async {
- final msg =
- jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
+ Future newFileTransfer(String remoteId, {bool? forceRelay}) async {
+ var msg = jsonEncode({
+ "type": WindowType.FileTransfer.index,
+ "id": remoteId,
+ "forceRelay": forceRelay,
+ });
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
@@ -107,9 +114,14 @@ class RustDeskMultiWindowManager {
}
}
- Future newPortForward(String remoteId, bool isRDP) async {
- final msg = jsonEncode(
- {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP});
+ Future newPortForward(String remoteId, bool isRDP,
+ {bool? forceRelay}) async {
+ final msg = jsonEncode({
+ "type": WindowType.PortForward.index,
+ "id": remoteId,
+ "isRDP": isRDP,
+ "forceRelay": forceRelay,
+ });
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj
index 18166c8ff..0019335ef 100644
--- a/flutter/macos/Runner.xcodeproj/project.pbxproj
+++ b/flutter/macos/Runner.xcodeproj/project.pbxproj
@@ -26,10 +26,6 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
- 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; };
- 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; };
- 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */; };
- 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */; };
84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; };
@@ -64,7 +60,7 @@
295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
- 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
@@ -78,10 +74,6 @@
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
- 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = ""; };
- 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = ""; };
- 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light-x2.png"; path = "../../res/mac-tray-light-x2.png"; sourceTree = ""; };
- 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark-x2.png"; path = "../../res/mac-tray-dark-x2.png"; sourceTree = ""; };
84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
@@ -127,7 +119,7 @@
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
- 33CC10ED2044A3C60003C045 /* rustdesk.app */,
+ 33CC10ED2044A3C60003C045 /* RustDesk.app */,
);
name = Products;
sourceTree = "";
@@ -135,10 +127,6 @@
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
- 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */,
- 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */,
- 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */,
- 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */,
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
@@ -212,7 +200,7 @@
);
name = Runner;
productName = Runner;
- productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */;
+ productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -265,12 +253,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */,
- 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */,
- 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */,
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
- 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -462,7 +446,6 @@
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
- PRODUCT_NAME = rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -608,7 +591,6 @@
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
- PRODUCT_NAME = rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
@@ -646,7 +628,6 @@
/dev/null,
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
- PRODUCT_NAME = rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
index 9af6f2121..fc39cb2ff 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
index 493ec7076..d08d9bb62 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
index 4bed6f3fa..3bd2b7ede 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
index 22893b8ea..88f2eee49 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
index 583a48571..18151e82b 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
index d3aa91800..f8d7befb3 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
index f98ccf1f3..d2bd35cd1 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist
index c926019ab..96616e8c4 100644
--- a/flutter/macos/Runner/Info.plist
+++ b/flutter/macos/Runner/Info.plist
@@ -43,6 +43,8 @@
$(PRODUCT_COPYRIGHT)
NSMainNibFile
MainMenu
+ NSMicrophoneUsageDescription
+ Record the sound from microphone for the purpose of the remote desktop.
NSPrincipalClass
NSApplication
diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift
index 97b46bb84..21e870320 100644
--- a/flutter/macos/Runner/MainFlutterWindow.swift
+++ b/flutter/macos/Runner/MainFlutterWindow.swift
@@ -1,4 +1,5 @@
import Cocoa
+import AVFoundation
import FlutterMacOS
import desktop_multi_window
// import bitsdojo_window_macos
@@ -81,6 +82,23 @@ class MainFlutterWindow: NSWindow {
case "terminate":
NSApplication.shared.terminate(self)
result(nil)
+ case "canRecordAudio":
+ switch AVCaptureDevice.authorizationStatus(for: .audio) {
+ case .authorized:
+ result(1)
+ break
+ case .notDetermined:
+ result(0)
+ break
+ default:
+ result(-1)
+ break
+ }
+ case "requestRecordAudio":
+ AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
+ result(granted)
+ })
+ break
default:
result(FlutterMethodNotImplemented)
}
diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock
index 0a1b1dcc8..e6ec0454d 100644
--- a/flutter/pubspec.lock
+++ b/flutter/pubspec.lock
@@ -488,6 +488,54 @@ packages:
url: "https://github.com/Kingtous/flutter_improved_scrolling"
source: git
version: "0.0.3"
+ flutter_keyboard_visibility:
+ dependency: "direct main"
+ description:
+ name: flutter_keyboard_visibility
+ sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.4.0"
+ flutter_keyboard_visibility_linux:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_linux
+ sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ flutter_keyboard_visibility_macos:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_macos
+ sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ flutter_keyboard_visibility_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_platform_interface
+ sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ flutter_keyboard_visibility_web:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_web
+ sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ flutter_keyboard_visibility_windows:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_windows
+ sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
flutter_launcher_icons:
dependency: "direct main"
description:
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index 8701d9f5b..df29252c9 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -91,6 +91,7 @@ dependencies:
win32: any
password_strength: ^0.2.0
flutter_launcher_icons: ^0.11.0
+ flutter_keyboard_visibility: ^5.4.0
dev_dependencies:
diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart
index 592a28fcf..2c037c7b0 100644
--- a/flutter/test/cm_test.dart
+++ b/flutter/test/cm_test.dart
@@ -16,7 +16,7 @@ final testClients = [
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
];
-/// -t lib/cm_main.dart to test cm
+/// flutter run -d {platform} -t lib/cm_test.dart to test cm
void main(List args) async {
isTest = true;
WidgetsFlutterBinding.ensureInitialized();
diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml
index cc4173a97..fc4db9a63 100644
--- a/libs/enigo/Cargo.toml
+++ b/libs/enigo/Cargo.toml
@@ -37,8 +37,5 @@ core-graphics = "0.22"
objc = "0.2"
unicode-segmentation = "1.6"
-[target.'cfg(target_os = "linux")'.dependencies]
-libc = "0.2"
-
[build-dependencies]
pkg-config = "0.3"
diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs
index 2115d7283..f0f7d49af 100644
--- a/libs/enigo/src/linux/xdo.rs
+++ b/libs/enigo/src/linux/xdo.rs
@@ -1,8 +1,6 @@
-use libc;
-
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
-use self::libc::{c_char, c_int, c_void, useconds_t};
+use hbb_common::libc::{c_char, c_int, c_void, useconds_t};
use std::{borrow::Cow, ffi::CString, ptr};
const CURRENT_WINDOW: c_int = 0;
diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs
index 2e1108b9e..115cb9789 100644
--- a/libs/enigo/src/win/win_impl.rs
+++ b/libs/enigo/src/win/win_impl.rs
@@ -39,7 +39,7 @@ fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD {
unsafe { SendInput(1, &mut input as LPINPUT, size_of:: () as c_int) }
}
-fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD {
+fn keybd_event(mut flags: u32, vk: u16, scan: u16) -> DWORD {
let mut scan = scan;
unsafe {
// https://github.com/rustdesk/rustdesk/issues/366
@@ -52,35 +52,33 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD {
scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _;
}
}
- let mut input: INPUT = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
- input.type_ = INPUT_KEYBOARD;
+
+ if flags & KEYEVENTF_UNICODE == 0 {
+ if scan >> 8 == 0xE0 || scan >> 8 == 0xE1 {
+ flags |= winapi::um::winuser::KEYEVENTF_EXTENDEDKEY;
+ }
+ }
+ let mut union: INPUT_u = unsafe { std::mem::zeroed() };
unsafe {
- let dst_ptr = (&mut input.u as *mut _) as *mut u8;
- let flags = match vk as _ {
- winapi::um::winuser::VK_HOME |
- winapi::um::winuser::VK_UP |
- winapi::um::winuser::VK_PRIOR |
- winapi::um::winuser::VK_LEFT |
- winapi::um::winuser::VK_RIGHT |
- winapi::um::winuser::VK_END |
- winapi::um::winuser::VK_DOWN |
- winapi::um::winuser::VK_NEXT |
- winapi::um::winuser::VK_INSERT |
- winapi::um::winuser::VK_DELETE => flags | winapi::um::winuser::KEYEVENTF_EXTENDEDKEY,
- _ => flags,
- };
-
- let k = KEYBDINPUT {
+ *union.ki_mut() = KEYBDINPUT {
wVk: vk,
wScan: scan,
dwFlags: flags,
time: 0,
dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE,
};
- let src_ptr = (&k as *const _) as *const u8;
- std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size_of::());
}
- unsafe { SendInput(1, &mut input as LPINPUT, size_of:: () as c_int) }
+ let mut inputs = [INPUT {
+ type_: INPUT_KEYBOARD,
+ u: union,
+ }; 1];
+ unsafe {
+ SendInput(
+ inputs.len() as UINT,
+ inputs.as_mut_ptr(),
+ size_of:: () as c_int,
+ )
+ }
}
fn get_error() -> String {
diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml
index 59f0896cc..0457bb19a 100644
--- a/libs/hbb_common/Cargo.toml
+++ b/libs/hbb_common/Cargo.toml
@@ -31,6 +31,8 @@ sodiumoxide = "0.2"
regex = "1.4"
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
chrono = "0.4"
+backtrace = "0.3"
+libc = "0.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
@@ -46,6 +48,9 @@ protobuf-codegen = { version = "3.1" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
+[target.'cfg(target_os = "macos")'.dependencies]
+osascript = "0.3.0"
+
[dev-dependencies]
toml = "0.5"
serde_json = "1.0"
diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs
index fe0d31076..5ebc3a287 100644
--- a/libs/hbb_common/build.rs
+++ b/libs/hbb_common/build.rs
@@ -2,11 +2,11 @@ fn main() {
let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap());
std::fs::create_dir_all(&out_dir).unwrap();
-
+
protobuf_codegen::Codegen::new()
.pure()
.out_dir(out_dir)
- .inputs(&["protos/rendezvous.proto", "protos/message.proto"])
+ .inputs(["protos/rendezvous.proto", "protos/message.proto"])
.include("protos")
.customize(protobuf_codegen::Customize::default().tokio_bytes(true))
.run()
diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs
new file mode 100644
index 000000000..0be788428
--- /dev/null
+++ b/libs/hbb_common/examples/system_message.rs
@@ -0,0 +1,20 @@
+extern crate hbb_common;
+#[cfg(target_os = "linux")]
+use hbb_common::platform::linux;
+#[cfg(target_os = "macos")]
+use hbb_common::platform::macos;
+
+fn main() {
+ #[cfg(target_os = "linux")]
+ let res = linux::system_message("test title", "test message", true);
+ #[cfg(target_os = "macos")]
+ let res = macos::alert(
+ "System Preferences".to_owned(),
+ "warning".to_owned(),
+ "test title".to_owned(),
+ "test message".to_owned(),
+ ["Ok".to_owned()].to_vec(),
+ );
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ println!("result {:?}", &res);
+}
diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto
index b7965f237..2a3fd05b4 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -201,6 +201,8 @@ message KeyEvent {
bool press = 2;
oneof union {
ControlKey control_key = 3;
+ // high word, sym key code. win: virtual-key code, linux: keysym ?, macos:
+ // low word, position key code. win: scancode, linux: key code, macos: key code
uint32 chr = 4;
uint32 unicode = 5;
string seq = 6;
@@ -598,6 +600,18 @@ message Misc {
}
}
+message VoiceCallRequest {
+ int64 req_timestamp = 1;
+ // Indicates whether the request is a connect action or a disconnect action.
+ bool is_connect = 2;
+}
+
+message VoiceCallResponse {
+ bool accepted = 1;
+ int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
+ int64 ack_timestamp = 3;
+}
+
message Message {
oneof union {
SignedId signed_id = 3;
@@ -620,5 +634,8 @@ message Message {
Cliprdr cliprdr = 20;
MessageBox message_box = 21;
SwitchSidesResponse switch_sides_response = 22;
+ VoiceCallRequest voice_call_request = 23;
+ VoiceCallResponse voice_call_response = 24;
+ PeerInfo peer_info = 25;
}
}
diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs
index 699aa9bff..bfc798715 100644
--- a/libs/hbb_common/src/bytes_codec.rs
+++ b/libs/hbb_common/src/bytes_codec.rs
@@ -143,32 +143,32 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec = Vec::new();
bytes.resize(0x3F, 1);
- assert!(!codec.encode(bytes.into(), &mut buf).is_err());
+ assert!(codec.encode(bytes.into(), &mut buf).is_ok());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3F + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
- assert!(false);
+ panic!();
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
- assert!(false);
+ panic!();
}
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
- assert!(false);
+ panic!();
}
buf2.extend(&buf_saved[1..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
- assert!(false);
+ panic!();
}
}
@@ -177,21 +177,21 @@ mod tests {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec = Vec::new();
- assert!(!codec.encode("".into(), &mut buf).is_err());
+ assert!(codec.encode("".into(), &mut buf).is_ok());
assert_eq!(buf.len(), 1);
bytes.resize(0x3F + 1, 2);
- assert!(!codec.encode(bytes.into(), &mut buf).is_err());
+ assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3F + 2 + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0);
} else {
- assert!(false);
+ panic!();
}
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F + 1);
assert_eq!(res[0], 2);
} else {
- assert!(false);
+ panic!();
}
}
@@ -201,13 +201,13 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec = Vec::new();
bytes.resize(0x3F - 1, 3);
- assert!(!codec.encode(bytes.into(), &mut buf).is_err());
+ assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3F + 1 - 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F - 1);
assert_eq!(res[0], 3);
} else {
- assert!(false);
+ panic!();
}
}
#[test]
@@ -216,13 +216,13 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec = Vec::new();
bytes.resize(0x3FFF, 4);
- assert!(!codec.encode(bytes.into(), &mut buf).is_err());
+ assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3FFF + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFF);
assert_eq!(res[0], 4);
} else {
- assert!(false);
+ panic!();
}
}
@@ -232,13 +232,13 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec = Vec::new();
bytes.resize(0x3FFFFF, 5);
- assert!(!codec.encode(bytes.into(), &mut buf).is_err());
+ assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3FFFFF + 3);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF);
assert_eq!(res[0], 5);
} else {
- assert!(false);
+ panic!();
}
}
@@ -248,33 +248,33 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec = Vec::new();
bytes.resize(0x3FFFFF + 1, 6);
- assert!(!codec.encode(bytes.into(), &mut buf).is_err());
+ assert!(codec.encode(bytes.into(), &mut buf).is_ok());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3FFFFF + 4 + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
- assert!(false);
+ panic!();
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
- assert!(false);
+ panic!();
}
buf2.extend(&buf_saved[1..6]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
- assert!(false);
+ panic!();
}
buf2.extend(&buf_saved[6..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
- assert!(false);
+ panic!();
}
}
}
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index 71dd9a5c6..3bfc885c5 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -30,13 +30,7 @@ pub const REG_INTERVAL: i64 = 12_000;
pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3;
const PASSWORD_ENC_VERSION: &str = "00";
-// 128x128
-#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding
-pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII=
-";
-#[cfg(not(target_os = "macos"))] // 128x128 no padding
-pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA7VBMVEUAAAAAcf8Acf8Acf8Adf8Acf8Acf8AcP8Acv8AcP8Acf8Acf8Acf8Acv8Acf8Acf8Ab/8AcP8Acf8Acf8Acf/////7/f8Dc/8TfP/1+f/n8v9Hmf/u9v+Uw//Q5f9hp/8Yfv8Qev8Ld/+52P+z1f+s0f81j/8wjP8Hdf/3+/8mh/8fg//x9//h7//H4P9xsP9rrf9oq/8rif/r9P/D3v+92/+Duv9bpP/d7f/U5/9NnP8/lP8jhP/L4v/B3P+OwP9+t/95tf9Rn/8bgf/Z6v+Zx/90sv9lqf85kf+hy/9UoP+Wxf+kzP+dyP+Lvv/H4q8IAAAAFHRSTlMA+u6bB6x5XR4V0+S4i4k5N+a81W8MiAQAAAVcSURBVHjazdvpWtpAGIbhgEutdW3fL2GHsMsiq4KI+66t5384XahF/GbizJAy3j/1Ah5CJhNCxpm1vbryLRrBfxKJrq+sbjtSa5u7WIDdzTVH5PNSBAsSWfrsMJ+iWKDoJ2fW8hIWbGl55vW/YuE2XhUsb8CCr9OCJVix9G//gyWf/o6/KCyJfrbwAfAPYS0CayK/j4mbsGjrV8AXWLTrONuwasdZhVWrzgqsWnG+wap1Jwqrok4EVkUcmKhdVvBaOVnzYEY/oJpMD4mo6ONF/ZSIUsX2FZjQA7xRqUET+y/v2W/Sy59u62DCDMgdJmhqgIk7eqWQBBNWwPhmj147w8QTzTjKVsGEEBBLuzSrhIkivTF8DD/Aa6forQNMHBD/VyXkgHGfuBN5ALln1TADOnESyGCiT8L/1kILqD6Q0BEm9kkofhdSwNUJiV1jQvZ/SnthBNSaJJGZbgGJUnX+gEqCZPpsJ2T2Y/MGVBrE8eOAvCA/X8A4QXLnmEhTgIPqPAG5IQU4fhmkFOT7HAFenwIU8Jd/TUEODQIUtu1eOj/dUD9cknOTpgEDkup3YrOfVStDUomcWcBVisTiNxVw3TPpgCl4RgFFybZ/9iHmn8uS2yYBA8m7qUEu9oOEejH9gHxC+PazCHbcFM8K+gGHJNAs4z2xgnAkVHQDcnG1IzvnCSfvom7AM3EZ9voah4+KXoAvGFJHMSgqEfegF3BBTKoOVfkMMXFfJ8AT7MuXUDeOE9PWCUiKBpKOlmAP1gngH2LChw7vhJgr9YD8Hnt0BxrE27CtHnDJR4AHTX1+KFAP4Ef0LHTxN9HwlAMSbAjmoavKZ8ayakDXYAhwN3wzqgZk2UPvwRjshmeqATeCT09f3mWnEqoBGf4NxAB/moRqADuOtmDiid6KqQVcsQeOYOKW3uqqBRwL5nITj/yrlFpAVrDpTJT5llQLaLMHwshY7UDgvD+VujDC96WWWsBtSAE5FnChFnAeUkDMdAvw88EqTNT5SYXpTlgPaRQM1AIGorkolNnoUS1gJHigCX48SaoF3Asuspg4Mz0U8+FTgIkCG01V09kwBQP8xG5ofD5AXeirkPEJSUlwSVIfP5ykVQNaggvz+k7prTvVgDKF8BnUXP4kqgEe/257E8Ig7EE1gA8g2stBTz7FLxqrB3SIeYaeQ2IG6gE5l2+Cmt5MGOfP4KsGiH8DOYWOoujnDY2ALHF3810goZFOQDVBTFx9Uj7eI6bp6QTgnLjeGGq6KeJuoRUQixN3pDYWyz1Rva8XIL5UPFQZCsmG3gV7R+dieS+Jd3iHLglce7oBuCOhp3zwHLxPQpfQDvBOSKjZqUIml3ZJ6AD6AajFSZJwewWR8ZPsEY26SQDaJOMeZP23w6bTJ6kBjAJQILm9hzqm7otu4G+nhgGxIQUlPLKzL7GhbxqAboMCuN2XXd+lAL0ajAMwclV+FD6jAPEy5ghAlhfwX2FODX445gHKxyN++fs64PUHmDMAbbYN2DlKk2QaScwdgMs4SZxMv4OJJSoIIQBl2Qtk3gk4qiOUANRPJQHB+0A6j5AC4J27QQEZ4eZPAsYBXFk0N/YD7iUrxRBqALxOTzoMC3x8lCFlfkMjuz8iLfk6fzQCQgjg8q3ZEd8RzUVuKelBh96Nzcc3qelL1V+2zfRv1xc56Ino3tpdPT7cd//MspfTrD/7R6p4W4O2qLMObfnyIHvvYcrPtkZjDybW7d/eb32Bg/UlHnYXuXz5CMt8rC90sr7Uy/5iN+vL/ewveLS/5NNKwcbyR1r2a3/h8wdY+v3L2tZC5oUvW2uO1M7qyvp/Xv6/48z4CTxjJEfyjEaMAAAAAElFTkSuQmCC
-";
+
#[cfg(target_os = "macos")]
lazy_static::lazy_static! {
pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned()));
@@ -288,7 +282,7 @@ fn patch(path: PathBuf) -> PathBuf {
.trim()
.to_owned();
if user != "root" {
- return format!("/home/{}", user).into();
+ return format!("/home/{user}").into();
}
}
}
@@ -525,7 +519,7 @@ impl Config {
let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into();
fs::create_dir(&path).ok();
fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok();
- path.push(format!("ipc{}", postfix));
+ path.push(format!("ipc{postfix}"));
path.to_str().unwrap_or("").to_owned()
}
}
@@ -562,7 +556,7 @@ impl Config {
.unwrap_or_default();
}
if !rendezvous_server.contains(':') {
- rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT);
+ rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}");
}
rendezvous_server
}
diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs
index c9f9e90d7..99cb6f408 100644
--- a/libs/hbb_common/src/lib.rs
+++ b/libs/hbb_common/src/lib.rs
@@ -39,6 +39,7 @@ pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr;
pub mod password_security;
pub use chrono;
+pub use libc;
pub use directories_next;
pub mod keyboard;
@@ -211,11 +212,7 @@ pub fn gen_version() {
// generate build date
let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
file.write_all(
- format!(
- "#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{}\";",
- build_date
- )
- .as_bytes(),
+ format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(),
)
.ok();
file.sync_all().ok();
@@ -342,39 +339,39 @@ mod test {
#[test]
fn test_ipv6() {
- assert_eq!(is_ipv6_str("1:2:3"), true);
- assert_eq!(is_ipv6_str("[ab:2:3]:12"), true);
- assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true);
- assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false);
- assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false);
- assert_eq!(is_ipv6_str("1.1.1.1"), false);
- assert_eq!(is_ip_str("1.1.1.1"), true);
- assert_eq!(is_ipv6_str("1:2:"), false);
- assert_eq!(is_ipv6_str("1:2::0"), true);
- assert_eq!(is_ipv6_str("[1:2::0]:1"), true);
- assert_eq!(is_ipv6_str("[1:2::0]:"), false);
- assert_eq!(is_ipv6_str("1:2::0]:1"), false);
+ assert!(is_ipv6_str("1:2:3"));
+ assert!(is_ipv6_str("[ab:2:3]:12"));
+ assert!(is_ipv6_str("[ABEF:2a:3]:12"));
+ assert!(!is_ipv6_str("[ABEG:2a:3]:12"));
+ assert!(!is_ipv6_str("1[ab:2:3]:12"));
+ assert!(!is_ipv6_str("1.1.1.1"));
+ assert!(is_ip_str("1.1.1.1"));
+ assert!(!is_ipv6_str("1:2:"));
+ assert!(is_ipv6_str("1:2::0"));
+ assert!(is_ipv6_str("[1:2::0]:1"));
+ assert!(!is_ipv6_str("[1:2::0]:"));
+ assert!(!is_ipv6_str("1:2::0]:1"));
}
#[test]
fn test_hostname_port() {
- assert_eq!(is_domain_port_str("a:12"), false);
- assert_eq!(is_domain_port_str("a.b.c:12"), false);
- assert_eq!(is_domain_port_str("test.com:12"), true);
- assert_eq!(is_domain_port_str("test-UPPER.com:12"), true);
- assert_eq!(is_domain_port_str("some-other.domain.com:12"), true);
- assert_eq!(is_domain_port_str("under_score:12"), false);
- assert_eq!(is_domain_port_str("a@bc:12"), false);
- assert_eq!(is_domain_port_str("1.1.1.1:12"), false);
- assert_eq!(is_domain_port_str("1.2.3:12"), false);
- assert_eq!(is_domain_port_str("1.2.3.45:12"), false);
- assert_eq!(is_domain_port_str("a.b.c:123456"), false);
- assert_eq!(is_domain_port_str("---:12"), false);
- assert_eq!(is_domain_port_str(".:12"), false);
+ assert!(!is_domain_port_str("a:12"));
+ assert!(!is_domain_port_str("a.b.c:12"));
+ assert!(is_domain_port_str("test.com:12"));
+ assert!(is_domain_port_str("test-UPPER.com:12"));
+ assert!(is_domain_port_str("some-other.domain.com:12"));
+ assert!(!is_domain_port_str("under_score:12"));
+ assert!(!is_domain_port_str("a@bc:12"));
+ assert!(!is_domain_port_str("1.1.1.1:12"));
+ assert!(!is_domain_port_str("1.2.3:12"));
+ assert!(!is_domain_port_str("1.2.3.45:12"));
+ assert!(!is_domain_port_str("a.b.c:123456"));
+ assert!(!is_domain_port_str("---:12"));
+ assert!(!is_domain_port_str(".:12"));
// todo: should we also check for these edge cases?
// out-of-range port
- assert_eq!(is_domain_port_str("test.com:0"), true);
- assert_eq!(is_domain_port_str("test.com:98989"), true);
+ assert!(is_domain_port_str("test.com:0"));
+ assert!(is_domain_port_str("test.com:98989"));
}
#[test]
diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs
index 0b66107fc..ddfe28baa 100644
--- a/libs/hbb_common/src/password_security.rs
+++ b/libs/hbb_common/src/password_security.rs
@@ -192,51 +192,51 @@ mod test {
let data = "Hello World";
let encrypted = encrypt_str_or_original(data, version);
let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
- println!("data: {}", data);
- println!("encrypted: {}", encrypted);
- println!("decrypted: {}", decrypted);
+ println!("data: {data}");
+ println!("encrypted: {encrypted}");
+ println!("decrypted: {decrypted}");
assert_eq!(data, decrypted);
assert_eq!(version, &encrypted[..2]);
- assert_eq!(succ, true);
- assert_eq!(store, false);
+ assert!(succ);
+ assert!(!store);
let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
- assert_eq!(store, true);
- assert_eq!(decrypt_str_or_original(&decrypted, version).1, false);
+ assert!(store);
+ assert!(!decrypt_str_or_original(&decrypted, version).1);
assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted);
println!("test vec");
let data: Vec = vec![1, 2, 3, 4, 5, 6];
let encrypted = encrypt_vec_or_original(&data, version);
let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
- println!("data: {:?}", data);
- println!("encrypted: {:?}", encrypted);
- println!("decrypted: {:?}", decrypted);
+ println!("data: {data:?}");
+ println!("encrypted: {encrypted:?}");
+ println!("decrypted: {decrypted:?}");
assert_eq!(data, decrypted);
assert_eq!(version.as_bytes(), &encrypted[..2]);
- assert_eq!(store, false);
- assert_eq!(succ, true);
+ assert!(!store);
+ assert!(succ);
let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
- assert_eq!(store, true);
- assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false);
+ assert!(store);
+ assert!(!decrypt_vec_or_original(&decrypted, version).1);
assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted);
println!("test original");
let data = version.to_string() + "Hello World";
let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
assert_eq!(data, decrypted);
- assert_eq!(store, true);
- assert_eq!(succ, false);
+ assert!(store);
+ assert!(!succ);
let verbytes = version.as_bytes();
- let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6];
+ let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6];
let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
assert_eq!(data, decrypted);
- assert_eq!(store, true);
- assert_eq!(succ, false);
+ assert!(store);
+ assert!(!succ);
let (_, succ, store) = decrypt_str_or_original("", version);
- assert_eq!(store, false);
- assert_eq!(succ, false);
- let (_, succ, store) = decrypt_vec_or_original(&vec![], version);
- assert_eq!(store, false);
- assert_eq!(succ, false);
+ assert!(!store);
+ assert!(!succ);
+ let (_, succ, store) = decrypt_vec_or_original(&[], version);
+ assert!(!store);
+ assert!(!succ);
}
}
diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs
index 716025dc7..191ea2e6f 100644
--- a/libs/hbb_common/src/platform/linux.rs
+++ b/libs/hbb_common/src/platform/linux.rs
@@ -1,4 +1,5 @@
use crate::ResultType;
+use std::{collections::HashMap, process::Command};
lazy_static::lazy_static! {
pub static ref DISTRO: Distro = Distro::new();
@@ -60,7 +61,7 @@ fn get_display_server_of_session(session: &str) -> String {
.replace("TTY=", "")
.trim_end()
.into();
- if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty))
+ if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
// And check if Xorg is running on that tty
{
if xorg_results.trim_end() != "" {
@@ -155,3 +156,42 @@ fn run_loginctl(args: Option>) -> std::io::Result ResultType<()> {
+ let cmds: HashMap<&str, Vec<&str>> = HashMap::from([
+ ("notify-send", [title, msg].to_vec()),
+ (
+ "zenity",
+ [
+ "--info",
+ "--timeout",
+ if forever { "0" } else { "3" },
+ "--title",
+ title,
+ "--text",
+ msg,
+ ]
+ .to_vec(),
+ ),
+ ("kdialog", ["--title", title, "--msgbox", msg].to_vec()),
+ (
+ "xmessage",
+ [
+ "-center",
+ "-timeout",
+ if forever { "0" } else { "3" },
+ title,
+ msg,
+ ]
+ .to_vec(),
+ ),
+ ]);
+ for (k, v) in cmds {
+ if Command::new(k).args(v).spawn().is_ok() {
+ return Ok(());
+ }
+ }
+ crate::bail!("failed to post system message");
+}
diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs
new file mode 100644
index 000000000..dd83a8738
--- /dev/null
+++ b/libs/hbb_common/src/platform/macos.rs
@@ -0,0 +1,55 @@
+use crate::ResultType;
+use osascript;
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(Serialize)]
+struct AlertParams {
+ title: String,
+ message: String,
+ alert_type: String,
+ buttons: Vec,
+}
+
+#[derive(Deserialize)]
+struct AlertResult {
+ #[serde(rename = "buttonReturned")]
+ button: String,
+}
+
+/// Firstly run the specified app, then alert a dialog. Return the clicked button value.
+///
+/// # Arguments
+///
+/// * `app` - The app to execute the script.
+/// * `alert_type` - Alert type. . informational, warning, critical
+/// * `title` - The alert title.
+/// * `message` - The alert message.
+/// * `buttons` - The buttons to show.
+pub fn alert(
+ app: String,
+ alert_type: String,
+ title: String,
+ message: String,
+ buttons: Vec,
+) -> ResultType {
+ let script = osascript::JavaScript::new(&format!(
+ "
+ var App = Application('{}');
+ App.includeStandardAdditions = true;
+ return App.displayAlert($params.title, {{
+ message: $params.message,
+ 'as': $params.alert_type,
+ buttons: $params.buttons,
+ }});
+ ",
+ app
+ ));
+
+ let result: AlertResult = script.execute_with_params(AlertParams {
+ title,
+ message,
+ alert_type,
+ buttons,
+ })?;
+ Ok(result.button)
+}
diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs
index 8daba257f..aa929ca99 100644
--- a/libs/hbb_common/src/platform/mod.rs
+++ b/libs/hbb_common/src/platform/mod.rs
@@ -1,2 +1,51 @@
#[cfg(target_os = "linux")]
pub mod linux;
+
+#[cfg(target_os = "macos")]
+pub mod macos;
+
+use crate::{config::Config, log};
+use std::process::exit;
+
+extern "C" fn breakdown_signal_handler(sig: i32) {
+ let mut stack = vec![];
+ backtrace::trace(|frame| {
+ backtrace::resolve_frame(frame, |symbol| {
+ if let Some(name) = symbol.name() {
+ stack.push(name.to_string());
+ }
+ });
+ true // keep going to the next frame
+ });
+ let mut info = String::default();
+ if stack.iter().any(|s| {
+ s.contains(&"nouveau_pushbuf_kick")
+ || s.to_lowercase().contains("nvidia")
+ || s.contains("gdk_window_end_draw_frame")
+ }) {
+ Config::set_option("allow-always-software-render".to_string(), "Y".to_string());
+ info = "Always use software rendering will be set.".to_string();
+ log::info!("{}", info);
+ }
+ log::error!(
+ "Got signal {} and exit. stack:\n{}",
+ sig,
+ stack.join("\n").to_string()
+ );
+ if !info.is_empty() {
+ #[cfg(target_os = "linux")]
+ linux::system_message(
+ "RustDesk",
+ &format!("Got signal {} and exit.{}", sig, info),
+ true,
+ )
+ .ok();
+ }
+ exit(0);
+}
+
+pub fn register_breakdown_handler() {
+ unsafe {
+ libc::signal(libc::SIGSEGV, breakdown_signal_handler as _);
+ }
+}
diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs
index c001c58fb..57d9b68fe 100644
--- a/libs/hbb_common/src/protos/mod.rs
+++ b/libs/hbb_common/src/protos/mod.rs
@@ -1 +1 @@
-include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
\ No newline at end of file
+include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs
index a034b4e12..2d9b5a984 100644
--- a/libs/hbb_common/src/socket_client.rs
+++ b/libs/hbb_common/src/socket_client.rs
@@ -13,22 +13,22 @@ use tokio_socks::{IntoTargetAddr, TargetAddr};
pub fn check_port(host: T, port: i32) -> String {
let host = host.to_string();
if crate::is_ipv6_str(&host) {
- if host.starts_with("[") {
+ if host.starts_with('[') {
return host;
}
- return format!("[{}]:{}", host, port);
+ return format!("[{host}]:{port}");
}
- if !host.contains(":") {
- return format!("{}:{}", host, port);
+ if !host.contains(':') {
+ return format!("{host}:{port}");
}
- return host;
+ host
}
#[inline]
pub fn increase_port(host: T, offset: i32) -> String {
let host = host.to_string();
if crate::is_ipv6_str(&host) {
- if host.starts_with("[") {
+ if host.starts_with('[') {
let tmp: Vec<&str> = host.split("]:").collect();
if tmp.len() == 2 {
let port: i32 = tmp[1].parse().unwrap_or(0);
@@ -37,8 +37,8 @@ pub fn increase_port(host: T, offset: i32) -> String {
}
}
}
- } else if host.contains(":") {
- let tmp: Vec<&str> = host.split(":").collect();
+ } else if host.contains(':') {
+ let tmp: Vec<&str> = host.split(':').collect();
if tmp.len() == 2 {
let port: i32 = tmp[1].parse().unwrap_or(0);
if port > 0 {
@@ -46,7 +46,7 @@ pub fn increase_port(host: T, offset: i32) -> String {
}
}
}
- return host;
+ host
}
pub fn test_if_valid_server(host: &str) -> String {
@@ -148,7 +148,7 @@ pub async fn query_nip_io(addr: &SocketAddr) -> ResultType {
pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String {
if !ipv4 && crate::is_ipv4_str(&addr) {
if let Some(ip) = addr.split(':').next() {
- return addr.replace(ip, &format!("{}.nip.io", ip));
+ return addr.replace(ip, &format!("{ip}.nip.io"));
}
}
addr
@@ -163,7 +163,7 @@ async fn test_target(target: &str) -> ResultType {
tokio::net::lookup_host(target)
.await?
.next()
- .context(format!("Failed to look up host for {}", target))
+ .context(format!("Failed to look up host for {target}"))
}
#[inline]
diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs
index a7ac4eb3a..f574e8309 100644
--- a/libs/hbb_common/src/tcp.rs
+++ b/libs/hbb_common/src/tcp.rs
@@ -100,7 +100,7 @@ impl FramedStream {
}
}
}
- bail!(format!("Failed to connect to {}", remote_addr));
+ bail!(format!("Failed to connect to {remote_addr}"));
}
pub async fn connect<'a, 't, P, T>(
diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml
index e2eb43177..82cb88faf 100644
--- a/libs/scrap/Cargo.toml
+++ b/libs/scrap/Cargo.toml
@@ -16,7 +16,6 @@ mediacodec = ["ndk"]
[dependencies]
block = "0.1"
cfg-if = "1.0"
-libc = "0.2"
num_cpus = "1.13"
lazy_static = "1.4"
hbb_common = { path = "../hbb_common" }
diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs
index 504f0a4b3..77070d1a2 100644
--- a/libs/scrap/src/lib.rs
+++ b/libs/scrap/src/lib.rs
@@ -2,7 +2,7 @@
extern crate block;
#[macro_use]
extern crate cfg_if;
-pub extern crate libc;
+pub use hbb_common::libc;
#[cfg(dxgi)]
extern crate winapi;
diff --git a/libs/scrap/src/quartz/capturer.rs b/libs/scrap/src/quartz/capturer.rs
index 5be55ea22..cf442c2b4 100644
--- a/libs/scrap/src/quartz/capturer.rs
+++ b/libs/scrap/src/quartz/capturer.rs
@@ -1,7 +1,7 @@
use std::ptr;
use block::{Block, ConcreteBlock};
-use libc::c_void;
+use hbb_common::libc::c_void;
use std::sync::{Arc, Mutex};
use super::config::Config;
diff --git a/libs/scrap/src/quartz/config.rs b/libs/scrap/src/quartz/config.rs
index 11a6d5fc0..d5f992f0b 100644
--- a/libs/scrap/src/quartz/config.rs
+++ b/libs/scrap/src/quartz/config.rs
@@ -1,6 +1,6 @@
use std::ptr;
-use libc::c_void;
+use hbb_common::libc::c_void;
use super::ffi::*;
diff --git a/libs/scrap/src/quartz/ffi.rs b/libs/scrap/src/quartz/ffi.rs
index ca39c0a61..6b8c6e0e1 100644
--- a/libs/scrap/src/quartz/ffi.rs
+++ b/libs/scrap/src/quartz/ffi.rs
@@ -1,7 +1,7 @@
#![allow(dead_code)]
use block::RcBlock;
-use libc::c_void;
+use hbb_common::libc::c_void;
pub type CGDisplayStreamRef = *mut c_void;
pub type CFDictionaryRef = *mut c_void;
diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs
index 0dcfcfdab..6486af55c 100644
--- a/libs/scrap/src/x11/capturer.rs
+++ b/libs/scrap/src/x11/capturer.rs
@@ -1,6 +1,6 @@
use std::{io, ptr, slice};
-use libc;
+use hbb_common::libc;
use super::ffi::*;
use super::Display;
diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs
index 5df5c46a8..500f57615 100644
--- a/libs/scrap/src/x11/ffi.rs
+++ b/libs/scrap/src/x11/ffi.rs
@@ -1,6 +1,6 @@
#![allow(non_camel_case_types)]
-use libc::c_void;
+use hbb_common::libc::c_void;
#[link(name = "xcb")]
#[link(name = "xcb-shm")]
diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs
index cb3310be9..406c27352 100644
--- a/libs/scrap/src/x11/iter.rs
+++ b/libs/scrap/src/x11/iter.rs
@@ -1,7 +1,7 @@
use std::ptr;
use std::rc::Rc;
-use libc;
+use hbb_common::libc;
use super::ffi::*;
use super::{Display, Rect, Server};
diff --git a/res/logo.svg b/res/logo.svg
index 0001d0762..965218c95 100644
--- a/res/logo.svg
+++ b/res/logo.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/res/mac-icon.png b/res/mac-icon.png
index b6e08923f..fc39cb2ff 100644
Binary files a/res/mac-icon.png and b/res/mac-icon.png differ
diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png
index bdd48ad15..595b850ae 100644
Binary files a/res/mac-tray-dark-x2.png and b/res/mac-tray-dark-x2.png differ
diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png
deleted file mode 100644
index a98fe63b0..000000000
Binary files a/res/mac-tray-dark.png and /dev/null differ
diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png
index 253450ecb..2e2711888 100644
Binary files a/res/mac-tray-light-x2.png and b/res/mac-tray-light-x2.png differ
diff --git a/res/mac-tray-light.png b/res/mac-tray-light.png
deleted file mode 100644
index b827e462f..000000000
Binary files a/res/mac-tray-light.png and /dev/null differ
diff --git a/res/setup.nsi b/res/setup.nsi
index 5410e0ff5..635851d0a 100644
--- a/res/setup.nsi
+++ b/res/setup.nsi
@@ -56,8 +56,74 @@ InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}"
####################################################################
# Language
-!insertmacro MUI_LANGUAGE "English"
+!insertmacro MUI_LANGUAGE "English" ; The first language is the default language
+!insertmacro MUI_LANGUAGE "French"
+!insertmacro MUI_LANGUAGE "German"
+!insertmacro MUI_LANGUAGE "Spanish"
+!insertmacro MUI_LANGUAGE "SpanishInternational"
!insertmacro MUI_LANGUAGE "SimpChinese"
+!insertmacro MUI_LANGUAGE "TradChinese"
+!insertmacro MUI_LANGUAGE "Japanese"
+!insertmacro MUI_LANGUAGE "Korean"
+!insertmacro MUI_LANGUAGE "Italian"
+!insertmacro MUI_LANGUAGE "Dutch"
+!insertmacro MUI_LANGUAGE "Danish"
+!insertmacro MUI_LANGUAGE "Swedish"
+!insertmacro MUI_LANGUAGE "Norwegian"
+!insertmacro MUI_LANGUAGE "NorwegianNynorsk"
+!insertmacro MUI_LANGUAGE "Finnish"
+!insertmacro MUI_LANGUAGE "Greek"
+!insertmacro MUI_LANGUAGE "Russian"
+!insertmacro MUI_LANGUAGE "Portuguese"
+!insertmacro MUI_LANGUAGE "PortugueseBR"
+!insertmacro MUI_LANGUAGE "Polish"
+!insertmacro MUI_LANGUAGE "Ukrainian"
+!insertmacro MUI_LANGUAGE "Czech"
+!insertmacro MUI_LANGUAGE "Slovak"
+!insertmacro MUI_LANGUAGE "Croatian"
+!insertmacro MUI_LANGUAGE "Bulgarian"
+!insertmacro MUI_LANGUAGE "Hungarian"
+!insertmacro MUI_LANGUAGE "Thai"
+!insertmacro MUI_LANGUAGE "Romanian"
+!insertmacro MUI_LANGUAGE "Latvian"
+!insertmacro MUI_LANGUAGE "Macedonian"
+!insertmacro MUI_LANGUAGE "Estonian"
+!insertmacro MUI_LANGUAGE "Turkish"
+!insertmacro MUI_LANGUAGE "Lithuanian"
+!insertmacro MUI_LANGUAGE "Slovenian"
+!insertmacro MUI_LANGUAGE "Serbian"
+!insertmacro MUI_LANGUAGE "SerbianLatin"
+!insertmacro MUI_LANGUAGE "Arabic"
+!insertmacro MUI_LANGUAGE "Farsi"
+!insertmacro MUI_LANGUAGE "Hebrew"
+!insertmacro MUI_LANGUAGE "Indonesian"
+!insertmacro MUI_LANGUAGE "Mongolian"
+!insertmacro MUI_LANGUAGE "Luxembourgish"
+!insertmacro MUI_LANGUAGE "Albanian"
+!insertmacro MUI_LANGUAGE "Breton"
+!insertmacro MUI_LANGUAGE "Belarusian"
+!insertmacro MUI_LANGUAGE "Icelandic"
+!insertmacro MUI_LANGUAGE "Malay"
+!insertmacro MUI_LANGUAGE "Bosnian"
+!insertmacro MUI_LANGUAGE "Kurdish"
+!insertmacro MUI_LANGUAGE "Irish"
+!insertmacro MUI_LANGUAGE "Uzbek"
+!insertmacro MUI_LANGUAGE "Galician"
+!insertmacro MUI_LANGUAGE "Afrikaans"
+!insertmacro MUI_LANGUAGE "Catalan"
+!insertmacro MUI_LANGUAGE "Esperanto"
+!insertmacro MUI_LANGUAGE "Asturian"
+!insertmacro MUI_LANGUAGE "Basque"
+!insertmacro MUI_LANGUAGE "Pashto"
+!insertmacro MUI_LANGUAGE "ScotsGaelic"
+!insertmacro MUI_LANGUAGE "Georgian"
+!insertmacro MUI_LANGUAGE "Vietnamese"
+!insertmacro MUI_LANGUAGE "Welsh"
+!insertmacro MUI_LANGUAGE "Armenian"
+!insertmacro MUI_LANGUAGE "Corsican"
+!insertmacro MUI_LANGUAGE "Tatar"
+!insertmacro MUI_LANGUAGE "Hindi"
+
####################################################################
# Sections
diff --git a/src/cli.rs b/src/cli.rs
index 117486ee4..40ab21188 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -36,7 +36,7 @@ impl Session {
.lc
.write()
.unwrap()
- .initialize(id.to_owned(), ConnType::PORT_FORWARD);
+ .initialize(id.to_owned(), ConnType::PORT_FORWARD, None);
session
}
}
diff --git a/src/client.rs b/src/client.rs
index e0ac68c5d..f36bdae78 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,3 +1,11 @@
+use std::{
+ collections::HashMap,
+ net::SocketAddr,
+ ops::{Deref, Not},
+ str::FromStr,
+ sync::{mpsc, Arc, Mutex, RwLock},
+};
+
pub use async_trait::async_trait;
use bytes::Bytes;
#[cfg(not(any(target_os = "android", target_os = "linux")))]
@@ -7,16 +15,11 @@ use cpal::{
};
use magnum_opus::{Channels::*, Decoder as AudioDecoder};
use sha2::{Digest, Sha256};
-use std::{
- collections::HashMap,
- net::SocketAddr,
- ops::{Deref, Not},
- str::FromStr,
- sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock},
-};
use uuid::Uuid;
pub use file_trait::FileManager;
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use hbb_common::tokio::sync::mpsc::UnboundedSender;
use hbb_common::{
allow_err,
anyhow::{anyhow, Context},
@@ -44,24 +47,35 @@ use scrap::{
VpxDecoderConfig, VpxVideoCodecId,
};
+use crate::{
+ common::{self, is_keyboard_mode_supported},
+ server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED},
+};
+
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use crate::{
+ common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL},
+ ui_session_interface::SessionPermissionConfig,
+};
+
pub use super::lang::*;
pub mod file_trait;
pub mod helper;
pub mod io_loop;
-use crate::{
- common::{self, is_keyboard_mode_supported},
- server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED},
-};
-pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
-pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true);
-pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
+
pub const MILLI1: Duration = Duration::from_millis(1);
pub const SEC30: Duration = Duration::from_secs(30);
/// Client of the remote desktop.
pub struct Client;
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+struct TextClipboardState {
+ is_required: bool,
+ running: bool,
+}
+
#[cfg(not(any(target_os = "android", target_os = "linux")))]
lazy_static::lazy_static! {
static ref AUDIO_HOST: Host = cpal::default_host();
@@ -70,6 +84,8 @@ lazy_static::lazy_static! {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
lazy_static::lazy_static! {
static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new()));
+ static ref OLD_CLIPBOARD_TEXT: Arc> = Default::default();
+ static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new()));
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -85,7 +101,7 @@ pub fn get_key_state(key: enigo::Key) -> bool {
cfg_if::cfg_if! {
if #[cfg(target_os = "android")] {
-use libc::{c_float, c_int, c_void};
+use hbb_common::libc::{c_float, c_int, c_void};
type Oboe = *mut c_void;
extern "C" {
fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe;
@@ -595,6 +611,86 @@ impl Client {
conn.send(&msg_out).await?;
Ok(conn)
}
+
+ #[inline]
+ #[cfg(feature = "flutter")]
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ pub fn set_is_text_clipboard_required(b: bool) {
+ TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b;
+ }
+
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ fn try_stop_clipboard(_self_id: &str) {
+ #[cfg(feature = "flutter")]
+ if crate::flutter::other_sessions_running(_self_id) {
+ return;
+ }
+ TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
+ }
+
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ fn try_start_clipboard(_conf_tx: Option<(SessionPermissionConfig, UnboundedSender)>) {
+ let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap();
+ if clipboard_lock.running {
+ return;
+ }
+
+ match ClipboardContext::new() {
+ Ok(mut ctx) => {
+ clipboard_lock.running = true;
+ // ignore clipboard update before service start
+ check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT));
+ std::thread::spawn(move || {
+ log::info!("Start text clipboard loop");
+ loop {
+ std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
+ if !TEXT_CLIPBOARD_STATE.lock().unwrap().running {
+ break;
+ }
+
+ if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
+ continue;
+ }
+
+ if let Some(msg) = check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)) {
+ #[cfg(feature = "flutter")]
+ crate::flutter::send_text_clipboard_msg(msg);
+ #[cfg(not(feature = "flutter"))]
+ if let Some((cfg, tx)) = &_conf_tx {
+ if cfg.is_text_clipboard_required() {
+ let _ = tx.send(Data::Message(msg));
+ }
+ }
+ }
+ }
+ log::info!("Stop text clipboard loop");
+ });
+ }
+ Err(err) => {
+ log::error!("Failed to start clipboard service of client: {}", err);
+ }
+ }
+ }
+
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ fn get_current_text_clipboard_msg() -> Option {
+ let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap();
+ if txt.is_empty() {
+ None
+ } else {
+ Some(crate::create_clipboard_msg(txt.clone()))
+ }
+ }
+}
+
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+impl TextClipboardState {
+ fn new() -> Self {
+ Self {
+ is_required: true,
+ running: false,
+ }
+ }
}
/// Audio handler for the [`Client`].
@@ -714,6 +810,7 @@ impl AudioHandler {
.check_audio(frame.timestamp)
.not()
{
+ log::debug!("audio frame {} is ignored", frame.timestamp);
return;
}
}
@@ -724,6 +821,7 @@ impl AudioHandler {
}
#[cfg(target_os = "linux")]
if self.simple.is_none() {
+ log::debug!("PulseAudio simple binding does not exists");
return;
}
#[cfg(target_os = "android")]
@@ -911,6 +1009,8 @@ pub struct LoginConfigHandler {
pub direct: Option,
pub received: bool,
switch_uuid: Option,
+ pub success_time: Option,
+ pub direct_error_counter: usize,
}
impl Deref for LoginConfigHandler {
@@ -938,7 +1038,13 @@ impl LoginConfigHandler {
///
/// * `id` - id of peer
/// * `conn_type` - Connection type enum.
- pub fn initialize(&mut self, id: String, conn_type: ConnType, switch_uuid: Option) {
+ pub fn initialize(
+ &mut self,
+ id: String,
+ conn_type: ConnType,
+ switch_uuid: Option,
+ force_relay: bool,
+ ) {
self.id = id;
self.conn_type = conn_type;
let config = self.load_config();
@@ -947,10 +1053,12 @@ impl LoginConfigHandler {
self.session_id = rand::random();
self.supported_encoding = None;
self.restarting_remote_device = false;
- self.force_relay = !self.get_option("force-always-relay").is_empty();
+ self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay;
self.direct = None;
self.received = false;
self.switch_uuid = switch_uuid;
+ self.success_time = None;
+ self.direct_error_counter = 0;
}
/// Check if the client should auto login.
@@ -1133,6 +1241,11 @@ impl LoginConfigHandler {
if !name.contains("block-input") {
self.save_config(config);
}
+ #[cfg(feature = "flutter")]
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ if name == "disable-clipboard" {
+ crate::flutter::update_text_clipboard_required();
+ }
let mut misc = Misc::new();
misc.set_option(option);
let mut msg_out = Message::new();
@@ -1540,10 +1653,9 @@ pub type MediaSender = mpsc::Sender;
/// * `video_callback` - The callback for video frame. Being called when a video frame is ready.
pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender)
where
- F: 'static + FnMut(&[u8]) + Send,
+ F: 'static + FnMut(&mut Vec) + Send,
{
let (video_sender, video_receiver) = mpsc::channel::();
- let (audio_sender, audio_receiver) = mpsc::channel::();
let mut video_callback = video_callback;
let latency_controller = LatencyController::new();
@@ -1556,7 +1668,7 @@ where
match data {
MediaData::VideoFrame(vf) => {
if let Ok(true) = video_handler.handle_frame(vf) {
- video_callback(&video_handler.rgb);
+ video_callback(&mut video_handler.rgb);
}
}
MediaData::Reset => {
@@ -1573,8 +1685,19 @@ where
}
log::info!("Video decoder loop exits");
});
+ let audio_sender = start_audio_thread(Some(latency_controller_cl));
+ return (video_sender, audio_sender);
+}
+
+/// Start an audio thread
+/// Return a audio [`MediaSender`]
+pub fn start_audio_thread(
+ latency_controller: Option>>,
+) -> MediaSender {
+ let latency_controller = latency_controller.unwrap_or(LatencyController::new());
+ let (audio_sender, audio_receiver) = mpsc::channel::();
std::thread::spawn(move || {
- let mut audio_handler = AudioHandler::new(latency_controller_cl);
+ let mut audio_handler = AudioHandler::new(latency_controller);
loop {
if let Ok(data) = audio_receiver.recv() {
match data {
@@ -1582,6 +1705,7 @@ where
audio_handler.handle_frame(af);
}
MediaData::AudioFormat(f) => {
+ log::debug!("recved audio format, sample rate={}", f.sample_rate);
audio_handler.handle_format(f);
}
_ => {}
@@ -1592,7 +1716,7 @@ where
}
log::info!("Audio decoder loop exits");
});
- return (video_sender, audio_sender);
+ audio_sender
}
/// Handle latency test.
@@ -1934,6 +2058,8 @@ pub enum Data {
RecordScreen(bool, i32, i32, String),
ElevateDirect,
ElevateWithLogon(String, String),
+ NewVoiceCall,
+ CloseVoiceCall,
}
/// Keycode for key events.
@@ -2086,9 +2212,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
&& !text.to_lowercase().contains("resolve")
&& !text.to_lowercase().contains("mismatch")
&& !text.to_lowercase().contains("manually")
- && !text.to_lowercase().contains("not allowed")
- && !text.to_lowercase().contains("as expected")
- && !text.to_lowercase().contains("reset by the peer")))
+ && !text.to_lowercase().contains("not allowed")))
}
#[inline]
diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs
index 2ecfca837..49e3f2358 100644
--- a/src/client/file_trait.rs
+++ b/src/client/file_trait.rs
@@ -7,7 +7,7 @@ pub trait FileManager: Interface {
fs::get_home_as_string()
}
- #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
+ #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))]
fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value {
match fs::read_dir(&fs::get_path(&path), include_hidden) {
Err(_) => sciter::Value::null(),
@@ -20,7 +20,7 @@ pub trait FileManager: Interface {
}
}
- #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
+ #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))]
fn read_dir(&self, path: &str, include_hidden: bool) -> String {
use crate::common::make_fd_to_json;
match fs::read_dir(&fs::get_path(path), include_hidden) {
diff --git a/src/client/helper.rs b/src/client/helper.rs
index e4736c0e8..20acd811a 100644
--- a/src/client/helper.rs
+++ b/src/client/helper.rs
@@ -5,7 +5,7 @@ use std::{
use hbb_common::{
log,
- message_proto::{video_frame, VideoFrame},
+ message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time,
};
const MAX_LATENCY: i64 = 500;
@@ -18,6 +18,7 @@ pub struct LatencyController {
last_video_remote_ts: i64, // generated on remote device
update_time: Instant,
allow_audio: bool,
+ audio_only: bool
}
impl Default for LatencyController {
@@ -26,6 +27,7 @@ impl Default for LatencyController {
last_video_remote_ts: Default::default(),
update_time: Instant::now(),
allow_audio: Default::default(),
+ audio_only: false
}
}
}
@@ -36,6 +38,11 @@ impl LatencyController {
Arc::new(Mutex::new(LatencyController::default()))
}
+ /// Set whether this [LatencyController] should be working in audio only mode.
+ pub fn set_audio_only(&mut self, only: bool) {
+ self.audio_only = only;
+ }
+
/// Update the latency controller with the latest video timestamp.
pub fn update_video(&mut self, timestamp: i64) {
self.last_video_remote_ts = timestamp;
@@ -46,7 +53,11 @@ impl LatencyController {
pub fn check_audio(&mut self, timestamp: i64) -> bool {
// Compute audio latency.
let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts;
- let latency = expected - timestamp;
+ let latency = if self.audio_only {
+ expected
+ } else {
+ expected - timestamp
+ };
// Set MAX and MIN, avoid fixing too frequently.
if self.allow_audio {
if latency.abs() > MAX_LATENCY {
@@ -59,6 +70,9 @@ impl LatencyController {
self.allow_audio = true;
}
}
+ // No video frame here, which means the update time is not up to date.
+ // We manually update the time here.
+ self.update_time = Instant::now();
self.allow_audio
}
}
@@ -101,3 +115,24 @@ pub struct QualityStatus {
pub target_bitrate: Option,
pub codec_format: Option,
}
+
+#[inline]
+pub fn new_voice_call_request(is_connect: bool) -> Message {
+ let mut req = VoiceCallRequest::new();
+ req.is_connect = is_connect;
+ req.req_timestamp = get_time();
+ let mut msg = Message::new();
+ msg.set_voice_call_request(req);
+ msg
+}
+
+#[inline]
+pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message {
+ let mut resp = VoiceCallResponse::new();
+ resp.accepted = accepted;
+ resp.req_timestamp = request_timestamp;
+ resp.ack_timestamp = get_time();
+ let mut msg = Message::new();
+ msg.set_voice_call_response(resp);
+ msg
+}
\ No newline at end of file
diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs
index 0178fe9e8..b51c481a5 100644
--- a/src/client/io_loop.rs
+++ b/src/client/io_loop.rs
@@ -1,17 +1,10 @@
-use crate::client::{
- Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
- SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED,
-};
-use crate::common;
-#[cfg(not(any(target_os = "android", target_os = "ios")))]
-use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
+use std::collections::HashMap;
+use std::num::NonZeroI64;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::{Arc, Mutex};
#[cfg(windows)]
use clipboard::{cliprdr::CliprdrClientContext, ContextSend};
-
-use crate::ui_session_interface::{InvokeUiSession, Session};
-use crate::{client::Data, client::Interface};
-
use hbb_common::config::{PeerConfig, TransferSerde};
use hbb_common::fs::{
can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult,
@@ -20,6 +13,7 @@ use hbb_common::fs::{
use hbb_common::message_proto::permission_info::Permission;
use hbb_common::protobuf::Message as _;
use hbb_common::rendezvous_proto::ConnType;
+use hbb_common::tokio::sync::mpsc::error::TryRecvError;
#[cfg(windows)]
use hbb_common::tokio::sync::Mutex as TokioMutex;
use hbb_common::tokio::{
@@ -27,12 +21,19 @@ use hbb_common::tokio::{
sync::mpsc,
time::{self, Duration, Instant, Interval},
};
-use hbb_common::{allow_err, message_proto::*, sleep};
+use hbb_common::{allow_err, get_time, message_proto::*, sleep};
use hbb_common::{fs, log, Stream};
-use std::collections::HashMap;
-use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::{Arc, Mutex};
+use crate::client::{
+ new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1,
+ SEC30,
+};
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use crate::common::update_clipboard;
+use crate::common::{get_default_sound_input, set_sound_input};
+use crate::ui_session_interface::{InvokeUiSession, Session};
+use crate::{audio_service, common, ConnInner, CLIENT_SERVER};
+use crate::{client::Data, client::Interface};
pub struct Remote {
handler: Session,
@@ -40,6 +41,9 @@ pub struct Remote {
audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver,
sender: mpsc::UnboundedSender,
+ // Stop sending local audio to remote client.
+ stop_voice_call_sender: Option>,
+ voice_call_request_timestamp: Option,
old_clipboard: Arc>,
read_jobs: Vec,
write_jobs: Vec,
@@ -81,11 +85,12 @@ impl Remote {
data_count: Arc::new(AtomicUsize::new(0)),
frame_count,
video_format: CodecFormat::Unknown,
+ stop_voice_call_sender: None,
+ voice_call_request_timestamp: None,
}
}
pub async fn io_loop(&mut self, key: &str, token: &str) {
- let stop_clipboard = self.start_clipboard();
let mut last_recv_time = Instant::now();
let mut received = false;
let conn_type = if self.handler.is_file_transfer() {
@@ -93,6 +98,7 @@ impl Remote