diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 5ba29c8b6..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-name: Bug Report
-about: Report a bug (English only, Please).
-title: ""
-labels: bug
-assignees: ''
-
----
-
-
-
-**Describe the bug you encountered:**
-
-...
-
-**What did you expect to happen instead?**
-
-...
-
-
-**How did you install `RustDesk`?**
-
-
-
----
-
-**RustDesk version and environment**
-
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 000000000..c2d92097c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,60 @@
+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:
+ label: Bug Description
+ description: A clear and concise description of what the bug is
+ validations:
+ required: true
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: How to Reproduce
+ description: What steps can we take to reproduce this behavior?
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ 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: 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: |
+ 1.1.9 -> 1.1.8
+ validations:
+ required: true
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots
+ description: If applicable, please add screenshots to help explain your problem
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context
+ description: Add any additonal context about the problem here
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 01de3b330..7b43e397b 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,4 +1,3 @@
-blank_issues_enabled: true
contact_links:
- name: Ask a question
url: https://github.com/rustdesk/rustdesk/discussions/category_choices
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 0d21f017d..000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Feature Request
-about: Suggest an idea for this project ((English only, Please).
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 000000000..50cd6d0cf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,33 @@
+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:
+ label: Description
+ description: Describe your suggested feature and the main use cases
+ validations:
+ required: true
+
+ - type: textarea
+ id: users
+ attributes:
+ label: Impact
+ description: What types of users can benefit from using the suggested feature?
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context
+ description: Add any additonal context about the feature here
diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml
new file mode 100644
index 000000000..a1ff080c5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/task.yaml
@@ -0,0 +1,20 @@
+name: 📝 Task
+description: Create a task for the team to work on
+title: "[Task]: "
+labels: [Task]
+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
+ attributes:
+ label: SubTasks
+ placeholder: |
+ - Sub Task 1
+ - Sub Task 2
+ validations:
+ required: false
diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml
index 83ad7629e..f03cd0be8 100644
--- a/.github/workflows/flutter-nightly.yml
+++ b/.github/workflows/flutter-nightly.yml
@@ -59,10 +59,9 @@ jobs:
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
- toolchain: "1.62"
+ toolchain: stable
target: ${{ matrix.job.target }}
override: true
- components: rustfmt
profile: minimal # minimal component installation (ie, no documentation)
- uses: Swatinem/rust-cache@v2
@@ -243,7 +242,6 @@ 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
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
diff --git a/Cargo.lock b/Cargo.lock
index 5c4af56e9..83f623ca7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1137,7 +1137,7 @@ checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a"
dependencies = [
"dconf_rs",
"detect-desktop-environment",
- "dirs",
+ "dirs 4.0.0",
"objc",
"rust-ini",
"web-sys",
@@ -1334,8 +1334,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",
@@ -1401,6 +1402,16 @@ dependencies = [
"dirs-sys-next",
]
+[[package]]
+name = "dirs"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
+dependencies = [
+ "cfg-if 0.1.10",
+ "dirs-sys",
+]
+
[[package]]
name = "dirs"
version = "4.0.0"
@@ -1873,6 +1884,19 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fruitbasket"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351"
+dependencies = [
+ "dirs 2.0.2",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "time 0.1.45",
+]
+
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@@ -3657,6 +3681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
+ "objc_exception",
]
[[package]]
@@ -3670,6 +3695,15 @@ dependencies = [
"objc_id",
]
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "objc_id"
version = "0.1.1"
@@ -4371,7 +4405,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
-source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358"
+source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130"
dependencies = [
"cocoa",
"core-foundation 0.9.3",
@@ -4655,6 +4689,7 @@ dependencies = [
"flexi_logger",
"flutter_rust_bridge",
"flutter_rust_bridge_codegen",
+ "fruitbasket",
"glib 0.16.5",
"gtk",
"hbb_common",
@@ -4673,6 +4708,7 @@ dependencies = [
"mouce",
"num_cpus",
"objc",
+ "objc_id",
"parity-tokio-ipc",
"rdev",
"repng",
@@ -4713,7 +4749,7 @@ name = "rustdesk-portable-packer"
version = "0.1.0"
dependencies = [
"brotli",
- "dirs",
+ "dirs 4.0.0",
"embed-resource",
"md5",
]
@@ -6591,7 +6627,7 @@ dependencies = [
"async-trait",
"byteorder",
"derivative",
- "dirs",
+ "dirs 4.0.0",
"enumflags2",
"event-listener",
"futures-core",
diff --git a/Cargo.toml b/Cargo.toml
index 1e9af30e5..b315024e9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,7 +59,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"
@@ -106,6 +106,8 @@ core-graphics = "0.22"
include_dir = "0.7.2"
tray-item = "0.7" # looks better than trayicon
dark-light = "0.2"
+fruitbasket = "0.10.0"
+objc_id = "0.1.1"
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.25" }
diff --git a/README.md b/README.md
index bc9bacf19..866063726 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/
## Free Public Servers
-Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
+Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
diff --git a/build.py b/build.py
index 6b107ff4b..dce434720 100755
--- a/build.py
+++ b/build.py
@@ -322,8 +322,9 @@ def build_flutter_dmg(version, features):
os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h')
os.chdir('flutter')
os.system('flutter build macos --release')
+ os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk')
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-FA.md b/docs/README-FA.md
index 02b156dbb..496e81849 100644
--- a/docs/README-FA.md
+++ b/docs/README-FA.md
@@ -1,6 +1,6 @@

- تصاویر محیط نرمافزار •
+ تصاویر محیط نرمافزار •
ساختار •
داکر •
ساخت •
@@ -9,12 +9,12 @@
[English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.
-با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
+با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
[](https://ko-fi.com/I2I04VU09)
-راستدسک (RustDesk) نرمافزاری برای گارکردن با رایانهی رومیزی از راه دور است و با زبان برنامهنویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آنها کنترل کامل داشته باشید.
+راستدسک (RustDesk) نرمافزاری برای کارکردن با رایانهی رومیزی از راه دور است و با زبان برنامهنویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آنها کنترل کامل داشته باشید.
میتوانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راهاندازی کنید](https://rustdesk.com/server) یا
[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk).
@@ -130,7 +130,7 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
-سپس، هر بار که نیاز به ساخت ترمافزار داشتید، دستور زیر را اجرا کنید:
+سپس، هر بار که نیاز به ساخت نرمافزار داشتید، دستور زیر را اجرا کنید:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index f1114f913..c595885f2 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,13 +1,9 @@
# Security Policy
-## Supported Versions
-
-| Version | Supported |
-| --------- | ------------------ |
-| 1.1.x | :white_check_mark: |
-| 1.x | :white_check_mark: |
-| Below 1.0 | :x: |
-
## Reporting a Vulnerability
-Here we should write what to do in case of a security vulnerability
+We value security for the project very highly. We encourage all users to report any vulnerabilities they discover to us.
+If you find a security vulnerability in the RustDesk project, please report it responsibly by sending an email to info@rustdesk.com.
+
+At this juncture, we don't have a bug bounty program. We are a small team trying to solve a big problem. We urge you to report any vulnerabilities responsibly
+so that we can continue building a secure application for the entire community.
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 d5d2c49c8..eac2fe724 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-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index e30cc5019..8c01e98de 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-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 41ccba607..d32c8f8e8 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-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index c10349d71..a2f07afb4 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-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 52fde7830..e8c754f4a 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/assets/chat.svg b/flutter/assets/chat.svg
new file mode 100644
index 000000000..03491be6e
--- /dev/null
+++ b/flutter/assets/chat.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..e1b962124
--- /dev/null
+++ b/flutter/assets/record_screen.svg
@@ -0,0 +1,24 @@
+
+
+
+
\ 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..5654befc7
--- /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..fd8334f92
--- /dev/null
+++ b/flutter/assets/voice_call_waiting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index c35862a8c..16cef3177 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 900bd13fa..298f4d9af 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 5fc34ce9a..fd3b01b6d 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index ab315a4c6..18ebaab69 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 6d69c01e1..a8ee14a31 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index b6c8034cd..a83f88b05 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index cf6c7c775..331e72531 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 5fc34ce9a..fd3b01b6d 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 6928a4e6d..aee7e4321 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index a13129e15..2d0da17b1 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index a13129e15..2d0da17b1 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index 319e70f91..7ee56922e 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 229bdf563..76abd423b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index caffb26a3..e08138333 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 751104548..46de51af6 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 04e29eaa0..a731f0b08 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -3,14 +3,11 @@ import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
-import 'dart:typed_data';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
-import 'package:flutter_hbb/utils/platform_channel.dart';
-import 'package:win32/win32.dart' as win32;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -19,14 +16,17 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
+import 'package:flutter_hbb/utils/platform_channel.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
-import 'package:window_manager/window_manager.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:window_size/window_size.dart' as window_size;
import 'package:url_launcher/url_launcher.dart';
+import 'package:win32/win32.dart' as win32;
+import 'package:window_manager/window_manager.dart';
+import 'package:window_size/window_size.dart' as window_size;
+import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
@@ -34,8 +34,6 @@ import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
-import '../consts.dart';
-
final globalKey = GlobalKey();
final navigationBarKey = GlobalKey();
@@ -702,7 +700,6 @@ void msgBox(String id, String type, String title, String text, String link,
buttons.insert(
0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
}
- // TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
@@ -716,8 +713,7 @@ void msgBox(String id, String type, String title, String text, String link,
dialogManager.show(
(setState, close) => CustomAlertDialog(
title: null,
- content: SelectionArea(
- child: msgboxContent(type, title, text).paddingOnly(bottom: 10)),
+ content: SelectionArea(child: msgboxContent(type, title, text)),
actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
@@ -782,7 +778,7 @@ Widget msgboxContent(String type, String title, String text) {
),
),
],
- );
+ ).marginOnly(bottom: 12);
}
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
@@ -1280,10 +1276,12 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async {
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
-Future initUniLinks() async {
- if (!Platform.isWindows && !Platform.isMacOS) {
- return;
+Future initUniLinks() async {
+ if (Platform.isLinux) {
+ return false;
}
+ // Register uni links for Windows. The required info of url scheme is already
+ // declared in `Info.plist` for macOS.
if (Platform.isWindows) {
registerProtocol('rustdesk');
}
@@ -1291,22 +1289,33 @@ Future initUniLinks() async {
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
- return;
+ return false;
}
- parseRustdeskUri(initialLink);
+ return parseRustdeskUri(initialLink);
} catch (err) {
debugPrintStack(label: "$err");
+ return false;
}
}
-StreamSubscription? listenUniLinks() {
- if (!(Platform.isWindows || Platform.isMacOS)) {
+/// Listen for uni links.
+///
+/// * handleByFlutter: Should uni links be handled by Flutter.
+///
+/// Returns a [StreamSubscription] which can listen the uni links.
+StreamSubscription? listenUniLinks({handleByFlutter = true}) {
+ if (Platform.isLinux) {
return null;
}
final sub = uriLinkStream.listen((Uri? uri) {
+ debugPrint("A uri was received: $uri.");
if (uri != null) {
- callUniLinksUriHandler(uri);
+ if (handleByFlutter) {
+ callUniLinksUriHandler(uri);
+ } else {
+ bind.sendUrlScheme(url: uri.toString());
+ }
} else {
print("uni listen error: uri is empty.");
}
@@ -1316,11 +1325,19 @@ StreamSubscription? listenUniLinks() {
return sub;
}
-/// Returns true if we successfully handle the startup arguments.
+/// Handle command line arguments
+///
+/// * Returns true if we successfully handle the startup arguments.
bool checkArguments() {
+ if (kBootArgs.isNotEmpty) {
+ final ret = parseRustdeskUri(kBootArgs.first);
+ if (ret) {
+ return true;
+ }
+ }
// bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
// check connect args
- final connectIndex = kBootArgs.indexOf("--connect");
+ var connectIndex = kBootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
}
@@ -1355,7 +1372,7 @@ bool checkArguments() {
bool parseRustdeskUri(String uriPath) {
final uri = Uri.tryParse(uriPath);
if (uri == null) {
- print("uri is not valid: $uriPath");
+ debugPrint("uri is not valid: $uriPath");
return false;
}
return callUniLinksUriHandler(uri);
@@ -1374,7 +1391,7 @@ bool callUniLinksUriHandler(Uri uri) {
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid);
});
- return false;
+ return true;
}
return false;
}
@@ -1514,8 +1531,12 @@ Future onActiveWindowChanged() async {
} catch (err) {
debugPrintStack(label: "$err");
} finally {
+ debugPrint("Start closing RustDesk...");
await windowManager.setPreventClose(false);
await windowManager.close();
+ if (Platform.isMacOS) {
+ RdPlatformChannel.instance.terminate();
+ }
}
}
}
@@ -1708,3 +1729,30 @@ 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");
+}
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index 278f5861c..4080f9c11 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -1,6 +1,7 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
@@ -11,106 +12,15 @@ import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
+import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
+import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
+import 'package:provider/provider.dart';
+import 'package:visibility_detector/visibility_detector.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
-const int groupTabIndex = 4;
-const String defaultGroupTabname = 'Group';
-
-class StatePeerTab {
- final RxInt currentTab = 0.obs;
- final RxInt tabHiddenFlag = 0.obs;
- final RxList tabNames = [
- 'Recent Sessions',
- 'Favorites',
- 'Discovered',
- 'Address Book',
- defaultGroupTabname,
- ].obs;
-
- StatePeerTab._() {
- tabHiddenFlag.value = (int.tryParse(
- bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
- radix: 2) ??
- 0);
- var tabs = _notHiddenTabs();
- currentTab.value =
- int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
- if (!tabs.contains(currentTab.value)) {
- currentTab.value = 0;
- }
- }
- static final StatePeerTab instance = StatePeerTab._();
-
- check() {
- var tabs = _notHiddenTabs();
- if (filterGroupCard()) {
- if (currentTab.value == groupTabIndex) {
- currentTab.value =
- tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
- bind.setLocalFlutterConfig(
- k: 'peer-tab-index', v: currentTab.value.toString());
- }
- } else {
- if (gFFI.userModel.isAdmin.isFalse &&
- gFFI.userModel.groupName.isNotEmpty) {
- tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
- } else {
- tabNames[groupTabIndex] = defaultGroupTabname;
- }
- if (tabs.contains(groupTabIndex) &&
- int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
- groupTabIndex) {
- currentTab.value = groupTabIndex;
- }
- }
- }
-
- List currentTabs() {
- var v = List.empty(growable: true);
- for (int i = 0; i < tabNames.length; i++) {
- if (!_isTabHidden(i) && !_isTabFilter(i)) {
- v.add(i);
- }
- }
- return v;
- }
-
- bool filterGroupCard() {
- if (gFFI.groupModel.users.isEmpty ||
- (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
- return true;
- } else {
- return false;
- }
- }
-
- bool _isTabHidden(int tabindex) {
- return tabHiddenFlag & (1 << tabindex) != 0;
- }
-
- bool _isTabFilter(int tabIndex) {
- if (tabIndex == groupTabIndex) {
- return filterGroupCard();
- }
- return false;
- }
-
- List _notHiddenTabs() {
- var v = List.empty(growable: true);
- for (int i = 0; i < tabNames.length; i++) {
- if (!_isTabHidden(i)) {
- v.add(i);
- }
- }
- return v;
- }
-}
-
-final statePeerTab = StatePeerTab.instance;
-
class PeerTabPage extends StatefulWidget {
const PeerTabPage({Key? key}) : super(key: key);
@override
@@ -156,11 +66,10 @@ class _PeerTabPageState extends State
),
() => {}),
];
+ final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
@override
void initState() {
- adjustTab();
-
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
@@ -172,16 +81,11 @@ class _PeerTabPageState extends State
Future handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
- statePeerTab.currentTab.value = tabIndex;
+ gFFI.peerTabModel.setCurrentTab(tabIndex);
entries[tabIndex].load();
}
}
- @override
- void dispose() {
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
return Column(
@@ -199,6 +103,7 @@ class _PeerTabPageState extends State
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
+ buildScrollJumper(),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
@@ -213,82 +118,115 @@ class _PeerTabPageState extends State
}
Widget _createSwitchBar(BuildContext context) {
- final textColor = Theme.of(context).textTheme.titleLarge?.color;
- return Obx(() {
- var tabs = statePeerTab.currentTabs();
- return ListView(
- scrollDirection: Axis.horizontal,
- physics: NeverScrollableScrollPhysics(),
- controller: ScrollController(),
- children: tabs.map((t) {
- return InkWell(
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- decoration: BoxDecoration(
- color: statePeerTab.currentTab.value == t
- ? Theme.of(context).backgroundColor
- : null,
- borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
- ),
- child: Align(
- alignment: Alignment.center,
- child: Text(
- translatedTabname(t),
- textAlign: TextAlign.center,
- style: TextStyle(
- height: 1,
- fontSize: 14,
- color: statePeerTab.currentTab.value == t
- ? textColor
- : textColor
- ?..withOpacity(0.5)),
- ),
- )),
- onTap: () async {
- await handleTabSelection(t);
- await bind.setLocalFlutterConfig(
- k: 'peer-tab-index', v: t.toString());
+ final model = Provider.of(context);
+ int indexCounter = -1;
+ return ReorderableListView(
+ buildDefaultDragHandles: false,
+ onReorder: (oldIndex, newIndex) {
+ model.onReorder(oldIndex, newIndex);
+ },
+ scrollDirection: Axis.horizontal,
+ physics: NeverScrollableScrollPhysics(),
+ scrollController: model.sc,
+ children: model.visibleOrderedTabs.map((t) {
+ indexCounter++;
+ return ReorderableDragStartListener(
+ key: ValueKey(t),
+ index: indexCounter,
+ child: VisibilityDetector(
+ key: ValueKey(t),
+ onVisibilityChanged: (info) {
+ final id = (info.key as ValueKey).value;
+ model.setTabFullyVisible(id, info.visibleFraction > 0.99);
},
- );
- }).toList());
- });
+ child: Listener(
+ // handle mouse wheel
+ onPointerSignal: (e) {
+ if (e is PointerScrollEvent) {
+ if (!model.sc.canScroll) return;
+ _scrollDebounce.call(() {
+ model.sc.animateTo(model.sc.offset + e.scrollDelta.dy,
+ duration: Duration(milliseconds: 200),
+ curve: Curves.ease);
+ });
+ }
+ },
+ child: InkWell(
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ decoration: BoxDecoration(
+ color: model.currentTab == t
+ ? Theme.of(context).backgroundColor
+ : null,
+ borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
+ ),
+ child: Align(
+ alignment: Alignment.center,
+ child: Text(
+ model.translatedTabname(t),
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ height: 1,
+ fontSize: 14,
+ color: model.currentTab == t
+ ? MyTheme.tabbar(context).selectedTextColor
+ : MyTheme.tabbar(context).unSelectedTextColor
+ ?..withOpacity(0.5)),
+ ),
+ )),
+ onTap: () async {
+ await handleTabSelection(t);
+ await bind.setLocalFlutterConfig(
+ k: 'peer-tab-index', v: t.toString());
+ },
+ ),
+ ),
+ ),
+ );
+ }).toList());
}
- translatedTabname(int index) {
- if (index < statePeerTab.tabNames.length) {
- final name = statePeerTab.tabNames[index];
- if (index == groupTabIndex) {
- if (name == defaultGroupTabname) {
- return translate(name);
- } else {
- return name;
- }
- } else {
- return translate(name);
- }
- }
- assert(false);
- return index.toString();
+ Widget buildScrollJumper() {
+ final model = Provider.of(context);
+ return Offstage(
+ offstage: !model.showScrollBtn,
+ child: Row(
+ children: [
+ GestureDetector(
+ child: Icon(Icons.arrow_left,
+ size: 22,
+ color: model.leftFullyVisible
+ ? Theme.of(context).disabledColor
+ : null),
+ onTap: model.sc.backward),
+ GestureDetector(
+ child: Icon(Icons.arrow_right,
+ size: 22,
+ color: model.rightFullyVisible
+ ? Theme.of(context).disabledColor
+ : null),
+ onTap: model.sc.forward)
+ ],
+ ));
}
Widget _createPeersView() {
- final verticalMargin = isDesktop ? 12.0 : 6.0;
- return Expanded(
- child: Obx(() {
- var tabs = statePeerTab.currentTabs();
- if (tabs.isEmpty) {
- return visibleContextMenuListener(Center(
- child: Text(translate('Right click to select tabs')),
- ));
+ final model = Provider.of(context);
+ Widget child;
+ if (model.visibleOrderedTabs.isEmpty) {
+ child = visibleContextMenuListener(Center(
+ child: Text(translate('Right click to select tabs')),
+ ));
+ } else {
+ if (model.visibleOrderedTabs.contains(model.currentTab)) {
+ child = entries[model.currentTab].widget;
} else {
- if (tabs.contains(statePeerTab.currentTab.value)) {
- return entries[statePeerTab.currentTab.value].widget;
- } else {
- statePeerTab.currentTab.value = tabs[0];
- return entries[statePeerTab.currentTab.value].widget;
- }
+ model.setCurrentTab(model.visibleOrderedTabs[0]);
+ child = entries[0].widget;
}
- }).marginSymmetric(vertical: verticalMargin));
+ }
+ return Expanded(
+ child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
@@ -321,13 +259,6 @@ class _PeerTabPageState extends State
);
}
- adjustTab() {
- var tabs = statePeerTab.currentTabs();
- if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
- statePeerTab.currentTab.value = tabs[0];
- }
- }
-
Widget visibleContextMenuListener(Widget child) {
return Listener(
onPointerDown: (e) {
@@ -347,44 +278,36 @@ class _PeerTabPageState extends State
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
- return Obx(() {
- final List menu = List.empty(growable: true);
- for (int i = 0; i < statePeerTab.tabNames.length; i++) {
- if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
- continue;
- }
- int bitMask = 1 << i;
- menu.add(MenuEntrySwitch(
- switchType: SwitchType.scheckbox,
- text: translatedTabname(i),
- getter: () async {
- return statePeerTab.tabHiddenFlag & bitMask == 0;
- },
- setter: (show) async {
- if (show) {
- statePeerTab.tabHiddenFlag.value &= ~bitMask;
- } else {
- statePeerTab.tabHiddenFlag.value |= bitMask;
- }
- await bind.setLocalFlutterConfig(
- k: 'hidden-peer-card',
- v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
- cancelFunc();
- adjustTab();
- }));
- }
- return mod_menu.PopupMenu(
- items: menu
- .map((entry) => entry.build(
- context,
- const MenuConfig(
- commonColor: MyTheme.accent,
- height: 20.0,
- dividerHeight: 12.0,
- )))
- .expand((i) => i)
- .toList());
- });
+ final model = Provider.of(context);
+ final List menu = List.empty(growable: true);
+ final List menuIndex = List.empty(growable: true);
+ var list = model.orderedNotFilteredTabs();
+ for (int i = 0; i < list.length; i++) {
+ int tabIndex = list[i];
+ int bitMask = 1 << tabIndex;
+ menuIndex.add(tabIndex);
+ menu.add(MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: model.translatedTabname(tabIndex),
+ getter: () async {
+ return model.tabHiddenFlag & bitMask == 0;
+ },
+ setter: (show) async {
+ model.onHideShow(tabIndex, show);
+ cancelFunc();
+ }));
+ }
+ return mod_menu.PopupMenu(
+ items: menu
+ .map((entry) => entry.build(
+ context,
+ const MenuConfig(
+ commonColor: MyTheme.accent,
+ height: 20.0,
+ dividerHeight: 12.0,
+ )))
+ .expand((i) => i)
+ .toList());
}
}
@@ -421,7 +344,9 @@ class _PeerSearchBarState extends State {
FocusNode focusNode = FocusNode();
focusNode.addListener(() {
focused.value = focusNode.hasFocus;
- peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length);
+ peerSearchTextController.selection = TextSelection(
+ baseOffset: 0,
+ extentOffset: peerSearchTextController.value.text.length);
});
return Container(
width: 120,
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index e4081d9a5..26e25a209 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -1,17 +1,19 @@
-import 'package:flutter/material.dart';
import 'dart:io';
+import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
+const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
-/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
+/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main";
+const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
@@ -24,7 +26,6 @@ const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kUniLinksPrefix = "rustdesk://";
-const String kActionNewConnection = "connection/new/";
const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings";
@@ -105,6 +106,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/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index 0501c298a..2986adc7a 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
@@ -79,7 +80,16 @@ class _DesktopHomePageState extends State
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
- buildHelpCards(),
+ FutureBuilder(
+ future: buildHelpCards(),
+ builder: (_, data) {
+ if (data.hasData) {
+ return data.data!;
+ } else {
+ return const Offstage();
+ }
+ },
+ ),
],
),
),
@@ -302,7 +312,7 @@ class _DesktopHomePageState extends State
);
}
- Widget buildHelpCards() {
+ Future buildHelpCards() async {
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@@ -349,6 +359,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 +500,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);
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index c444d1f5f..a7289335f 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
@@ -376,10 +375,10 @@ class _RemotePageState extends State
class ImagePaint extends StatefulWidget {
final String id;
- final Rx zoomCursor;
- final Rx cursorOverImage;
- final Rx keyboardEnabled;
- final Rx remoteCursorMoved;
+ final RxBool zoomCursor;
+ final RxBool cursorOverImage;
+ final RxBool keyboardEnabled;
+ final RxBool remoteCursorMoved;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
@@ -402,10 +401,10 @@ class _ImagePaintState extends State {
final ScrollController _vertical = ScrollController();
String get id => widget.id;
- Rx get zoomCursor => widget.zoomCursor;
- Rx get cursorOverImage => widget.cursorOverImage;
- Rx get keyboardEnabled => widget.keyboardEnabled;
- Rx get remoteCursorMoved => widget.remoteCursorMoved;
+ RxBool get zoomCursor => widget.zoomCursor;
+ RxBool get cursorOverImage => widget.cursorOverImage;
+ RxBool get keyboardEnabled => widget.keyboardEnabled;
+ RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
@@ -414,27 +413,50 @@ class _ImagePaintState extends State {
var c = Provider.of(context);
final s = c.scale;
- mouseRegion({child}) => Obx(() => MouseRegion(
- cursor: cursorOverImage.isTrue
- ? c.cursorEmbedded
- ? SystemMouseCursors.none
- : keyboardEnabled.isTrue
- ? (() {
- if (remoteCursorMoved.isTrue) {
- _lastRemoteCursorMoved = true;
- return SystemMouseCursors.none;
- } else {
- if (_lastRemoteCursorMoved) {
- _lastRemoteCursorMoved = false;
- _firstEnterImage.value = true;
- }
- return _buildCustomCursor(context, s);
- }
- }())
- : _buildDisabledCursor(context, s)
- : MouseCursor.defer,
- onHover: (evt) {},
- child: child));
+ mouseRegion({child}) => Obx(() {
+ double getCursorScale() {
+ var c = Provider.of(context);
+ var cursorScale = 1.0;
+ if (Platform.isWindows) {
+ // debug win10
+ final isViewAdaptive =
+ c.viewStyle.style == kRemoteViewStyleAdaptive;
+ if (zoomCursor.value && isViewAdaptive) {
+ cursorScale = s * c.devicePixelRatio;
+ }
+ } else {
+ final isViewOriginal =
+ c.viewStyle.style == kRemoteViewStyleOriginal;
+ if (zoomCursor.value || isViewOriginal) {
+ cursorScale = s;
+ }
+ }
+ return cursorScale;
+ }
+
+ return MouseRegion(
+ cursor: cursorOverImage.isTrue
+ ? c.cursorEmbedded
+ ? SystemMouseCursors.none
+ : keyboardEnabled.isTrue
+ ? (() {
+ if (remoteCursorMoved.isTrue) {
+ _lastRemoteCursorMoved = true;
+ return SystemMouseCursors.none;
+ } else {
+ if (_lastRemoteCursorMoved) {
+ _lastRemoteCursorMoved = false;
+ _firstEnterImage.value = true;
+ }
+ return _buildCustomCursor(
+ context, getCursorScale());
+ }
+ }())
+ : _buildDisabledCursor(context, getCursorScale())
+ : MouseCursor.defer,
+ onHover: (evt) {},
+ child: child);
+ });
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
@@ -480,7 +502,7 @@ class _ImagePaintState extends State {
if (cache == null) {
return MouseCursor.defer;
} else {
- final key = cache.updateGetKey(scale, zoomCursor.value);
+ final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
// [Safety]
@@ -646,7 +668,8 @@ class CursorPaint extends StatelessWidget {
double x = (m.x - hotx) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cy;
double scale = 1.0;
- if (zoomCursor.isTrue) {
+ final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal;
+ if (zoomCursor.value || isViewOriginal) {
x = m.x - hotx + cx / c.scale;
y = m.y - hoty + cy / c.scale;
scale = c.scale;
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 55124fbcc..9b00b481f 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -243,96 +243,35 @@ class _ConnectionTabPageState extends State {
padding: padding,
),
MenuEntryDivider(),
- MenuEntryRadios(
- text: translate('Ratio'),
- optionsGetter: () => [
- MenuEntryRadioOption(
- text: translate('Scale original'),
- value: kRemoteViewStyleOriginal,
- dismissOnClicked: true,
- ),
- MenuEntryRadioOption(
- text: translate('Scale adaptive'),
- value: kRemoteViewStyleAdaptive,
- dismissOnClicked: true,
- ),
- ],
- curOptionGetter: () async =>
- // null means peer id is not found, which there's no need to care about
- await bind.sessionGetViewStyle(id: key) ?? '',
- optionSetter: (String oldValue, String newValue) async {
- await bind.sessionSetViewStyle(id: key, value: newValue);
- ffi.canvasModel.updateViewStyle();
- cancelFunc();
- },
- padding: padding,
+ RemoteMenuEntry.viewStyle(
+ key,
+ ffi,
+ padding,
+ dismissFunc: cancelFunc,
),
]);
if (!ffi.canvasModel.cursorEmbedded) {
menu.add(MenuEntryDivider());
- menu.add(() {
- final state = ShowRemoteCursorState.find(key);
- return MenuEntrySwitch2(
- switchType: SwitchType.scheckbox,
- text: translate('Show remote cursor'),
- getter: () {
- return state;
- },
- setter: (bool v) async {
- state.value = v;
- await bind.sessionToggleOption(
- id: key, value: 'show-remote-cursor');
- cancelFunc();
- },
- padding: padding,
- );
- }());
+ menu.add(RemoteMenuEntry.showRemoteCursor(
+ key,
+ padding,
+ dismissFunc: cancelFunc,
+ ));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
- menu.add(MenuEntrySwitch(
- switchType: SwitchType.scheckbox,
- text: translate('Disable clipboard'),
- getter: () async {
- return bind.sessionGetToggleOptionSync(
- id: key, arg: 'disable-clipboard');
- },
- setter: (bool v) async {
- await bind.sessionToggleOption(id: key, value: 'disable-clipboard');
- cancelFunc();
- },
- padding: padding,
- ));
+ menu.add(RemoteMenuEntry.disableClipboard(key, padding,
+ dismissFunc: cancelFunc));
}
- menu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Insert Lock'),
- style: style,
- ),
- proc: () {
- bind.sessionLockScreen(id: key);
- cancelFunc();
- },
- padding: padding,
- dismissOnClicked: true,
- ));
+ menu.add(
+ RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
- menu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- '${translate("Insert")} Ctrl + Alt + Del',
- style: style,
- ),
- proc: () {
- bind.sessionCtrlAltDel(id: key);
- cancelFunc();
- },
- padding: padding,
- dismissOnClicked: true,
- ));
+ menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding,
+ dismissFunc: cancelFunc));
}
}
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index b4d7f4fac..b66a08e74 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -514,6 +514,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,
@@ -619,7 +652,7 @@ class _CmControlPanel extends StatelessWidget {
.marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
}
- buildButton(
+ Widget buildButton(
BuildContext context, {
required Color? color,
required Function() onClick,
@@ -685,6 +718,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 a371e8f52..666c9a6e2 100644
--- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
+++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
@@ -790,6 +790,7 @@ class _PopupMenuRoute extends PopupRoute {
_PopupMenuRoute({
required this.position,
required this.items,
+ this.menuWrapper,
this.initialValue,
this.elevation,
required this.barrierLabel,
@@ -802,6 +803,7 @@ class _PopupMenuRoute extends PopupRoute {
final RelativeRect position;
final List> items;
+ final MenuWrapper? menuWrapper;
final List itemSizes;
final T? initialValue;
final double? elevation;
@@ -844,11 +846,14 @@ class _PopupMenuRoute extends PopupRoute {
}
}
- final Widget menu = _PopupMenu(
+ Widget menu = _PopupMenu(
route: this,
semanticLabel: semanticLabel,
constraints: constraints,
);
+ if (this.menuWrapper != null) {
+ menu = this.menuWrapper!(menu);
+ }
final MediaQueryData mediaQuery = MediaQuery.of(context);
return MediaQuery.removePadding(
context: context,
@@ -1035,6 +1040,7 @@ Future showMenu({
required BuildContext context,
required RelativeRect position,
required List> items,
+ MenuWrapper? menuWrapper,
T? initialValue,
double? elevation,
String? semanticLabel,
@@ -1062,6 +1068,7 @@ Future showMenu({
return navigator.push(_PopupMenuRoute(
position: position,
items: items,
+ menuWrapper: menuWrapper,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
@@ -1094,6 +1101,8 @@ typedef PopupMenuCanceled = void Function();
typedef PopupMenuItemBuilder = List> Function(
BuildContext context);
+typedef MenuWrapper = Widget Function(Widget child);
+
/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
/// because an item was selected. The value passed to [onSelected] is the value of
/// the selected menu item.
@@ -1124,6 +1133,7 @@ class PopupMenuButton extends StatefulWidget {
const PopupMenuButton({
Key? key,
required this.itemBuilder,
+ this.menuWrapper,
this.initialValue,
this.onHover,
this.onSelected,
@@ -1151,6 +1161,9 @@ class PopupMenuButton extends StatefulWidget {
/// Called when the button is pressed to create the items to show in the menu.
final PopupMenuItemBuilder itemBuilder;
+ /// Menu wrapper.
+ final MenuWrapper? menuWrapper;
+
/// The value of the menu item, if any, that should be highlighted when the menu opens.
final T? initialValue;
@@ -1333,6 +1346,7 @@ class PopupMenuButtonState extends State> {
context: context,
elevation: widget.elevation ?? popupMenuTheme.elevation,
items: items,
+ menuWrapper: widget.menuWrapper,
initialValue: widget.initialValue,
position: position,
shape: widget.shape ?? popupMenuTheme.shape,
diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart
index 0cbdad929..9833dcbca 100644
--- a/flutter/lib/desktop/widgets/popup_menu.dart
+++ b/flutter/lib/desktop/widgets/popup_menu.dart
@@ -109,13 +109,17 @@ class MenuConfig {
this.boxWidth});
}
+typedef DismissCallback = Function();
+
abstract class MenuEntryBase {
bool dismissOnClicked;
+ DismissCallback? dismissCallback;
RxBool? enabled;
MenuEntryBase({
this.dismissOnClicked = false,
this.enabled,
+ this.dismissCallback,
});
List> build(BuildContext context, MenuConfig conf);
@@ -146,12 +150,14 @@ class MenuEntryRadioOption {
String value;
bool dismissOnClicked;
RxBool? enabled;
+ DismissCallback? dismissCallback;
MenuEntryRadioOption({
required this.text,
required this.value,
this.dismissOnClicked = false,
this.enabled,
+ this.dismissCallback,
});
}
@@ -177,8 +183,13 @@ class MenuEntryRadios extends MenuEntryBase {
required this.optionSetter,
this.padding,
dismissOnClicked = false,
+ dismissCallback,
RxBool? enabled,
- }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
+ }) : super(
+ dismissOnClicked: dismissOnClicked,
+ enabled: enabled,
+ dismissCallback: dismissCallback,
+ ) {
() async {
_curOption.value = await curOptionGetter();
}();
@@ -249,6 +260,9 @@ class MenuEntryRadios extends MenuEntryBase {
onPressed() {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
+ if (opt.dismissCallback != null) {
+ opt.dismissCallback!();
+ }
}
setOption(opt.value);
}
@@ -360,6 +374,9 @@ class MenuEntrySubRadios extends MenuEntryBase {
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
+ if (opt.dismissCallback != null) {
+ opt.dismissCallback!();
+ }
}
setOption(opt.value);
},
@@ -421,7 +438,12 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase {
this.textStyle,
this.padding,
RxBool? enabled,
- }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
+ dismissCallback,
+ }) : super(
+ dismissOnClicked: dismissOnClicked,
+ enabled: enabled,
+ dismissCallback: dismissCallback,
+ );
RxBool get curOption;
Future setOption(bool? option);
@@ -463,6 +485,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
+ if (super.dismissCallback != null) {
+ super.dismissCallback!();
+ }
}
setOption(v);
},
@@ -474,6 +499,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
+ if (super.dismissCallback != null) {
+ super.dismissCallback!();
+ }
}
setOption(v);
},
@@ -485,6 +513,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase {
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
+ if (super.dismissCallback != null) {
+ super.dismissCallback!();
+ }
}
setOption(!curOption.value);
},
@@ -508,6 +539,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
+ dismissCallback,
}) : super(
switchType: switchType,
text: text,
@@ -515,6 +547,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase {
padding: padding,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
+ dismissCallback: dismissCallback,
) {
() async {
_curOption.value = await getter();
@@ -551,12 +584,15 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
+ dismissCallback,
}) : super(
- switchType: switchType,
- text: text,
- textStyle: textStyle,
- padding: padding,
- dismissOnClicked: dismissOnClicked);
+ switchType: switchType,
+ text: text,
+ textStyle: textStyle,
+ padding: padding,
+ dismissOnClicked: dismissOnClicked,
+ dismissCallback: dismissCallback,
+ );
@override
RxBool get curOption => getter();
@@ -627,9 +663,11 @@ class MenuEntryButton extends MenuEntryBase {
this.padding,
dismissOnClicked = false,
RxBool? enabled,
+ dismissCallback,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
+ dismissCallback: dismissCallback,
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
@@ -641,6 +679,9 @@ class MenuEntryButton extends MenuEntryBase {
? () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
+ if (super.dismissCallback != null) {
+ super.dismissCallback!();
+ }
}
proc();
}
diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index 6ad030464..6bb49000b 100644
--- a/flutter/lib/desktop/widgets/remote_menubar.dart
+++ b/flutter/lib/desktop/widgets/remote_menubar.dart
@@ -9,6 +9,7 @@ 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';
@@ -99,6 +100,175 @@ class _MenubarTheme {
static const double dividerHeight = 12.0;
}
+typedef DismissFunc = void Function();
+
+class RemoteMenuEntry {
+ static MenuEntryRadios viewStyle(
+ String remoteId,
+ FFI ffi,
+ EdgeInsets padding, {
+ DismissFunc? dismissFunc,
+ DismissCallback? dismissCallback,
+ RxString? rxViewStyle,
+ }) {
+ return MenuEntryRadios(
+ text: translate('Ratio'),
+ optionsGetter: () => [
+ MenuEntryRadioOption(
+ text: translate('Scale original'),
+ value: kRemoteViewStyleOriginal,
+ dismissOnClicked: true,
+ dismissCallback: dismissCallback,
+ ),
+ MenuEntryRadioOption(
+ text: translate('Scale adaptive'),
+ value: kRemoteViewStyleAdaptive,
+ dismissOnClicked: true,
+ dismissCallback: dismissCallback,
+ ),
+ ],
+ curOptionGetter: () async {
+ // null means peer id is not found, which there's no need to care about
+ final viewStyle = await bind.sessionGetViewStyle(id: remoteId) ?? '';
+ if (rxViewStyle != null) {
+ rxViewStyle.value = viewStyle;
+ }
+ return viewStyle;
+ },
+ optionSetter: (String oldValue, String newValue) async {
+ await bind.sessionSetViewStyle(id: remoteId, value: newValue);
+ if (rxViewStyle != null) {
+ rxViewStyle.value = newValue;
+ }
+ ffi.canvasModel.updateViewStyle();
+ if (dismissFunc != null) {
+ dismissFunc();
+ }
+ },
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: dismissCallback,
+ );
+ }
+
+ static MenuEntrySwitch2 showRemoteCursor(
+ String remoteId,
+ EdgeInsets padding, {
+ DismissFunc? dismissFunc,
+ DismissCallback? dismissCallback,
+ }) {
+ final state = ShowRemoteCursorState.find(remoteId);
+ final optKey = 'show-remote-cursor';
+ return MenuEntrySwitch2(
+ switchType: SwitchType.scheckbox,
+ text: translate('Show remote cursor'),
+ getter: () {
+ return state;
+ },
+ setter: (bool v) async {
+ await bind.sessionToggleOption(id: remoteId, value: optKey);
+ state.value =
+ bind.sessionGetToggleOptionSync(id: remoteId, arg: optKey);
+ if (dismissFunc != null) {
+ dismissFunc();
+ }
+ },
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: dismissCallback,
+ );
+ }
+
+ static MenuEntrySwitch disableClipboard(
+ String remoteId,
+ EdgeInsets? padding, {
+ DismissFunc? dismissFunc,
+ DismissCallback? dismissCallback,
+ }) {
+ return createSwitchMenuEntry(
+ remoteId,
+ 'Disable clipboard',
+ 'disable-clipboard',
+ padding,
+ true,
+ dismissCallback: dismissCallback,
+ );
+ }
+
+ static MenuEntrySwitch createSwitchMenuEntry(
+ String remoteId,
+ String text,
+ String option,
+ EdgeInsets? padding,
+ bool dismissOnClicked, {
+ DismissFunc? dismissFunc,
+ DismissCallback? dismissCallback,
+ }) {
+ return MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: translate(text),
+ getter: () async {
+ return bind.sessionGetToggleOptionSync(id: remoteId, arg: option);
+ },
+ setter: (bool v) async {
+ await bind.sessionToggleOption(id: remoteId, value: option);
+ if (dismissFunc != null) {
+ dismissFunc();
+ }
+ },
+ padding: padding,
+ dismissOnClicked: dismissOnClicked,
+ dismissCallback: dismissCallback,
+ );
+ }
+
+ static MenuEntryButton insertLock(
+ String remoteId,
+ EdgeInsets? padding, {
+ DismissFunc? dismissFunc,
+ DismissCallback? dismissCallback,
+ }) {
+ return MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Insert Lock'),
+ style: style,
+ ),
+ proc: () {
+ bind.sessionLockScreen(id: remoteId);
+ if (dismissFunc != null) {
+ dismissFunc();
+ }
+ },
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: dismissCallback,
+ );
+ }
+
+ static insertCtrlAltDel(
+ String remoteId,
+ EdgeInsets? padding, {
+ DismissFunc? dismissFunc,
+ DismissCallback? dismissCallback,
+ }) {
+ return MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ '${translate("Insert")} Ctrl + Alt + Del',
+ style: style,
+ ),
+ proc: () {
+ bind.sessionCtrlAltDel(id: remoteId);
+ if (dismissFunc != null) {
+ dismissFunc();
+ }
+ },
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: dismissCallback,
+ );
+ }
+}
+
class RemoteMenubar extends StatefulWidget {
final String id;
final FFI ffi;
@@ -221,6 +391,18 @@ class _RemoteMenubarState extends State {
}
}
+ Widget _buildPointerTrackWidget(Widget child) {
+ return Listener(
+ onPointerHover: (PointerHoverEvent e) =>
+ widget.ffi.inputModel.lastMousePos = e.position,
+ child: MouseRegion(
+ child: child,
+ ),
+ );
+ }
+
+ _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos();
+
Widget _buildMenubar(BuildContext context) {
final List menubarItems = [];
if (!isWebDesktop) {
@@ -244,6 +426,7 @@ 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));
@@ -297,31 +480,6 @@ class _RemoteMenubarState extends State {
);
}
- final _chatButtonKey = GlobalKey();
- Widget _buildChat(BuildContext context) {
- return IconButton(
- key: _chatButtonKey,
- tooltip: translate('Chat'),
- onPressed: () {
- 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);
- },
- icon: const Icon(
- Icons.message,
- color: _MenubarTheme.commonColor,
- ),
- );
- }
-
Widget _buildMonitor(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
return mod_menu.PopupMenuButton(
@@ -375,6 +533,7 @@ class _RemoteMenubarState extends State {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
+ _menuDismissCallback();
}
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
@@ -390,13 +549,10 @@ class _RemoteMenubarState extends State {
mod_menu.PopupMenuItem(
height: _MenubarTheme.height,
padding: EdgeInsets.zero,
- child: Listener(
- onPointerHover: (PointerHoverEvent e) =>
- widget.ffi.inputModel.lastMousePos = e.position,
- child: MouseRegion(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: rowChildren),
+ child: _buildPointerTrackWidget(
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: rowChildren,
),
),
)
@@ -446,6 +602,7 @@ class _RemoteMenubarState extends State {
),
tooltip: translate('Display Settings'),
position: mod_menu.PopupMenuPosition.under,
+ menuWrapper: _buildPointerTrackWidget,
itemBuilder: (BuildContext context) =>
_getDisplayMenu(snapshot.data!, remoteCount)
.map((entry) => entry.build(
@@ -500,12 +657,17 @@ class _RemoteMenubarState extends State {
? 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,
- ),
+ icon: value.start
+ ? Icon(
+ Icons.pause_circle_filled,
+ color: _MenubarTheme.commonColor,
+ )
+ : SvgPicture.asset(
+ "assets/record_screen.svg",
+ color: _MenubarTheme.commonColor,
+ width: Theme.of(context).iconTheme.size ?? 22.0,
+ height: Theme.of(context).iconTheme.size ?? 22.0,
+ ),
));
} else {
return Offstage();
@@ -526,6 +688,130 @@ class _RemoteMenubarState extends State {
);
}
+ 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: _MenubarTheme.commonColor,
+ width: Theme.of(context).iconTheme.size ?? 24.0,
+ height: Theme.of(context).iconTheme.size ?? 24.0,
+ ),
+ tooltip: translate('Chat'),
+ position: mod_menu.PopupMenuPosition.under,
+ itemBuilder: (BuildContext context) => _getChatMenu(context)
+ .map((entry) => entry.build(
+ context,
+ const MenuConfig(
+ commonColor: _MenubarTheme.commonColor,
+ height: _MenubarTheme.height,
+ dividerHeight: _MenubarTheme.dividerHeight,
+ )))
+ .expand((i) => i)
+ .toList(),
+ );
+ }
+
+ Widget _getVoiceCallIcon() {
+ switch (widget.ffi.chatModel.voiceCallStatus.value) {
+ case VoiceCallStatus.waitingForResponse:
+ return IconButton(
+ onPressed: () {
+ widget.ffi.chatModel.closeVoiceCall(widget.id);
+ },
+ icon: SvgPicture.asset(
+ "assets/voice_call_waiting.svg",
+ color: Colors.red,
+ width: Theme.of(context).iconTheme.size ?? 20.0,
+ height: Theme.of(context).iconTheme.size ?? 20.0,
+ ));
+ case VoiceCallStatus.connected:
+ return IconButton(
+ onPressed: () {
+ widget.ffi.chatModel.closeVoiceCall(widget.id);
+ },
+ icon: Icon(
+ Icons.phone_disabled_rounded,
+ color: Colors.red,
+ size: Theme.of(context).iconTheme.size ?? 22.0,
+ ),
+ );
+ 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()
+ : IconButton(
+ padding: EdgeInsets.zero,
+ icon: _getVoiceCallIcon(),
+ tooltip: translate(tooltipText),
+ onPressed: () => bind.sessionRequestVoiceCall(id: widget.id),
+ );
+ },
+ );
+ }
+
+ 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;
@@ -554,6 +840,7 @@ class _RemoteMenubarState extends State {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
+ _menuDismissCallback();
}
showSetOSPassword(
widget.id, false, widget.ffi.dialogManager);
@@ -566,6 +853,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
MenuEntryButton(
childBuilder: (TextStyle? style) => Text(
@@ -577,6 +865,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
MenuEntryButton(
childBuilder: (TextStyle? style) => Text(
@@ -588,6 +877,7 @@ class _RemoteMenubarState extends State {
connect(context, widget.id, isTcpTunneling: true);
},
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
]);
// {handler.get_audit_server() && {translate('Note')}}
@@ -605,23 +895,15 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
);
}
displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
- displayMenu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- '${translate("Insert")} Ctrl + Alt + Del',
- style: style,
- ),
- proc: () {
- bind.sessionCtrlAltDel(id: widget.id);
- },
- padding: padding,
- dismissOnClicked: true,
- ));
+ displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding,
+ dismissCallback: _menuDismissCallback));
}
}
if (perms['restart'] != false &&
@@ -638,21 +920,13 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
if (perms['keyboard'] != false) {
- displayMenu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Insert Lock'),
- style: style,
- ),
- proc: () {
- bind.sessionLockScreen(id: widget.id);
- },
- padding: padding,
- dismissOnClicked: true,
- ));
+ displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding,
+ dismissCallback: _menuDismissCallback));
if (pi.platform == kPeerPlatformWindows) {
displayMenu.add(MenuEntryButton(
@@ -670,6 +944,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
if (pi.platform != kPeerPlatformAndroid &&
@@ -684,6 +959,7 @@ class _RemoteMenubarState extends State {
showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager),
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
}
@@ -699,6 +975,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
@@ -720,10 +997,10 @@ class _RemoteMenubarState extends State {
// },
// padding: padding,
// dismissOnClicked: true,
+ // dismissCallback: _menuDismissCallback,
// ));
// }
}
-
return displayMenu;
}
@@ -758,33 +1035,12 @@ class _RemoteMenubarState extends State {
const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
final peer_version = widget.ffi.ffiModel.pi.version;
final displayMenu = [
- MenuEntryRadios(
- text: translate('Ratio'),
- optionsGetter: () => [
- MenuEntryRadioOption(
- text: translate('Scale original'),
- value: kRemoteViewStyleOriginal,
- dismissOnClicked: true,
- ),
- MenuEntryRadioOption(
- text: translate('Scale adaptive'),
- value: kRemoteViewStyleAdaptive,
- dismissOnClicked: true,
- ),
- ],
- curOptionGetter: () async {
- // null means peer id is not found, which there's no need to care about
- final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? '';
- widget.state.viewStyle.value = viewStyle;
- return viewStyle;
- },
- optionSetter: (String oldValue, String newValue) async {
- await bind.sessionSetViewStyle(id: widget.id, value: newValue);
- widget.state.viewStyle.value = newValue;
- widget.ffi.canvasModel.updateViewStyle();
- },
- padding: padding,
- dismissOnClicked: true,
+ RemoteMenuEntry.viewStyle(
+ widget.id,
+ widget.ffi,
+ padding,
+ dismissCallback: _menuDismissCallback,
+ rxViewStyle: widget.state.viewStyle,
),
MenuEntryDivider(),
MenuEntryRadios(
@@ -794,21 +1050,26 @@ class _RemoteMenubarState extends State {
text: translate('Good image quality'),
value: kRemoteImageQualityBest,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Balanced'),
value: kRemoteImageQualityBalanced,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Optimize reaction time'),
value: kRemoteImageQualityLow,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
- text: translate('Custom'),
- value: kRemoteImageQualityCustom,
- dismissOnClicked: true),
+ text: translate('Custom'),
+ value: kRemoteImageQualityCustom,
+ dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
+ ),
],
curOptionGetter: () async =>
// null means peer id is not found, which there's no need to care about
@@ -973,12 +1234,14 @@ class _RemoteMenubarState extends State {
text: translate('ScrollAuto'),
value: kRemoteScrollStyleAuto,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
enabled: widget.ffi.canvasModel.imageOverflow,
),
MenuEntryRadioOption(
text: translate('Scrollbar'),
value: kRemoteScrollStyleBar,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
enabled: widget.ffi.canvasModel.imageOverflow,
),
],
@@ -991,6 +1254,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
displayMenu.insert(3, MenuEntryDivider());
@@ -1061,6 +1325,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
);
}
@@ -1087,11 +1352,13 @@ class _RemoteMenubarState extends State {
text: translate('Auto'),
value: 'auto',
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: 'VP9',
value: 'vp9',
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
),
];
if (codecs[0]) {
@@ -1099,6 +1366,7 @@ class _RemoteMenubarState extends State {
text: 'H264',
value: 'h264',
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
if (codecs[1]) {
@@ -1106,6 +1374,7 @@ class _RemoteMenubarState extends State {
text: 'H265',
value: 'h265',
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
return list;
@@ -1122,6 +1391,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
}
@@ -1129,23 +1399,11 @@ class _RemoteMenubarState extends State {
/// Show remote cursor
if (!widget.ffi.canvasModel.cursorEmbedded) {
- displayMenu.add(() {
- final state = ShowRemoteCursorState.find(widget.id);
- return MenuEntrySwitch2(
- switchType: SwitchType.scheckbox,
- text: translate('Show remote cursor'),
- getter: () {
- return state;
- },
- setter: (bool v) async {
- state.value = v;
- await bind.sessionToggleOption(
- id: widget.id, value: 'show-remote-cursor');
- },
- padding: padding,
- dismissOnClicked: true,
- );
- }());
+ displayMenu.add(RemoteMenuEntry.showRemoteCursor(
+ widget.id,
+ padding,
+ dismissCallback: _menuDismissCallback,
+ ));
}
/// Show remote cursor scaling with image
@@ -1160,11 +1418,13 @@ class _RemoteMenubarState extends State {
return state;
},
setter: (bool v) async {
- state.value = v;
await bind.sessionToggleOption(id: widget.id, value: opt);
+ state.value =
+ bind.sessionGetToggleOptionSync(id: widget.id, arg: opt);
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
);
}());
}
@@ -1184,6 +1444,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
final perms = widget.ffi.ffiModel.permissions;
@@ -1192,6 +1453,8 @@ class _RemoteMenubarState extends State {
if (perms['audio'] != false) {
displayMenu
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
+ displayMenu
+ .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
}
if (Platform.isWindows &&
@@ -1203,8 +1466,11 @@ class _RemoteMenubarState extends State {
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
- displayMenu.add(_createSwitchMenuEntry(
- 'Disable clipboard', 'disable-clipboard', padding, true));
+ displayMenu.add(RemoteMenuEntry.disableClipboard(
+ widget.id,
+ padding,
+ dismissCallback: _menuDismissCallback,
+ ));
}
displayMenu.add(_createSwitchMenuEntry(
'Lock after session end', 'lock-after-session-end', padding, true));
@@ -1221,6 +1487,7 @@ class _RemoteMenubarState extends State {
},
padding: padding,
dismissOnClicked: true,
+ dismissCallback: _menuDismissCallback,
));
}
}
@@ -1233,25 +1500,29 @@ 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") {
+ for (KeyboardModeMenu mode in modes) {
+ if (bind.sessionIsKeyboardModeSupported(
+ id: widget.id, mode: mode.key)) {
+ if (mode.key == 'translate') {
+ if (!Platform.isWindows ||
+ widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
+ continue;
+ }
+ }
list.add(MenuEntryRadioOption(
- text: translate('Legacy mode'), value: 'legacy'));
- } else if (mode == "map") {
- list.add(MenuEntryRadioOption(
- text: translate('Map mode'), value: 'map'));
+ text: translate(mode.menu), 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);
@@ -1292,6 +1563,7 @@ class _RemoteMenubarState extends State {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
+ _menuDismissCallback();
}
showKBLayoutTypeChooser(
localPlatform, widget.ffi.dialogManager);
@@ -1304,6 +1576,7 @@ class _RemoteMenubarState extends State {
proc: () {},
padding: EdgeInsets.zero,
dismissOnClicked: false,
+ dismissCallback: _menuDismissCallback,
),
);
}
@@ -1312,18 +1585,9 @@ class _RemoteMenubarState extends State {
MenuEntrySwitch _createSwitchMenuEntry(
String text, String option, EdgeInsets? padding, bool dismissOnClicked) {
- return MenuEntrySwitch(
- switchType: SwitchType.scheckbox,
- text: translate(text),
- getter: () async {
- return bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
- },
- setter: (bool v) async {
- await bind.sessionToggleOption(id: widget.id, value: option);
- },
- padding: padding,
- dismissOnClicked: dismissOnClicked,
- );
+ return RemoteMenuEntry.createSwitchMenuEntry(
+ widget.id, text, option, padding, dismissOnClicked,
+ dismissCallback: _menuDismissCallback);
}
}
@@ -1547,3 +1811,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 ddc51eddb..5c37900f2 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -1,23 +1,23 @@
-import 'dart:io';
import 'dart:async';
+import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
+import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
-import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:bot_toast/bot_toast.dart';
import '../../utils/multi_window_manager.dart';
@@ -545,7 +545,9 @@ class WindowActionPanelState extends State
void onWindowClose() async {
// hide window on close
if (widget.isMainWindow) {
- await rustDeskWinManager.unregisterActiveWindow(0);
+ if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
+ await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
+ }
// `hide` must be placed after unregisterActiveWindow, because once all windows are hidden,
// flutter closes the application on macOS. We should ensure the post-run logic has ran successfully.
// e.g.: saving window position.
@@ -976,7 +978,7 @@ class _CloseButton extends StatelessWidget {
offstage: !visible,
child: InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
- customBorder: const RoundedRectangleBorder(),
+ customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
@@ -1099,7 +1101,7 @@ class TabbarTheme extends ThemeExtension {
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
dividerColor: Color.fromARGB(255, 238, 238, 238),
hoverColor: Color.fromARGB(51, 158, 158, 158),
- closeHoverColor: Colors.black,
+ closeHoverColor: Color.fromARGB(255, 224, 224, 224),
selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240));
static const dark = TabbarTheme(
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index 5b1e0c37c..c61287d4f 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -1,22 +1,23 @@
+import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
-import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
+import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
-import 'package:bot_toast/bot_toast.dart';
// import 'package:window_manager/window_manager.dart';
@@ -31,6 +32,9 @@ int? kWindowId;
WindowType? kWindowType;
late List kBootArgs;
+/// Uni links.
+StreamSubscription? _uniLinkSubscription;
+
Future main(List args) async {
WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args");
@@ -114,7 +118,6 @@ Future initEnv(String appType) async {
void runMainApp(bool startService) async {
// register uni links
- initUniLinks();
await initEnv(kAppTypeMain);
// trigger connection status updater
await bind.mainCheckConnectStatus();
@@ -130,7 +133,11 @@ void runMainApp(bool startService) async {
// Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main);
// Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
- if (checkArguments()) {
+ final handledByUniLinks = await initUniLinks();
+ final handledByCli = checkArguments();
+ debugPrint(
+ "handled by uni links: $handledByUniLinks, handled by cli: $handledByCli");
+ if (handledByUniLinks || handledByCli) {
windowManager.hide();
} else {
windowManager.show();
@@ -139,8 +146,8 @@ void runMainApp(bool startService) async {
rustDeskWinManager.registerActiveWindow(kWindowMainId);
}
windowManager.setOpacity(1);
+ windowManager.setTitle(getWindowName());
});
- windowManager.setTitle(getWindowName());
}
void runMobileApp() async {
@@ -208,7 +215,8 @@ void runMultiWindow(
}
void runConnectionManagerScreen(bool hide) async {
- await initEnv(kAppTypeMain);
+ await initEnv(kAppTypeConnectionManager);
+ await bind.cmStartListenIpcThread();
_runApp(
'',
const DesktopServerPage(),
@@ -219,6 +227,8 @@ void runConnectionManagerScreen(bool hide) async {
} else {
showCmWindow();
}
+ // Start the uni links handler and redirect links to Native, not for Flutter.
+ _uniLinkSubscription = listenUniLinks(handleByFlutter: false);
}
void showCmWindow() {
@@ -350,6 +360,7 @@ class _AppState extends State {
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
+ ChangeNotifierProvider.value(value: gFFI.peerTabModel),
],
child: GetMaterialApp(
navigatorKey: globalKey,
diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart
index bded6d069..7e9a9879c 100644
--- a/flutter/lib/mobile/widgets/dialog.dart
+++ b/flutter/lib/mobile/widgets/dialog.dart
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
-import 'package:flutter_hbb/desktop/widgets/button.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -371,8 +370,7 @@ void showWaitUacDialog(
tag: '$id-wait-uac',
(setState, close) => CustomAlertDialog(
title: null,
- content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip')
- .marginOnly(bottom: 10),
+ content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
));
}
@@ -645,10 +643,9 @@ class _PasswordWidgetState extends State {
// Here is key idea
suffixIcon: IconButton(
icon: Icon(
- // Based on passwordVisible state choose the icon
- _passwordVisible ? Icons.visibility : Icons.visibility_off,
- color: Theme.of(context).primaryColorDark,
- ),
+ // Based on passwordVisible state choose the icon
+ _passwordVisible ? Icons.visibility : Icons.visibility_off,
+ color: MyTheme.lightTheme.primaryColor),
onPressed: () {
// Update the state i.e. toggle the state of passwordVisible variable
setState(() {
diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart
index 8320d08dd..8666e13e4 100644
--- a/flutter/lib/models/chat_model.dart
+++ b/flutter/lib/models/chat_model.dart
@@ -5,6 +5,7 @@ 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';
@@ -31,10 +32,14 @@ class ChatModel with ChangeNotifier {
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: "",
@@ -312,4 +317,34 @@ class ChatModel with ChangeNotifier {
}
});
}
+
+ 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/group_model.dart b/flutter/lib/models/group_model.dart
index 4d9fab0e4..5e2b85f90 100644
--- a/flutter/lib/models/group_model.dart
+++ b/flutter/lib/models/group_model.dart
@@ -35,7 +35,7 @@ class GroupModel {
await reset();
if (gFFI.userModel.userName.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
- statePeerTab.check();
+ gFFI.peerTabModel.check_dynamic_tabs();
return;
}
userLoading.value = true;
@@ -82,7 +82,7 @@ class GroupModel {
userLoadError.value = err.toString();
} finally {
userLoading.value = false;
- statePeerTab.check();
+ gFFI.peerTabModel.check_dynamic_tabs();
}
}
diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart
index d2f671cdc..8c37f50bd 100644
--- a/flutter/lib/models/input_model.dart
+++ b/flutter/lib/models/input_model.dart
@@ -310,7 +310,6 @@ class InputModel {
}
}
-
int _signOrZero(num x) {
if (x == 0) {
return 0;
@@ -362,7 +361,6 @@ class InputModel {
trackpadScrollDistance = Offset.zero;
}
-
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage");
if (e.kind != ui.PointerDeviceKind.mouse) {
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 5e4693ccc..ca99a5bd1 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -13,6 +13,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
+import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -197,6 +198,26 @@ class FfiModel with ChangeNotifier {
final peer_id = evt['peer_id'].toString();
await bind.sessionSwitchSides(id: peer_id);
closeConnection(id: peer_id);
+ } 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");
}
};
}
@@ -242,7 +263,6 @@ class FfiModel with ChangeNotifier {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay();
- parent.target?.inputModel.refreshMousePos();
notifyListeners();
}
@@ -538,6 +558,7 @@ class CanvasModel with ChangeNotifier {
double _y = 0;
// image scale
double _scale = 1.0;
+ double _devicePixelRatio = 1.0;
Size _size = Size.zero;
// the tabbar over the image
// double tabBarHeight = 0.0;
@@ -561,6 +582,7 @@ class CanvasModel with ChangeNotifier {
double get x => _x;
double get y => _y;
double get scale => _scale;
+ double get devicePixelRatio => _devicePixelRatio;
Size get size => _size;
ScrollStyle get scrollStyle => _scrollStyle;
ViewStyle get viewStyle => _lastViewStyle;
@@ -609,13 +631,15 @@ class CanvasModel with ChangeNotifier {
_lastViewStyle = viewStyle;
_scale = viewStyle.scale;
+ _devicePixelRatio = ui.window.devicePixelRatio;
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
- _scale = 1.0 / ui.window.devicePixelRatio;
+ _scale = 1.0 / _devicePixelRatio;
}
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
_imageOverflow.value = _x < 0 || y < 0;
notifyListeners();
+ parent.target?.inputModel.refreshMousePos();
}
updateScrollStyle() async {
@@ -745,7 +769,7 @@ class CanvasModel with ChangeNotifier {
class CursorData {
final String peerId;
final int id;
- final img2.Image? image;
+ final img2.Image image;
double scale;
Uint8List? data;
final double hotxOrigin;
@@ -770,33 +794,40 @@ class CursorData {
int _doubleToInt(double v) => (v * 10e6).round().toInt();
- double _checkUpdateScale(double scale, bool shouldScale) {
+ double _checkUpdateScale(double scale) {
double oldScale = this.scale;
- if (!shouldScale) {
- scale = 1.0;
- } else {
+ if (scale != 1.0) {
// Update data if scale changed.
- if (Platform.isWindows) {
- final tgtWidth = (width * scale).toInt();
- final tgtHeight = (width * scale).toInt();
- if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
- double sw = kMinCursorSize.toDouble() / width;
- double sh = kMinCursorSize.toDouble() / height;
- scale = sw < sh ? sh : sw;
- }
+ final tgtWidth = (width * scale).toInt();
+ final tgtHeight = (width * scale).toInt();
+ if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
+ double sw = kMinCursorSize.toDouble() / width;
+ double sh = kMinCursorSize.toDouble() / height;
+ scale = sw < sh ? sh : sw;
}
}
- if (Platform.isWindows) {
- if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
+ if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
+ if (Platform.isWindows) {
data = img2
.copyResize(
- image!,
+ image,
width: (width * scale).toInt(),
height: (height * scale).toInt(),
interpolation: img2.Interpolation.average,
)
.getBytes(format: img2.Format.bgra);
+ } else {
+ data = Uint8List.fromList(
+ img2.encodePng(
+ img2.copyResize(
+ image,
+ width: (width * scale).toInt(),
+ height: (height * scale).toInt(),
+ interpolation: img2.Interpolation.average,
+ ),
+ ),
+ );
}
}
@@ -806,8 +837,8 @@ class CursorData {
return scale;
}
- String updateGetKey(double scale, bool shouldScale) {
- scale = _checkUpdateScale(scale, shouldScale);
+ String updateGetKey(double scale) {
+ scale = _checkUpdateScale(scale);
return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}';
}
}
@@ -865,7 +896,7 @@ class PredefinedCursor {
_cache = CursorData(
peerId: '',
id: id,
- image: _image2?.clone(),
+ image: _image2!.clone(),
scale: scale,
data: data,
hotxOrigin:
@@ -892,9 +923,10 @@ class CursorModel with ChangeNotifier {
double _hoty = 0;
double _displayOriginX = 0;
double _displayOriginY = 0;
+ DateTime? _firstUpdateMouseTime;
bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now()
- .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec));
+ .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String id = '';
WeakReference parent;
@@ -913,6 +945,15 @@ class CursorModel with ChangeNotifier {
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec;
+ bool isConnIn2Secs() {
+ if (_firstUpdateMouseTime == null) {
+ _firstUpdateMouseTime = DateTime.now();
+ return true;
+ } else {
+ return DateTime.now().difference(_firstUpdateMouseTime!).inSeconds < 2;
+ }
+ }
+
CursorModel(this.parent);
Set get cachedKeys => _cacheKeys;
@@ -1065,9 +1106,9 @@ class CursorModel with ChangeNotifier {
Future _updateCache(
Uint8List rgba, ui.Image image, int id, int w, int h) async {
Uint8List? data;
- img2.Image? imgOrigin;
+ img2.Image imgOrigin =
+ img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba);
if (Platform.isWindows) {
- imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba);
data = imgOrigin.getBytes(format: img2.Format.bgra);
} else {
ByteData? imgBytes =
@@ -1109,8 +1150,10 @@ class CursorModel with ChangeNotifier {
/// Update the cursor position.
updateCursorPosition(Map evt, String id) async {
- gotMouseControl = false;
- _lastPeerMouse = DateTime.now();
+ if (!isConnIn2Secs()) {
+ gotMouseControl = false;
+ _lastPeerMouse = DateTime.now();
+ }
_x = double.parse(evt['x']);
_y = double.parse(evt['y']);
try {
@@ -1265,8 +1308,9 @@ class FFI {
late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global
+ late final PeerTabModel peerTabModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
- late final RecordingModel recordingModel; // recording
+ late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session
FFI() {
@@ -1278,6 +1322,7 @@ class FFI {
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
+ peerTabModel = PeerTabModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart
index cf2de4219..34a673953 100644
--- a/flutter/lib/models/native_model.dart
+++ b/flutter/lib/models/native_model.dart
@@ -8,6 +8,7 @@ import 'package:external_path/external_path.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_hbb/consts.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win32/win32.dart' as win32;
@@ -46,6 +47,8 @@ class PlatformFFI {
static get localeName => Platform.localeName;
+ static get isMain => instance._appType == kAppTypeMain;
+
static Future getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
@@ -112,8 +115,15 @@ class PlatformFFI {
}
_ffiBind = RustdeskImpl(dylib);
if (Platform.isLinux) {
- // start dbus service, no need to await
- await _ffiBind.mainStartDbusServer();
+ // Start a dbus service, no need to await
+ _ffiBind.mainStartDbusServer();
+ } else if (Platform.isMacOS && isMain) {
+ 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/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart
new file mode 100644
index 000000000..7c6211682
--- /dev/null
+++ b/flutter/lib/models/peer_tab_model.dart
@@ -0,0 +1,275 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:get/get.dart';
+import 'package:scroll_pos/scroll_pos.dart';
+
+import '../common.dart';
+import 'model.dart';
+
+const int groupTabIndex = 4;
+const String defaultGroupTabname = 'Group';
+
+class PeerTabModel with ChangeNotifier {
+ WeakReference parent;
+ int get currentTab => _currentTab;
+ int _currentTab = 0; // index in tabNames
+ List get visibleOrderedTabs => _visibleOrderedTabs;
+ List _visibleOrderedTabs = List.empty(growable: true);
+ List get tabOrder => _tabOrder;
+ List _tabOrder = List.from([0, 1, 2, 3, 4]); // constant length
+ int get tabHiddenFlag => _tabHiddenFlag;
+ int _tabHiddenFlag = 0;
+ bool get showScrollBtn => _showScrollBtn;
+ bool _showScrollBtn = false;
+ final List _fullyVisible = List.filled(5, false);
+ bool get leftFullyVisible => _leftFullyVisible;
+ bool _leftFullyVisible = false;
+ bool get rightFullyVisible => _rightFullyVisible;
+ bool _rightFullyVisible = false;
+ ScrollPosController sc = ScrollPosController();
+ List tabNames = [
+ 'Recent Sessions',
+ 'Favorites',
+ 'Discovered',
+ 'Address Book',
+ defaultGroupTabname,
+ ];
+
+ PeerTabModel(this.parent) {
+ // init tabHiddenFlag
+ _tabHiddenFlag = int.tryParse(
+ bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
+ radix: 2) ??
+ 0;
+ var tabs = _notHiddenTabs();
+ // remove dynamic tabs
+ tabs.remove(groupTabIndex);
+ // init tabOrder
+ try {
+ final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
+ if (conf.isNotEmpty) {
+ final json = jsonDecode(conf);
+ if (json is List) {
+ final List list =
+ json.map((e) => int.tryParse(e.toString()) ?? -1).toList();
+ if (list.length == _tabOrder.length &&
+ _tabOrder.every((e) => list.contains(e))) {
+ _tabOrder = list;
+ }
+ }
+ }
+ } catch (e) {
+ debugPrintStack(label: '$e');
+ }
+ // init visibleOrderedTabs
+ var tempList = _tabOrder.toList();
+ tempList.removeWhere((e) => !tabs.contains(e));
+ _visibleOrderedTabs = tempList;
+ // init currentTab
+ _currentTab =
+ int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
+ if (!tabs.contains(_currentTab)) {
+ if (tabs.isNotEmpty) {
+ _currentTab = tabs[0];
+ } else {
+ _currentTab = 0;
+ }
+ }
+ sc.itemCount = _visibleOrderedTabs.length;
+ }
+
+ check_dynamic_tabs() {
+ var visible = visibleTabs();
+ _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList();
+ if (_visibleOrderedTabs.contains(groupTabIndex) &&
+ int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
+ groupTabIndex) {
+ _currentTab = groupTabIndex;
+ }
+ if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) {
+ tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
+ } else {
+ tabNames[groupTabIndex] = defaultGroupTabname;
+ }
+ sc.itemCount = _visibleOrderedTabs.length;
+ notifyListeners();
+ }
+
+ setCurrentTab(int index) {
+ if (_currentTab != index) {
+ _currentTab = index;
+ notifyListeners();
+ }
+ }
+
+ setTabFullyVisible(int index, bool visible) {
+ if (index >= 0 && index < _fullyVisible.length) {
+ if (visible != _fullyVisible[index]) {
+ _fullyVisible[index] = visible;
+ bool changed = false;
+ bool show = _visibleOrderedTabs.any((e) => !_fullyVisible[e]);
+ if (show != _showScrollBtn) {
+ _showScrollBtn = show;
+ changed = true;
+ }
+ if (_visibleOrderedTabs.isNotEmpty && _visibleOrderedTabs[0] == index) {
+ if (_leftFullyVisible != visible) {
+ _leftFullyVisible = visible;
+ changed = true;
+ }
+ }
+ if (_visibleOrderedTabs.isNotEmpty &&
+ _visibleOrderedTabs.last == index) {
+ if (_rightFullyVisible != visible) {
+ _rightFullyVisible = visible;
+ changed = true;
+ }
+ }
+ if (changed) {
+ notifyListeners();
+ }
+ }
+ }
+ }
+
+ onReorder(oldIndex, newIndex) {
+ if (oldIndex < newIndex) {
+ newIndex -= 1;
+ }
+ var list = _visibleOrderedTabs.toList();
+ final int item = list.removeAt(oldIndex);
+ list.insert(newIndex, item);
+ _visibleOrderedTabs = list;
+
+ var tmpTabOrder = _visibleOrderedTabs.toList();
+ var left = _tabOrder.where((e) => !tmpTabOrder.contains(e)).toList();
+ for (var t in left) {
+ _addTabInOrder(tmpTabOrder, t);
+ }
+ _tabOrder = tmpTabOrder;
+ bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder));
+ notifyListeners();
+ }
+
+ onHideShow(int index, bool show) async {
+ int bitMask = 1 << index;
+ if (show) {
+ _tabHiddenFlag &= ~bitMask;
+ } else {
+ _tabHiddenFlag |= bitMask;
+ }
+ await bind.setLocalFlutterConfig(
+ k: 'hidden-peer-card', v: _tabHiddenFlag.toRadixString(2));
+ var visible = visibleTabs();
+ _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList();
+ if (_visibleOrderedTabs.isNotEmpty &&
+ !_visibleOrderedTabs.contains(_currentTab)) {
+ _currentTab = _visibleOrderedTabs[0];
+ }
+ notifyListeners();
+ }
+
+ List orderedNotFilteredTabs() {
+ var list = tabOrder.toList();
+ if (_filterGroupCard()) {
+ list.remove(groupTabIndex);
+ }
+ return list;
+ }
+
+ // return index array of tabNames
+ List visibleTabs() {
+ var v = List.empty(growable: true);
+ for (int i = 0; i < tabNames.length; i++) {
+ if (!_isTabHidden(i) && !_isTabFilter(i)) {
+ v.add(i);
+ }
+ }
+ return v;
+ }
+
+ String translatedTabname(int index) {
+ if (index >= 0 && index < tabNames.length) {
+ final name = tabNames[index];
+ if (index == groupTabIndex) {
+ if (name == defaultGroupTabname) {
+ return translate(name);
+ } else {
+ return name;
+ }
+ } else {
+ return translate(name);
+ }
+ }
+ assert(false);
+ return index.toString();
+ }
+
+ bool _isTabHidden(int tabindex) {
+ return _tabHiddenFlag & (1 << tabindex) != 0;
+ }
+
+ bool _isTabFilter(int tabIndex) {
+ if (tabIndex == groupTabIndex) {
+ return _filterGroupCard();
+ }
+ return false;
+ }
+
+ // return true if hide group card
+ bool _filterGroupCard() {
+ if (gFFI.groupModel.users.isEmpty ||
+ (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ List _notHiddenTabs() {
+ var v = List.empty(growable: true);
+ for (int i = 0; i < tabNames.length; i++) {
+ if (!_isTabHidden(i)) {
+ v.add(i);
+ }
+ }
+ return v;
+ }
+
+ // add tabIndex to list
+ _addTabInOrder(List list, int tabIndex) {
+ if (!_tabOrder.contains(tabIndex) || list.contains(tabIndex)) {
+ return;
+ }
+ bool sameOrder = true;
+ int lastIndex = -1;
+ for (int i = 0; i < list.length; i++) {
+ var index = _tabOrder.lastIndexOf(list[i]);
+ if (index > lastIndex) {
+ lastIndex = index;
+ continue;
+ } else {
+ sameOrder = false;
+ break;
+ }
+ }
+ if (sameOrder) {
+ var indexInTabOrder = _tabOrder.indexOf(tabIndex);
+ var left = List.empty(growable: true);
+ for (int i = 0; i < indexInTabOrder; i++) {
+ left.add(_tabOrder[i]);
+ }
+ int insertIndex = list.lastIndexWhere((e) => left.contains(e));
+ if (insertIndex < 0) {
+ insertIndex = 0;
+ } else {
+ insertIndex += 1;
+ }
+ list.insert(insertIndex, tabIndex);
+ } else {
+ list.add(tabIndex);
+ }
+ }
+}
diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart
index 56dca4cdf..aab12ab5d 100644
--- a/flutter/lib/models/server_model.dart
+++ b/flutter/lib/models/server_model.dart
@@ -579,6 +579,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 +622,8 @@ class Client {
bool recording = false;
bool disconnected = false;
bool fromSwitch = false;
+ bool inVoiceCall = false;
+ bool incomingVoiceCall = false;
RxBool hasUnreadChatMessage = false.obs;
@@ -623,6 +645,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/user_model.dart b/flutter/lib/models/user_model.dart
index 6694d8c5c..7f40b3333 100644
--- a/flutter/lib/models/user_model.dart
+++ b/flutter/lib/models/user_model.dart
@@ -62,7 +62,7 @@ class UserModel {
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
- statePeerTab.check();
+ gFFI.peerTabModel.check_dynamic_tabs();
}
Future _parseAndUpdateUser(UserPayload user) async {
diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart
index 550e9ab08..3af189ef6 100644
--- a/flutter/lib/utils/multi_window_manager.dart
+++ b/flutter/lib/utils/multi_window_manager.dart
@@ -160,6 +160,24 @@ class RustDeskMultiWindowManager {
return null;
}
+ void clearWindowType(WindowType type) {
+ switch (type) {
+ case WindowType.Main:
+ return;
+ case WindowType.RemoteDesktop:
+ _remoteDesktopWindowId = null;
+ break;
+ case WindowType.FileTransfer:
+ _fileTransferWindowId = null;
+ break;
+ case WindowType.PortForward:
+ _portForwardWindowId = null;
+ break;
+ case WindowType.Unknown:
+ break;
+ }
+ }
+
void setMethodHandler(
Future Function(MethodCall call, int fromWindowId)? handler) {
DesktopMultiWindow.setMethodHandler(handler);
@@ -186,8 +204,11 @@ class RustDeskMultiWindowManager {
}
await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close();
- } on Error {
+ } catch (e) {
+ debugPrint("$e");
return;
+ } finally {
+ clearWindowType(type);
}
}
}
diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart
index 1a36fb7a5..7b60ef63c 100644
--- a/flutter/lib/utils/platform_channel.dart
+++ b/flutter/lib/utils/platform_channel.dart
@@ -31,4 +31,10 @@ class RdPlatformChannel {
return _osxMethodChannel
.invokeMethod("setWindowTheme", {"themeName": theme.name});
}
+
+ /// Terminate .app manually.
+ Future terminate() {
+ assert(Platform.isMacOS);
+ return _osxMethodChannel.invokeMethod("terminate");
+ }
}
diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj
index 7a17c3de1..066560203 100644
--- a/flutter/macos/Runner.xcodeproj/project.pbxproj
+++ b/flutter/macos/Runner.xcodeproj/project.pbxproj
@@ -227,7 +227,7 @@
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
- LastSwiftMigration = 1100;
+ LastSwiftMigration = 1420;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
@@ -463,6 +463,7 @@
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Profile;
@@ -607,6 +608,7 @@
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -643,6 +645,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
SWIFT_VERSION = 5.0;
};
diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift
index 5708e35cb..3498decd3 100644
--- a/flutter/macos/Runner/AppDelegate.swift
+++ b/flutter/macos/Runner/AppDelegate.swift
@@ -3,21 +3,22 @@ import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
- var lauched = false;
+ var launched = false;
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
dummy_method_to_enforce_bundling()
- return true
+ // https://github.com/leanflutter/window_manager/issues/214
+ return false
}
override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
- if (lauched) {
+ if (launched) {
handle_applicationShouldOpenUntitledFile();
}
return true
}
override func applicationDidFinishLaunching(_ aNotification: Notification) {
- lauched = true;
+ launched = true;
NSApplication.shared.activate(ignoringOtherApps: true);
}
}
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index 7b4d860d6..682280dc5 100644
--- a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,68 +1,68 @@
{
- "images": [
- {
- "filename": "app_icon_16.png",
- "idiom": "mac",
- "scale": "1x",
- "size": "16x16"
+ "info": {
+ "author": "icons_launcher",
+ "version": 1
},
- {
- "filename": "app_icon_32.png",
- "idiom": "mac",
- "scale": "2x",
- "size": "16x16"
- },
- {
- "filename": "app_icon_32.png",
- "idiom": "mac",
- "scale": "1x",
- "size": "32x32"
- },
- {
- "filename": "app_icon_64.png",
- "idiom": "mac",
- "scale": "2x",
- "size": "32x32"
- },
- {
- "filename": "app_icon_128.png",
- "idiom": "mac",
- "scale": "1x",
- "size": "128x128"
- },
- {
- "filename": "app_icon_256.png",
- "idiom": "mac",
- "scale": "2x",
- "size": "128x128"
- },
- {
- "filename": "app_icon_256.png",
- "idiom": "mac",
- "scale": "1x",
- "size": "256x256"
- },
- {
- "filename": "app_icon_512.png",
- "idiom": "mac",
- "scale": "2x",
- "size": "256x256"
- },
- {
- "filename": "app_icon_512.png",
- "idiom": "mac",
- "scale": "1x",
- "size": "512x512"
- },
- {
- "filename": "app_icon_1024.png",
- "idiom": "mac",
- "scale": "2x",
- "size": "512x512"
- }
- ],
- "info": {
- "author": "icons_launcher",
- "version": 1
- }
+ "images": [
+ {
+ "size": "16x16",
+ "idiom": "mac",
+ "filename": "app_icon_16.png",
+ "scale": "1x"
+ },
+ {
+ "size": "16x16",
+ "idiom": "mac",
+ "filename": "app_icon_32.png",
+ "scale": "2x"
+ },
+ {
+ "size": "32x32",
+ "idiom": "mac",
+ "filename": "app_icon_32.png",
+ "scale": "1x"
+ },
+ {
+ "size": "32x32",
+ "idiom": "mac",
+ "filename": "app_icon_64.png",
+ "scale": "2x"
+ },
+ {
+ "size": "128x128",
+ "idiom": "mac",
+ "filename": "app_icon_128.png",
+ "scale": "1x"
+ },
+ {
+ "size": "128x128",
+ "idiom": "mac",
+ "filename": "app_icon_256.png",
+ "scale": "2x"
+ },
+ {
+ "size": "256x256",
+ "idiom": "mac",
+ "filename": "app_icon_256.png",
+ "scale": "1x"
+ },
+ {
+ "size": "256x256",
+ "idiom": "mac",
+ "filename": "app_icon_512.png",
+ "scale": "2x"
+ },
+ {
+ "size": "512x512",
+ "idiom": "mac",
+ "filename": "app_icon_512.png",
+ "scale": "1x"
+ },
+ {
+ "size": "512x512",
+ "idiom": "mac",
+ "filename": "app_icon_1024.png",
+ "scale": "2x"
+ }
+ ]
}
\ No newline at end of file
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 1c6cf008a..9af6f2121 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 8c2837a94..493ec7076 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 77b76503e..4bed6f3fa 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 ac1b10766..22893b8ea 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 9e593fcbd..583a48571 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 1205d915e..d3aa91800 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 76d846c4a..f98ccf1f3 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 d1077e0e4..96616e8c4 100644
--- a/flutter/macos/Runner/Info.plist
+++ b/flutter/macos/Runner/Info.plist
@@ -23,8 +23,10 @@
CFBundleTypeRole
Editor
- CFBundleURLName
+ CFBundleURLIconFile
+ CFBundleURLName
+ com.carriez.rustdesk
CFBundleURLSchemes
rustdesk
@@ -35,13 +37,15 @@
$(FLUTTER_BUILD_NUMBER)
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
+ LSUIElement
+ 1
NSHumanReadableCopyright
$(PRODUCT_COPYRIGHT)
NSMainNibFile
MainMenu
+ NSMicrophoneUsageDescription
+ Record the sound from microphone for the purpose of the remote desktop.
NSPrincipalClass
- NSApplication
- LSUIElement
- 1
+ NSApplication
diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift
index cea1e94bb..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
@@ -78,10 +79,29 @@ class MainFlutterWindow: NSWindow {
self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light")
result(nil)
break;
+ 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 c193c0651..cd618dfc4 100644
--- a/flutter/pubspec.lock
+++ b/flutter/pubspec.lock
@@ -475,10 +475,10 @@ packages:
dependency: "direct main"
description:
name: flutter_custom_cursor
- sha256: "6c5204cf6a16650355b8aa47a8402e79922c07641390a32021a1069b561909ec"
+ sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d"
url: "https://pub.dev"
source: hosted
- version: "0.0.3"
+ version: "0.0.4"
flutter_improved_scrolling:
dependency: "direct main"
description:
@@ -488,6 +488,14 @@ packages:
url: "https://github.com/Kingtous/flutter_improved_scrolling"
source: git
version: "0.0.3"
+ flutter_launcher_icons:
+ dependency: "direct main"
+ description:
+ name: flutter_launcher_icons
+ sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.0"
flutter_lints:
dependency: "direct dev"
description:
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index 95449e611..8701d9f5b 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -90,6 +90,7 @@ dependencies:
bot_toast: ^4.0.3
win32: any
password_strength: ^0.2.0
+ flutter_launcher_icons: ^0.11.0
dev_dependencies:
@@ -101,21 +102,21 @@ dev_dependencies:
flutter_lints: ^2.0.0
ffigen: ^7.2.4
-# rerun: flutter pub run flutter_launcher_icons:main
-icons_launcher:
+# rerun: flutter pub run flutter_launcher_icons
+flutter_icons:
image_path: "../res/icon.png"
- platforms:
- android:
- enable: true
- ios:
- enable: true
- windows:
- enable: true
- macos:
- enable: true
- image_path: "../res/mac-icon.png"
- linux:
- enable: true
+ remove_alpha_ios: true
+ android: true
+ ios: true
+ windows:
+ generate: true
+ macos:
+ image_path: "../res/mac-icon.png"
+ generate: true
+ linux: true
+ web:
+ generate: true
+
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png
index 5d4566850..e8c754f4a 100644
Binary files a/flutter/web/icons/Icon-192.png and b/flutter/web/icons/Icon-192.png differ
diff --git a/flutter/web/icons/Icon-512.png b/flutter/web/icons/Icon-512.png
index 2b1abc3f2..2f8929e26 100644
Binary files a/flutter/web/icons/Icon-512.png and b/flutter/web/icons/Icon-512.png differ
diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png
index 30147e96e..e8c754f4a 100644
Binary files a/flutter/web/icons/Icon-maskable-192.png and b/flutter/web/icons/Icon-maskable-192.png differ
diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png
index e84ca5bc7..2f8929e26 100644
Binary files a/flutter/web/icons/Icon-maskable-512.png and b/flutter/web/icons/Icon-maskable-512.png differ
diff --git a/flutter/web/manifest.json b/flutter/web/manifest.json
index 9723be242..77d831d4f 100644
--- a/flutter/web/manifest.json
+++ b/flutter/web/manifest.json
@@ -32,4 +32,4 @@
"purpose": "maskable"
}
]
-}
+}
\ No newline at end of file
diff --git a/flutter/windows/runner/resources/app_icon.ico b/flutter/windows/runner/resources/app_icon.ico
index 9b52c497e..bdf42cfdb 100644
Binary files a/flutter/windows/runner/resources/app_icon.ico and b/flutter/windows/runner/resources/app_icon.ico differ
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/protos/message.proto b/libs/hbb_common/protos/message.proto
index b7965f237..ed2706382 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -598,6 +598,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 +632,7 @@ message Message {
Cliprdr cliprdr = 20;
MessageBox message_box = 21;
SwitchSidesResponse switch_sides_response = 22;
+ VoiceCallRequest voice_call_request = 23;
+ VoiceCallResponse voice_call_response = 24;
}
}
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..1e4d80c9f 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -288,7 +288,7 @@ fn patch(path: PathBuf) -> PathBuf {
.trim()
.to_owned();
if user != "root" {
- return format!("/home/{}", user).into();
+ return format!("/home/{user}").into();
}
}
}
@@ -525,7 +525,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 +562,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..1c49adfb7 100644
--- a/libs/hbb_common/src/lib.rs
+++ b/libs/hbb_common/src/lib.rs
@@ -211,11 +211,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 +338,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..7c107d11c 100644
--- a/libs/hbb_common/src/platform/linux.rs
+++ b/libs/hbb_common/src/platform/linux.rs
@@ -60,7 +60,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() != "" {
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/res/128x128.png b/res/128x128.png
deleted file mode 100644
index 26cbf702c..000000000
Binary files a/res/128x128.png and /dev/null differ
diff --git a/res/128x128.png b/res/128x128.png
new file mode 120000
index 000000000..f69b60eb2
--- /dev/null
+++ b/res/128x128.png
@@ -0,0 +1 @@
+../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
\ No newline at end of file
diff --git a/res/128x128@2x.png b/res/128x128@2x.png
index d6f8d20fa..9bccf65bb 100644
Binary files a/res/128x128@2x.png and b/res/128x128@2x.png differ
diff --git a/res/32x32.png b/res/32x32.png
deleted file mode 100644
index 33dc80537..000000000
Binary files a/res/32x32.png and /dev/null differ
diff --git a/res/32x32.png b/res/32x32.png
new file mode 120000
index 000000000..7c1136a73
--- /dev/null
+++ b/res/32x32.png
@@ -0,0 +1 @@
+../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
\ No newline at end of file
diff --git a/res/64x64.png b/res/64x64.png
deleted file mode 100644
index d93638e6e..000000000
Binary files a/res/64x64.png and /dev/null differ
diff --git a/res/64x64.png b/res/64x64.png
new file mode 120000
index 000000000..45d1cf759
--- /dev/null
+++ b/res/64x64.png
@@ -0,0 +1 @@
+../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
\ No newline at end of file
diff --git a/res/design.svg b/res/design.svg
new file mode 100644
index 000000000..62e568242
--- /dev/null
+++ b/res/design.svg
@@ -0,0 +1,374 @@
+
+
diff --git a/res/icon-margin.png b/res/icon-margin.png
deleted file mode 100644
index 6449ec490..000000000
Binary files a/res/icon-margin.png and /dev/null differ
diff --git a/res/icon.ico b/res/icon.ico
deleted file mode 100644
index 41c02bb4e..000000000
Binary files a/res/icon.ico and /dev/null differ
diff --git a/res/icon.ico b/res/icon.ico
new file mode 120000
index 000000000..75324b38c
--- /dev/null
+++ b/res/icon.ico
@@ -0,0 +1 @@
+../flutter/windows/runner/resources/app_icon.ico
\ No newline at end of file
diff --git a/res/icon.png b/res/icon.png
index 823967c49..2575d80e7 100644
Binary files a/res/icon.png and b/res/icon.png differ
diff --git a/res/logo-header.svg b/res/logo-header.svg
index 40c19c43c..9712636bf 100644
--- a/res/logo-header.svg
+++ b/res/logo-header.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/res/mac-icon.png b/res/mac-icon.png
index a9813152e..b6e08923f 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 860f9fcf5..bdd48ad15 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
index ba8ed8c12..a98fe63b0 100644
Binary files a/res/mac-tray-dark.png and b/res/mac-tray-dark.png differ
diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png
index f723d980e..253450ecb 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
index ad8bfa396..b827e462f 100644
Binary files a/res/mac-tray-light.png and b/res/mac-tray-light.png differ
diff --git a/res/tray-icon.ico b/res/tray-icon.ico
index fd2e61628..df8bdaccb 100644
Binary files a/res/tray-icon.ico and b/res/tray-icon.ico differ
diff --git a/src/client.rs b/src/client.rs
index fb42ce840..020bea1f0 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,58 +1,61 @@
-pub use async_trait::async_trait;
-use bytes::Bytes;
-#[cfg(not(any(target_os = "android", target_os = "linux")))]
-use cpal::{
- traits::{DeviceTrait, HostTrait, StreamTrait},
- Device, Host, StreamConfig,
-};
-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},
+ sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock},
};
+
+pub use async_trait::async_trait;
+use bytes::Bytes;
+#[cfg(not(any(target_os = "android", target_os = "linux")))]
+use cpal::{
+ Device,
+ Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait},
+};
+use magnum_opus::{Channels::*, Decoder as AudioDecoder};
+use sha2::{Digest, Sha256};
use uuid::Uuid;
pub use file_trait::FileManager;
use hbb_common::{
+ AddrMangle,
allow_err,
anyhow::{anyhow, Context},
bail,
config::{
- Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT,
+ Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT,
RENDEZVOUS_TIMEOUT,
- },
- get_version_number, log,
- message_proto::{option_message::BoolOption, *},
+ }, get_version_number,
+ log,
+ message_proto::{*, option_message::BoolOption},
protobuf::Message as _,
rand,
rendezvous_proto::*,
+ ResultType,
socket_client,
sodiumoxide::crypto::{box_, secretbox, sign},
- timeout,
- tokio::time::Duration,
- AddrMangle, ResultType, Stream,
+ Stream, timeout, tokio::time::Duration,
};
-pub use helper::LatencyController;
pub use helper::*;
+pub use helper::LatencyController;
use scrap::{
codec::{Decoder, DecoderCfg},
record::{Recorder, RecorderContext},
VpxDecoderConfig, VpxVideoCodecId,
};
+use crate::{
+ common::{self, is_keyboard_mode_supported},
+ server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED},
+};
+
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);
@@ -714,6 +717,7 @@ impl AudioHandler {
.check_audio(frame.timestamp)
.not()
{
+ log::debug!("audio frame {} is ignored", frame.timestamp);
return;
}
}
@@ -724,6 +728,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")]
@@ -1117,8 +1122,12 @@ impl LoginConfigHandler {
} else if name == "show-quality-monitor" {
config.show_quality_monitor.v = !config.show_quality_monitor.v;
} else {
- let v = self.options.get(&name).is_some();
- if v {
+ let is_set = self
+ .options
+ .get(&name)
+ .map(|o| !o.is_empty())
+ .unwrap_or(false);
+ if is_set {
self.config.options.remove(&name);
} else {
self.config.options.insert(name, "Y".to_owned());
@@ -1539,7 +1548,6 @@ where
F: 'static + FnMut(&[u8]) + 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();
@@ -1569,8 +1577,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 {
@@ -1578,6 +1597,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);
}
_ => {}
@@ -1588,7 +1608,7 @@ where
}
log::info!("Audio decoder loop exits");
});
- return (video_sender, audio_sender);
+ audio_sender
}
/// Handle latency test.
@@ -1930,6 +1950,8 @@ pub enum Data {
RecordScreen(bool, i32, i32, String),
ElevateDirect,
ElevateWithLogon(String, String),
+ NewVoiceCall,
+ CloseVoiceCall,
}
/// Keycode for key events.
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..5186aff4d 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,20 @@ 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, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED,
+ SERVER_KEYBOARD_ENABLED,
+};
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
+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 +42,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,6 +86,8 @@ 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,
}
}
@@ -93,6 +100,7 @@ impl Remote {
} else {
ConnType::default()
};
+
match Client::start(
&self.handler.id,
key,
@@ -212,6 +220,10 @@ impl Remote {
}
}
log::debug!("Exit io_loop of id={}", self.handler.id);
+ // Stop client audio server.
+ if let Some(s) = self.stop_voice_call_sender.take() {
+ s.send(()).ok();
+ }
}
Err(err) => {
self.handler
@@ -253,6 +265,81 @@ impl Remote {
}
}
+ fn stop_voice_call(&mut self) {
+ let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None);
+ if let Some(stopper) = voice_call_sender {
+ let _ = stopper.send(());
+ }
+ }
+
+ // Start a voice call recorder, records audio and send to remote
+ fn start_voice_call(&mut self) -> Option> {
+ if self.handler.is_file_transfer() || self.handler.is_port_forward() {
+ return None;
+ }
+ // Switch to default input device
+ let default_sound_device = get_default_sound_input();
+ if let Some(device) = default_sound_device {
+ set_sound_input(device);
+ }
+ // Create a channel to receive error or closed message
+ let (tx, rx) = std::sync::mpsc::channel();
+ let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel();
+ // Create a stand-alone inner, add subscribe to audio service
+ let conn_id = CLIENT_SERVER.write().unwrap().get_new_id();
+ let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None);
+ // now we subscribe
+ CLIENT_SERVER.write().unwrap().subscribe(
+ audio_service::NAME,
+ client_conn_inner.clone(),
+ true,
+ );
+ let tx_audio = self.sender.clone();
+ std::thread::spawn(move || {
+ loop {
+ // check if client is closed
+ match rx.try_recv() {
+ Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
+ log::debug!("Exit voice call audio service of client");
+ // unsubscribe
+ CLIENT_SERVER.write().unwrap().subscribe(
+ audio_service::NAME,
+ client_conn_inner,
+ false,
+ );
+ break;
+ }
+ _ => {}
+ }
+ match rx_audio_data.try_recv() {
+ Ok((_instant, msg)) => match &msg.union {
+ Some(message::Union::AudioFrame(frame)) => {
+ let mut msg = Message::new();
+ msg.set_audio_frame(frame.clone());
+ tx_audio.send(Data::Message(msg)).ok();
+ log::debug!("send audio frame {}", frame.timestamp);
+ }
+ Some(message::Union::Misc(misc)) => {
+ let mut msg = Message::new();
+ msg.set_misc(misc.clone());
+ tx_audio.send(Data::Message(msg)).ok();
+ log::debug!("send audio misc {:?}", misc.audio_format());
+ }
+ _ => {}
+ },
+ Err(err) => {
+ if err == TryRecvError::Empty {
+ // ignore
+ } else {
+ log::debug!("Failed to record local audio channel: {}", err);
+ }
+ }
+ }
+ }
+ });
+ Some(tx)
+ }
+
fn start_clipboard(&mut self) -> Option