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 @@

RustDesk - Your remote desktop
- تصاویر محیط نرم‌افزار • + تصاویر محیط نرم‌افزارساختارداکرساخت • @@ -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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 @@ + +rustdesk 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 @@ -RUSTDESKYour remote desktop \ 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> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; @@ -654,6 +741,22 @@ impl Remote { msg.set_misc(misc); allow_err!(peer.send(&msg).await); } + Data::NewVoiceCall => { + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + allow_err!(peer.send(&msg).await); + self.handler.on_voice_call_waiting(); + } + Data::CloseVoiceCall => { + self.stop_voice_call(); + let msg = new_voice_call_request(false); + self.handler.on_voice_call_closed("Closed manually by the peer"); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -1146,6 +1249,34 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + // TODO: maybe we will do a voice call from the peer in the future. + } else { + log::debug!("The remote has requested to close the voice call"); + if let Some(sender) = self.stop_voice_call_sender.take() { + allow_err!(sender.send(())); + self.handler.on_voice_call_closed(""); + } + } + } + Some(message::Union::VoiceCallResponse(response)) => { + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepted the voice call. + self.handler.on_voice_call_started(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_closed(""); + } + } + } + } _ => {} } } diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..79a4664db 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,6 +30,8 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use crate::ui_interface::{get_option, set_option}; + pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -105,6 +107,54 @@ pub fn check_clipboard( None } +/// Set sound input device. +pub fn set_sound_input(device: String) { + let prior_device = get_option("audio-input".to_owned()); + if prior_device != device { + log::info!("switch to audio input device {}", device); + std::thread::spawn(move || { + set_option("audio-input".to_owned(), device); + }); + } else { + log::info!("audio input is already set to {}", device); + } +} + +/// Get system's default sound input device name. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_default_sound_input() -> Option { + #[cfg(not(target_os = "linux"))] + { + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let dev = host.default_input_device(); + return if let Some(dev) = dev { + match dev.name() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + #[cfg(target_os = "linux")] + { + let input = crate::platform::linux::get_default_pa_source(); + return if let Some(input) = input { + Some(input.1) + } else { + None + }; + } +} + +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn get_default_sound_input() -> Option { + None +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { @@ -671,8 +721,8 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: match keyboard_mode { KeyboardMode::Legacy => true, KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), - KeyboardMode::Translate => false, - KeyboardMode::Auto => false, + KeyboardMode::Translate => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Auto => version_number >= hbb_common::get_version_number("1.2.0"), } } @@ -712,8 +762,3 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } - -#[cfg(test)] -mod test_common { - use super::*; -} diff --git a/src/core_main.rs b/src/core_main.rs index 89a962f1d..0af7026e9 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -244,8 +244,6 @@ pub fn core_main() -> Option> { } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel - #[cfg(feature = "flutter")] - crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); } } @@ -346,5 +344,11 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option bool { #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { hbb_common::log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" { crate::platform::macos::check_main_window(); } } @@ -392,6 +394,22 @@ impl InvokeUiSession for FlutterHandler { fn switch_back(&self, peer_id: &str) { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + + fn on_voice_call_started(&self) { + self.push_event("on_voice_call_started", [].into()); + } + + fn on_voice_call_closed(&self, reason: &str) { + self.push_event("on_voice_call_closed", [("reason", reason)].into()) + } + + fn on_voice_call_waiting(&self) { + self.push_event("on_voice_call_waiting", [].into()); + } + + fn on_voice_call_incoming(&self) { + self.push_event("on_voice_call_incoming", [].into()); + } } /// Create a new remote session with the given id. @@ -519,6 +537,11 @@ pub mod connection_manager { fn show_elevation(&self, show: bool) { self.push_event("show_elevation", vec![("show", &show.to_string())]); } + + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + self.push_event("update_voice_call_state", vec![("client", &client_json)]); + } } impl FlutterHandler { @@ -526,13 +549,11 @@ pub mod connection_manager { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - - if let Some(s) = GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(super::APP_TYPE_MAIN) - { + + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + } else { + println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM); }; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d40c66d19..ec4a90973 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,30 +1,30 @@ -use std::{ - collections::HashMap, - ffi::{CStr, CString}, - os::raw::c_char, -}; +use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; +use std::str::FromStr; + +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::thread; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use crate::common::is_keyboard_mode_supported; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use hbb_common::{ - config::{self, LocalConfig, PeerConfig, ONLINE}, + config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; -use std::str::FromStr; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; -// use crate::hbbs_http::account::AuthResult; - -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, flutter::{session_add, session_start_}, }; +use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; +use crate::flutter::{self, SESSIONS}; +use crate::ui_interface::{self, *}; + +// use crate::hbbs_http::account::AuthResult; + fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); #[cfg(target_os = "android")] @@ -523,6 +523,13 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_default_sound_input() -> Option { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_default_sound_input(); + #[cfg(any(target_os = "android", target_os = "ios"))] + None +} + pub fn main_get_hostname() -> SyncReturn { SyncReturn(crate::common::hostname()) } @@ -822,6 +829,26 @@ pub fn session_new_rdp(id: String) { } } +pub fn session_request_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.request_voice_call(); + } +} + +pub fn session_close_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.close_voice_call(); + } +} + +pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { + crate::ui_cm_interface::handle_incoming_voice_call(id, accept); +} + +pub fn cm_close_voice_call(id: i32) { + crate::ui_cm_interface::close_voice_call(id); +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } @@ -904,7 +931,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - std::thread::spawn(|| { + thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1249,22 +1276,49 @@ pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) } +pub fn main_start_pa() { + #[cfg(target_os = "linux")] + thread::spawn(crate::ipc::start_pa); +} + pub fn main_hide_docker() -> SyncReturn { #[cfg(target_os = "macos")] crate::platform::macos::hide_dock(); SyncReturn(true) } +pub fn cm_start_listen_ipc_thread() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::connection_manager::start_listen_ipc_thread(); +} + +/// Start an ipc server for receiving the url scheme. +/// +/// * Should only be called in the main flutter window. +/// * macOS only +pub fn main_start_ipc_url_server() { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::server::start_ipc_url_server()); +} + +/// Send a url scheme throught the ipc. +/// +/// * macOS only +#[allow(unused_variables)] +pub fn send_url_scheme(_url: String) { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); +} + #[cfg(target_os = "android")] pub mod server_side { + use hbb_common::log; use jni::{ + JNIEnv, objects::{JClass, JString}, sys::jstring, - JNIEnv, }; - use hbb_common::log; - use crate::start_server; #[no_mangle] @@ -1273,7 +1327,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - std::thread::spawn(move || start_server(true)); + thread::spawn(move || start_server(true)); } #[no_mangle] diff --git a/src/ipc.rs b/src/ipc.rs index d4d803aec..699b0bcd7 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -190,7 +190,7 @@ pub enum Data { Socks(Option), FS(FS), Test, - SyncConfig(Option<(Config, Config2)>), + SyncConfig(Option>), #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), @@ -210,6 +210,11 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, + UrlLink(String), + VoiceCallIncoming, + StartVoiceCall, + VoiceCallResponse(bool), + CloseVoiceCall(String), } #[tokio::main(flavor = "current_thread")] @@ -414,7 +419,8 @@ async fn handle(data: Data, stream: &mut Connection) { let t = Config::get_nat_type(); allow_err!(stream.send(&Data::NatType(Some(t))).await); } - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = CheckIfRestart::new(); Config::set(config); Config2::set(config2); @@ -423,7 +429,9 @@ async fn handle(data: Data, stream: &mut Connection) { Data::SyncConfig(None) => { allow_err!( stream - .send(&Data::SyncConfig(Some((Config::get(), Config2::get())))) + .send(&Data::SyncConfig(Some( + (Config::get(), Config2::get()).into() + ))) .await ); } @@ -832,3 +840,22 @@ pub async fn test_rendezvous_server() -> ResultType<()> { c.send(&Data::TestRendezvousServer).await?; Ok(()) } + +#[tokio::main(flavor = "current_thread")] +pub async fn send_url_scheme(url: String) -> ResultType<()> { + connect(1_000, "_url") + .await? + .send(&Data::UrlLink(url)) + .await?; + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn verify_ffi_enum_data_size() { + println!("{}", std::mem::size_of::()); + assert!(std::mem::size_of::() < 96); + } +} diff --git a/src/keyboard.rs b/src/keyboard.rs index 054a39580..5b9920714 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -88,13 +88,17 @@ pub mod client { } } - pub fn process_event(event: &Event, lock_modes: Option) { + pub fn process_event(event: &Event, lock_modes: Option) -> KeyboardMode { + let keyboard_mode = get_keyboard_mode_enum(); + if is_long_press(&event) { - return; + return keyboard_mode; } - if let Some(key_event) = event_to_key_event(&event, lock_modes) { + + for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { send_key_event(&key_event); } + keyboard_mode } pub fn get_modifiers_state( @@ -189,12 +193,15 @@ pub mod client { #[cfg(windows)] pub fn update_grab_get_key_name() { match get_keyboard_mode_enum() { - KeyboardMode::Map => rdev::set_get_key_name(false), - KeyboardMode::Translate => rdev::set_get_key_name(true), + KeyboardMode::Map => rdev::set_get_key_unicode(false), + KeyboardMode::Translate => rdev::set_get_key_unicode(true), _ => {} }; } +#[cfg(target_os = "windows")] +static mut IS_0X021D_DOWN: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -203,16 +210,43 @@ pub fn start_grab_loop() { if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event, None); + + let mut _keyboard_mode = KeyboardMode::Map; + let _scan_code = event.scan_code; + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + _keyboard_mode = client::process_event(&event, None); if is_press { - return None; + None } else { - return Some(event); + Some(event) } } else { - return Some(event); + Some(event) + }; + + #[cfg(target_os = "windows")] + match _scan_code { + 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), + 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + // Right Alt + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} } + + #[cfg(target_os = "windows")] + unsafe { + // AltGr + if _scan_code == 0x021D { + IS_0X021D_DOWN = is_press; + } + } + + return res; }; let func = move |event: Event| match event.event_type { EventType::KeyPress(key) => try_handle_keyboard(event, key, true), @@ -222,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -341,7 +376,11 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { +pub fn event_to_key_events( + event: &Event, + keyboard_mode: KeyboardMode, + lock_modes: Option, +) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -355,37 +394,44 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option {} } - let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); - let mut key_event = match keyboard_mode { - KeyboardMode::Map => map_keyboard_mode(event, key_event)?, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, + let mut key_events = match keyboard_mode { + KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + Some(event) => [event].to_vec(), + None => Vec::new(), + }, + KeyboardMode::Translate => translate_keyboard_mode(event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { - legacy_keyboard_mode(event, key_event)? + legacy_keyboard_mode(event, key_event) } #[cfg(any(target_os = "android", target_os = "ios"))] { - None? + Vec::new() } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(lock_modes) = lock_modes { - add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); - } else { - add_numlock_capslock_status(&mut key_event); + + if keyboard_mode != KeyboardMode::Translate { + for key_event in &mut key_events { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(key_event, lock_modes); + } else { + add_numlock_capslock_status(key_event); + } + } } - return Some(key_event); + key_events } pub fn event_type_to_event(event_type: EventType) -> Event { Event { event_type, time: SystemTime::now(), - name: None, + unicode: None, code: 0, scan_code: 0, } @@ -423,13 +469,14 @@ pub fn get_peer_platform() -> String { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { + let mut events = Vec::new(); // legacy mode(0): Generate characters locally, look for keycode on other side. let (mut key, down_or_up) = match event.event_type { EventType::KeyPress(key) => (key, true), EventType::KeyRelease(key) => (key, false), _ => { - return None; + return events; } }; @@ -475,7 +522,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { if is_win && ctrl && alt { client::ctrl_alt_del(); - return None; + return events; } Some(ControlKey::Delete) } @@ -545,7 +592,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Some(ControlKey::Subtract), Key::KpPlus => Some(ControlKey::Add), Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return None; + return events; } Key::Home => Some(ControlKey::Home), Key::End => Some(ControlKey::End), @@ -558,7 +605,11 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { if s.len() <= 2 { // exclude chinese characters @@ -628,12 +679,12 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option Option { @@ -703,6 +755,98 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { - None +fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec) { + match &event.unicode { + Some(unicode_info) => { + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); + } + } + None => {} + } +} + +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down() -> bool { + if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { + return true; + } + if rdev::get_modifier(Key::Alt) || rdev::get_modifier(Key::AltGr) { + return true; + } + if rdev::get_modifier(Key::MetaLeft) || rdev::get_modifier(Key::MetaRight) { + return true; + } + return false; +} + +pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Option { + match event.event_type { + EventType::KeyPress(..) => { + key_event.down = true; + } + EventType::KeyRelease(..) => { + key_event.down = false; + } + _ => return None, + }; + + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + // #[cfg(target_os = "windows")] + // let keycode = match peer.as_str() { + // "windows" => event.code, + // "macos" => { + // if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + // rdev::win_scancode_to_macos_iso_code(event.scan_code)? + // } else { + // rdev::win_scancode_to_macos_code(event.scan_code)? + // } + // } + // _ => rdev::win_scancode_to_linux_code(event.scan_code)?, + // }; + + key_event.set_chr(event.code as _); + Some(key_event) +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + let mut events: Vec = Vec::new(); + if let Some(unicode_info) = &event.unicode { + if unicode_info.is_dead { + return events; + } + } + + #[cfg(target_os = "windows")] + unsafe { + if event.scan_code == 0x021D { + return events; + } + + if IS_0X021D_DOWN { + if event.scan_code == 0xE038 { + return events; + } + } + } + + #[cfg(target_os = "windows")] + if unsafe { IS_0X021D_DOWN } || !is_hot_key_modifiers_down() { + try_fill_unicode(event, &key_event, &mut events); + } + + #[cfg(not(target_os = "windows"))] + try_fill_unicode(event, &key_event, &mut events); + + if events.is_empty() { + if let Some(evt) = translate_virtual_keycode(event, key_event) { + events.push(evt); + } + return events; + } + events } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f2210f971..e98c6636a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 00d62946f..64c37709a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), ("Wait", "等待"), ("Elevation Error", "提权失败"), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), + ("Voice call", "语音通话"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止语音聊天"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 453ecefb3..70a3eb6c7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index dcaeb3eaa..ae943e1e8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 5b68c0e7a..44bbafdac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -200,7 +200,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Warnung"), ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), ("Reboot required", "Neustart erforderlich"), - ("Unsupported display server ", "Nicht unterstützter Display-Server"), + ("Unsupported display server ", "Nicht unterstützter Anzeigeserver"), ("x11 expected", "X11 erwartet"), ("Port", "Port"), ("Settings", "Einstellungen"), @@ -327,7 +327,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mobile Actions", "Mobile Aktionen"), ("Select Monitor", "Bildschirm auswählen"), ("Control Actions", "Aktionen"), - ("Display Settings", "Bildschirmeinstellungen"), + ("Display Settings", "Anzeigeeinstellungen"), ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), ("Scroll Style", "Scroll-Stil"), @@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), ("Scale original", "Keine Skalierung"), - ("Scale adaptive", "Automatische Skalierung"), + ("Scale adaptive", "Anpassbare Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), ("Theme", "Farbgebung"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clear", "Zurücksetzen"), ("Audio Input Device", "Audioeingabegerät"), ("Deny remote access", "Fernzugriff verbieten"), - ("Use IP Whitelisting", "IP-Whitelist benutzen"), + ("Use IP Whitelisting", "IP-Whitelist verwenden"), ("Network", "Netzwerk"), ("Enable RDP", "RDP aktivieren"), ("Pin menubar", "Menüleiste anpinnen"), @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), + ("config_microphone", ""), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), @@ -436,14 +437,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "Seiten wechseln"), ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, ob Sie Ihren Desktop freigeben möchten."), ("Closed as expected", "Wie erwartet geschlossen"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Anzeige"), + ("Default View Style", "Standard-Ansichtsstil"), + ("Default Scroll Style", "Standard-Scroll-Stil"), + ("Default Image Quality", "Standard-Bildqualität"), + ("Default Codec", "Standard-Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "fps"), + ("Auto", "Automatisch"), + ("Other Default Options", "Weitere Standardoptionen"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 6eed43a77..37c08a974 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -41,6 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), - ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk.") + ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0c7f13d7e..f457833f8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5fdb7ee2c..220447454 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), ("Always use software rendering", "Usar siempre renderizado por software"), ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), + ("config_microphone", ""), ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), ("Wait", "Esperar"), ("Elevation Error", "Error de elevación"), @@ -435,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", "Cerrado como se esperaba"), + ("Closed as expected", ""), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 72cde49f9..c206f91ff 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("config_microphone", ""), ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), ("Wait", "صبر کنید"), ("Elevation Error", "خطای ارتفاع"), @@ -436,14 +437,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "طرفین را عوض کنید"), ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ("Closed as expected", "طبق انتظار بسته شد"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "نمایش دادن"), + ("Default View Style", "سبک نمایش پیش فرض"), + ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Image Quality", "کیفیت تصویر پیش فرض"), + ("Default Codec", "کدک پیش فرض"), + ("Bitrate", "میزان بیت صفحه نمایش"), + ("FPS", "FPS"), + ("Auto", "خودکار"), + ("Other Default Options", "سایر گزینه های پیش فرض"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 19b932d2f..39ee3bc7f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -63,7 +63,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Ignorer"), ("Close", "Fermer"), ("Retry", "Réessayer"), - ("OK", "Confirmer"), + ("OK", "Valider"), ("Password Required", "Mot de passe requis"), ("Please enter your password", "Veuillez saisir votre mot de passe"), ("Remember password", "Mémoriser le mot de passe"), @@ -126,7 +126,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), - ("Insert", "Insérer"), + ("Insert", "Envoyer"), ("Insert Lock", "Verrouiller l'ordinateur distant"), ("Refresh", "Rafraîchir l'écran"), ("ID does not exist", "L'ID n'existe pas"), @@ -291,7 +291,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Overwrite", "Écraser"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_mac_permission", "https://rustdesk.com/docs/fr/manual/mac/#enable-permissions"), ("Help", "Aider"), ("Failed", "échouer"), ("Succeeded", "Succès"), @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("config_microphone", ""), ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), ("Wait", "En cours"), ("Elevation Error", "Erreur d'augmentation des privilèges"), @@ -435,15 +436,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fort"), ("Switch Sides", "Inverser la prise de contrôle"), ("Please confirm if you want to share your desktop?", "Veuillez confirmer le partager de votre bureau ?"), - ("Closed as expected", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Closed as expected", "Fermé normalement"), + ("Display", "Affichage"), + ("Default View Style", "Style de vue par défaut"), + ("Default Scroll Style", "Style de défilement par défaut"), + ("Default Image Quality", "Qualité d'image par défaut"), + ("Default Codec", "Codec par défaut"), + ("Bitrate", "Débit"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Autres options par défaut"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index bc25ab6c6..7cb678ecc 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), + ("config_microphone", ""), ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), ("Wait", "Περιμένετε"), ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 49ce8f140..25562f556 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 0fa6e0293..68a80e540 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 6edd4a461..a4ea58304 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), + ("config_microphone", ""), ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), @@ -436,14 +437,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "Cambia lato"), ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), ("Closed as expected", "Chiuso come previsto"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Visualizzazione"), + ("Default View Style", "Stile Visualizzazione Predefinito"), + ("Default Scroll Style", "Stile Scorrimento Predefinito"), + ("Default Image Quality", "Qualità Immagine Predefinita"), + ("Default Codec", "Codec Predefinito"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Altre Opzioni Predefinite"), + ("Voice call", "Chiamata vocale"), + ("Text chat", "Chat testuale"), + ("Stop voice call", "Interrompi la chiamata vocale"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 35e20d7fd..7069c0daf 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d03b07992..43eb552d3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 2006c67d1..49c7b9916 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index daf4a7846..41239961a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -270,8 +270,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OPEN", "Otwórz"), ("Chat", "Czat"), ("Total", "Łącznie"), - ("items", "elementy"), - ("Selected", "Zaznaczone"), + ("items", "elementów"), + ("Selected", "Zaznaczonych"), ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejścia"), ("Audio Capture", "Przechwytywanie dźwięku"), @@ -345,7 +345,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "Ciemny motyw"), ("Dark", "Ciemny"), ("Light", "Jasny"), - ("Follow System", "Zgodne z systemem"), + ("Follow System", "Zgodny z systemem"), ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), ("Enable Audio", "Włącz dźwięk"), @@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "albo"), ("Continue with", "Kontynuuj z"), ("Elevate", "Uzyskaj uprawnienia"), - ("Zoom cursor", "Zoom kursora"), + ("Zoom cursor", "Powiększenie kursora"), ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), ("Skipped", "Pominięte"), ("Add to Address Book", "Dodaj do Książki Adresowej"), - ("Group", "Grypy"), + ("Group", "Grupy"), ("Search", "Szukaj"), ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), ("Local keyboard type", "Lokalny typ klawiatury"), @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), ("Always use software rendering", "Zawsze używaj renderowania programowego"), ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("config_microphone", ""), ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), ("Wait", "Czekaj"), ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), @@ -433,17 +434,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Słabe"), ("Medium", "Średnie"), ("Strong", "Mocne"), - ("Switch Sides", "Zmień Strony"), + ("Switch Sides", "Zamień Strony"), ("Please confirm if you want to share your desktop?", "Czy na pewno chcesz udostępnić swój ekran?"), ("Closed as expected", "Zamknięto pomyślnie"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Wyświetlanie"), + ("Default View Style", "Domyślny styl wyświetlania"), + ("Default Scroll Style", "Domyślny styl przewijania"), + ("Default Image Quality", "Domyślna jakość obrazu"), + ("Default Codec", "Dokyślny kodek"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Inne opcje domyślne"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 64e5e9315..e69a140c9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0f64ae67f..0887a5915 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7e209dff8..304353d42 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7ec6c1554..1e6c6962a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("config_microphone", ""), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), @@ -434,16 +435,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средний"), ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), - ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", "Закрыто по ожиданию"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", ""), + ("Display", "Отображение"), + ("Default View Style", "Стиль отображения по умолчанию"), + ("Default Scroll Style", "Стиль прокрутки по умолчанию"), + ("Default Image Quality", "Качество изображения по умолчанию"), + ("Default Codec", "Кодек по умолчанию"), + ("Bitrate", "Битрейт"), + ("FPS", "FPS"), + ("Auto", "Авто"), + ("Other Default Options", "Другие параметры по умолчанию"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a703c0799..6f6f7a18e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 16c948ceb..2fb74fa5d 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 285a51732..5d4a6e1ad 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index dd943e0e6..31a3ade8f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 3050ff635..e30c09e44 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7572da9de..b88618074 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 535e4e772..1c75aaae7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 80b384c6c..a9e2c1715 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f5d9539d8..7c49a29a2 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "提權失敗"), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 37a7d6bcd..92c99d90c 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d78f5aa7b..8bb1d45e9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), @@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index ac3b32a49..8fa95ac90 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -534,6 +534,24 @@ pub fn get_pa_sources() -> Vec<(String, String)> { out } +pub fn get_default_pa_source() -> Option<(String, String)> { + use pulsectl::controllers::*; + match SourceController::create() { + Ok(mut handler) => { + if let Ok(dev) = handler.get_default_device() { + return Some(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + Err(err) => { + log::error!("Failed to get_pa_source: {:?}", err); + } + } + None +} + pub fn lock_screen() { std::process::Command::new("xdg-screensaver") .arg("lock") diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2e0d56eab..17f275c2a 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -11,6 +11,7 @@ use std::io::prelude::*; use std::{ ffi::OsString, fs, io, mem, + os::windows::process::CommandExt, path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -1644,6 +1645,29 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } +#[inline] +fn filter_foreground_window(process_id: DWORD) -> ResultType { + if let Ok(output) = std::process::Command::new("tasklist") + .args(vec![ + "/SVC", + "/NH", + "/FI", + &format!("PID eq {}", process_id), + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + { + let s = String::from_utf8_lossy(&output.stdout) + .to_string() + .to_lowercase(); + Ok(["Taskmgr", "mmc", "regedit"] + .iter() + .any(|name| s.contains(&name.to_string().to_lowercase()))) + } else { + bail!("run tasklist failed"); + } +} + pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -1651,7 +1675,12 @@ pub fn is_foreground_window_elevated() -> ResultType { if process_id == 0 { bail!("Failed to get processId, errno {}", GetLastError()) } - is_elevated(Some(process_id)) + let elevated = is_elevated(Some(process_id))?; + if elevated { + filter_foreground_window(process_id) + } else { + Ok(false) + } } } diff --git a/src/server.rs b/src/server.rs index 381e3df90..7807c4fac 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,16 @@ -use crate::ipc::Data; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + use bytes::Bytes; + pub use connection::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -19,12 +27,8 @@ use hbb_common::{ #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{GenericService, Service, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; + +use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { @@ -55,14 +59,19 @@ mod service; mod video_qos; pub mod video_service; -use hbb_common::tcp::new_listener; - pub type Childs = Arc>>; type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); + // A client server used to provide local services(audio, video, clipboard, etc.) + // for all initiative connections. + // + // [Note] + // Now we use this [`CLIENT_SERVER`] to do following operations: + // - record local audio, and send to remote + pub static ref CLIENT_SERVER: ServerPtr = new(); } pub struct Server { @@ -314,6 +323,13 @@ impl Server { } } } + + // get a new unique id + pub fn get_new_id(&mut self) -> i32 { + let new_id = self.id_count; + self.id_count += 1; + new_id + } } impl Drop for Server { @@ -404,7 +420,8 @@ pub async fn start_server(is_server: bool) { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; if Config::set(config) { log::info!("config synced"); } @@ -425,6 +442,48 @@ pub async fn start_server(is_server: bool) { } } +#[cfg(target_os = "macos")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc_url_server() { + log::debug!("Start an ipc server for listening to url schemes"); + match crate::ipc::new_listener("_url").await { + Ok(mut incoming) => { + while let Some(Ok(conn)) = incoming.next().await { + let mut conn = crate::ipc::Connection::new(conn); + match conn.next_timeout(1000).await { + Ok(Some(data)) => match data { + #[cfg(feature = "flutter")] + Data::UrlLink(url) => { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(crate::flutter::APP_TYPE_MAIN) + { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); + } + } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + }, + Err(err) => { + log::error!("{}", err); + } + _ => {} + } + } + } + Err(err) => { + log::error!("{}", err); + } + } +} + #[cfg(target_os = "macos")] async fn sync_and_watch_config_dir() { if crate::platform::is_root() { @@ -449,7 +508,8 @@ async fn sync_and_watch_config_dir() { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); if cfg0.0 != config { cfg0.0 = config.clone(); @@ -474,7 +534,7 @@ async fn sync_and_watch_config_dir() { let cfg = (Config::get(), Config2::get()); if cfg != cfg0 { log::info!("config updated, sync to root"); - match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { + match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); break; diff --git a/src/server/connection.rs b/src/server/connection.rs index e4b667d54..9ce53c960 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,11 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::video_service; +use crate::{ + client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + common::{get_default_sound_input, set_sound_input}, + video_service, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -32,7 +36,10 @@ use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; -use std::sync::{atomic::AtomicI64, mpsc as std_mpsc}; +use std::{ + num::NonZeroI64, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -90,12 +97,19 @@ pub struct Connection { recording: bool, last_test_delay: i64, lock_after_session_end: bool, - show_remote_cursor: bool, // by peer + show_remote_cursor: bool, + // by peer ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer - enable_file_transfer: bool, // by peer - tx_input: std_mpsc::Sender, // handle input messages + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + enable_file_transfer: bool, + // by peer + audio_sender: Option, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages video_ack_required: bool, peer_info: (String, String), server_audit_conn: String, @@ -106,6 +120,14 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + voice_call_request_timestamp: Option, + audio_input_device_before_voice_call: Option, +} + +impl ConnInner { + pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { + Self { id, tx, tx_video } + } } impl Subscriber for ConnInner { @@ -203,6 +225,9 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + audio_sender: None, + voice_call_request_timestamp: None, + audio_input_device_before_voice_call: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -367,6 +392,16 @@ impl Connection { msg.set_misc(misc); conn.send(msg).await; } + ipc::Data::VoiceCallResponse(accepted) => { + conn.handle_voice_call(accepted).await; + } + ipc::Data::CloseVoiceCall(_reason) => { + log::debug!("Close the voice call from the ipc."); + conn.close_voice_call().await; + // Notify the peer that we closed the voice call. + let msg = new_voice_call_request(false); + conn.send(msg).await; + } _ => {} } }, @@ -637,15 +672,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -771,7 +806,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -798,7 +833,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -842,7 +877,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -1125,7 +1160,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1289,12 +1324,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); + } } - } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1477,15 +1512,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), + } } } - } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1495,8 +1530,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1514,8 +1549,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1527,6 +1562,18 @@ impl Connection { } _ => {} }, + Some(misc::Union::AudioFormat(format)) => { + if !self.disable_audio { + // Drop the audio sender previously. + drop(std::mem::replace(&mut self.audio_sender, None)); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. + latency_controller.lock().unwrap().set_audio_only(true); + self.audio_sender = Some(start_audio_thread(Some(latency_controller))); + allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); + } + } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { @@ -1536,7 +1583,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1544,12 +1591,68 @@ impl Connection { } _ => {} }, + Some(message::Union::AudioFrame(frame)) => { + if !self.disable_audio { + if let Some(sender) = &self.audio_sender { + allow_err!(sender.send(MediaData::AudioFrame(frame))); + } else { + log::warn!("Processing audio frame without the voice call audio sender."); + } + } + } + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Notify the connection manager. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } + } + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. + } _ => {} } } true } + pub async fn handle_voice_call(&mut self, accepted: bool) { + if let Some(ts) = self.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + log::debug!("Backup the sound input device {}", audio_input_device); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } + self.send_to_cm(Data::StartVoiceCall); + } else { + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + } + self.send(msg).await; + } else { + log::warn!("Possible a voice call attack."); + } + } + + pub async fn close_voice_call(&mut self) { + // Restore to the prior audio device. + if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + set_sound_input(sound_input); + } + // Notify the connection manager that the voice call has been closed. + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + } + async fn update_option(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { @@ -1718,13 +1821,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2715a2643..edf0ef497 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -71,7 +71,6 @@ struct Input { y: i32, } -const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -202,11 +201,17 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> Ok(()) } +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum KeysDown { + RdevKey(RawKey), + EnigoKey(u64), +} + lazy_static::lazy_static! { static ref ENIGO: Arc> = { Arc::new(Mutex::new(Enigo::new())) }; - static ref KEYS_DOWN: Arc>> = Default::default(); + static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); static ref LATEST_SYS_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } @@ -375,12 +380,7 @@ fn record_key_is_control_key(record_key: u64) -> bool { #[inline] fn record_key_is_chr(record_key: u64) -> bool { - KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START -} - -#[inline] -fn record_key_is_rdev_layout(record_key: u64) -> bool { - KEY_CHAR_START <= record_key + record_key < KEY_CHAR_START } #[inline] @@ -396,15 +396,16 @@ fn record_key_to_key(record_key: u64) -> Option { } #[inline] -fn release_record_key(record_key: u64) { - let func = move || { - if record_key_is_rdev_layout(record_key) { - simulate_(&EventType::KeyRelease(RdevKey::Unknown( - (record_key - KEY_RDEV_START) as _, - ))); - } else if let Some(key) = record_key_to_key(record_key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); +fn release_record_key(record_key: KeysDown) { + let func = move || match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } } }; @@ -733,7 +734,7 @@ pub fn reset_input_ondisconn() { } } -fn sim_rdev_rawkey(code: u32, keydown: bool) { +fn sim_rdev_rawkey_position(code: u32, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -744,6 +745,21 @@ fn sim_rdev_rawkey(code: u32, keydown: bool) { #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +#[cfg(target_os = "windows")] +fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { + let rawkey = RawKey::WinVirtualKeycode(code); + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -874,9 +890,6 @@ fn sync_numlock_capslock_status(key_event: &KeyEvent) { } fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); - #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -894,7 +907,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - sim_rdev_rawkey(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } #[cfg(target_os = "macos")] @@ -924,10 +937,11 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { #[cfg(target_os = "linux")] fn is_altgr_pressed() -> bool { + let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _); KEYS_DOWN .lock() .unwrap() - .get(&(ControlKey::RAlt.value() as _)) + .get(&KeysDown::RdevKey(altgr_rawkey)) .is_some() } @@ -1011,7 +1025,7 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } -fn record_pressed_key(record_key: u64, down: bool) { +fn record_pressed_key(record_key: KeysDown, down: bool) { let mut key_down = KEYS_DOWN.lock().unwrap(); if down { key_down.insert(record_key, Instant::now()); @@ -1050,12 +1064,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { return; } let record_key = ck.value() as u64; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_control_key(&mut en, &ck, down) } Some(key_event::Union::Chr(chr)) => { let record_key = chr as u64 + KEY_CHAR_START; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_chr(&mut en, chr, down) } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), @@ -1067,6 +1081,29 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +#[cfg(target_os = "windows")] +fn translate_process_virtual_keycode(vk: u32, down: bool) { + crate::platform::windows::try_change_desktop(); + sim_rdev_rawkey_virtual(vk, down); +} + +fn translate_keyboard_mode(evt: &KeyEvent) { + match evt.union { + Some(key_event::Union::Unicode(_unicode)) => { + #[cfg(target_os = "windows")] + allow_err!(rdev::simulate_unicode(_unicode as _)); + } + Some(key_event::Union::Chr(..)) => + { + #[cfg(target_os = "windows")] + translate_process_virtual_keycode(evt.chr(), evt.down) + } + _ => { + log::debug!("Unreachable. Unexpected key event {:?}", &evt); + } + } +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -1080,7 +1117,7 @@ pub fn handle_key_(evt: &KeyEvent) { map_keyboard_mode(evt); } KeyboardMode::Translate => { - legacy_keyboard_mode(evt); + translate_keyboard_mode(evt); } _ => { legacy_keyboard_mode(evt); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index a2f6fb829..c783fef52 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -118,11 +118,9 @@ impl SharedMemory { fn flink(name: String) -> ResultType { let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); - let mut dir = PathBuf::from(disk); - let dir1 = dir.join("ProgramData"); - let dir2 = std::env::var("TEMP") - .map(|d| PathBuf::from(d)) - .unwrap_or(dir.join("Windows").join("Temp")); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let mut dir; if dir1.exists() { dir = dir1; } else if dir2.exists() { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index d041a433c..57fdf2c22 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -498,7 +498,7 @@ fn run(sp: GenericService) -> ResultType<()> { video_qos.target_bitrate, video_qos.fps ); - encoder.set_bitrate(video_qos.target_bitrate).unwrap(); + allow_err!(encoder.set_bitrate(video_qos.target_bitrate)); spf = video_qos.spf(); } drop(video_qos); @@ -954,10 +954,7 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { fn start_uac_elevation_check() { static START: Once = Once::new(); START.call_once(|| { - if !crate::platform::is_installed() - && !crate::platform::is_root() - && !crate::portable_service::client::running() - { + if !crate::platform::is_installed() && !crate::platform::is_root() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); if let Ok(uac) = crate::ui::win_privacy::is_process_consent_running() { diff --git a/src/ui.rs b/src/ui.rs index 8763194fe..7973a0ba4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -95,6 +95,9 @@ pub fn start(args: &mut [String]) { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; + // Start pulse audio local server. + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } else if args[0] == "--install" { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 2bd8824db..cce553154 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -55,6 +55,17 @@ impl InvokeUiCM for SciterHandler { fn show_elevation(&self, show: bool) { self.call("showElevation", &make_args!(show)); } + + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "updateVoiceCallState", + &make_args!( + client.id, + client.in_voice_call, + client.incoming_voice_call + ), + ); + } } impl SciterHandler { diff --git a/src/ui/header.tis b/src/ui/header.tis index dd0b35541..009995f4f 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -378,7 +378,7 @@ class Header: Reactor.Component { togglePrivacyMode(me.id); } else if (me.id == "show-quality-monitor") { toggleQualityMonitor(me.id); - }else if (me.attributes.hasClass("toggle-option")) { + } else if (me.attributes.hasClass("toggle-option")) { handler.toggle_option(me.id); toggleMenuState(); } else if (!me.attributes.hasClass("selected")) { @@ -434,6 +434,9 @@ function toggleMenuState() { var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); + var a = handler.get_audio_mode(); + if (!a) a = "guest-to-host"; + values.push(a); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 7daef8eab..f34b7c2c1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -1,3 +1,5 @@ +use std::{ffi::c_void, rc::Rc}; + #[cfg(target_os = "macos")] use cocoa::{ appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*, NSMenu, NSMenuItem}, @@ -8,11 +10,13 @@ use objc::{ class, declare::ClassDecl, msg_send, - runtime::{Object, Sel, BOOL}, + runtime::{BOOL, Object, Sel}, sel, sel_impl, }; -use sciter::{make_args, Host}; -use std::{ffi::c_void, rc::Rc}; +use objc::runtime::Class; +use sciter::{Host, make_args}; + +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -98,12 +102,21 @@ unsafe fn set_delegate(handler: Option>) { sel!(handleMenuItem:), handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); + decl.add_method(sel!(handleEvent:withReplyEvent:), handle_apple_event as extern fn(&Object, Sel, u64, u64)); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; let state = DelegateState { handler }; let handler_ptr = Box::into_raw(Box::new(state)); (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + // Set the url scheme handler + let cls = Class::get("NSAppleEventManager").unwrap(); + let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; + let _: () = msg_send![manager, + setEventHandler: delegate + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass: fruitbasket::kInternetEventClass + andEventID: fruitbasket::kAEGetURL]; let () = msg_send![NSApp(), setDelegate: delegate]; } @@ -125,7 +138,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - hbb_common::log::debug!("icon clicked on finder"); + log::debug!("icon clicked on finder"); if std::env::args().nth(1) == Some("--server".to_owned()) { crate::platform::macos::check_main_window(); } @@ -167,6 +180,24 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} + +extern fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { + let event = event as *mut Object; + let url = fruitbasket::parse_url_event(event); + log::debug!("an event was received: {}", url); + std::thread::spawn(move || handle_url_scheme(url)); +} + unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { let title = NSString::alloc(nil).init_str(title); let action = sel!(handleMenuItem:); @@ -233,4 +264,4 @@ pub fn make_tray() { set_delegate(None); } crate::tray::make_tray(); -} \ No newline at end of file +} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 21504d20d..999b409e0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -6,12 +6,12 @@ use std::{ use sciter::{ dom::{ - event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, - Element, HELEMENT, + Element, + event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, }, make_args, - video::{video_destination, AssetPtr, COLOR_SPACE}, Value, + video::{AssetPtr, COLOR_SPACE, video_destination}, }; use hbb_common::{ @@ -266,6 +266,22 @@ impl InvokeUiSession for SciterHandler { } fn switch_back(&self, _id: &str) {} + + fn on_voice_call_started(&self) { + self.call("onVoiceCallStart", &make_args!()); + } + + fn on_voice_call_closed(&self, reason: &str) { + self.call("onVoiceCallClosed", &make_args!(reason)); + } + + fn on_voice_call_waiting(&self) { + self.call("onVoiceCallWaiting", &make_args!()); + } + + fn on_voice_call_incoming(&self) { + self.call("onVoiceCallIncoming", &make_args!()); + } } pub struct SciterSession(Session); @@ -420,6 +436,8 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); + fn request_voice_call(); + fn close_voice_call(); } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 5d451e4d4..de33b0169 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -49,6 +49,8 @@ pub struct Client { pub restart: bool, pub recording: bool, pub from_switch: bool, + pub in_voice_call: bool, + pub incoming_voice_call: bool, #[serde(skip)] tx: UnboundedSender, } @@ -88,6 +90,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn change_language(&self); fn show_elevation(&self, show: bool); + + fn update_voice_call_state(&self, client: &Client); } impl Deref for ConnectionManager { @@ -138,6 +142,8 @@ impl ConnectionManager { recording, from_switch, tx, + in_voice_call: false, + incoming_voice_call: false }; CLIENTS .write() @@ -180,6 +186,30 @@ impl ConnectionManager { fn show_elevation(&self, show: bool) { self.ui_handler.show_elevation(show); } + + fn voice_call_started(&self, id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = true; + self.ui_handler.update_voice_call_state(client); + } + } + + fn voice_call_incoming(&self, id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = true; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } + } + + fn voice_call_closed(&self, id: i32, _reason: &str) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } + } } #[inline] @@ -389,6 +419,15 @@ impl IpcTaskRunner { Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => { self.cm.show_elevation(show); } + Data::StartVoiceCall => { + self.cm.voice_call_started(self.conn_id); + } + Data::VoiceCallIncoming => { + self.cm.voice_call_incoming(self.conn_id); + } + Data::CloseVoiceCall(reason) => { + self.cm.voice_call_closed(self.conn_id, reason.as_str()); + } _ => { } @@ -805,3 +844,19 @@ pub fn elevate_portable(_id: i32) { } } } + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn handle_incoming_voice_call(id: i32, accept: bool) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::VoiceCallResponse(accept))); + }; +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn close_voice_call(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); + }; +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..87ea8e9eb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,26 +1,30 @@ -use crate::client::io_loop::Remote; -use crate::client::{ - check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, - LoginConfigHandler, QualityStatus, KEY_MAP, -}; -use crate::common::{self, GrabState}; -use crate::keyboard; -use crate::{client::Data, client::Interface}; -use async_trait::async_trait; -use bytes::Bytes; -use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; -use hbb_common::rendezvous_proto::ConnType; -use hbb_common::tokio::{self, sync::mpsc}; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; -use rdev::{Event, EventType::*}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use async_trait::async_trait; +use bytes::Bytes; +use rdev::{Event, EventType::*}; use uuid::Uuid; + +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; +use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{self, sync::mpsc}; + +use crate::{client::Data, client::Interface}; +use crate::client::{ + check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui, + handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler, + QualityStatus, send_mouse, start_video_audio_threads, +}; +use crate::client::io_loop::Remote; +use crate::common::{self, GrabState}; +use crate::keyboard; + pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -361,11 +365,24 @@ impl Session { } pub fn enter(&self) { + #[cfg(target_os = "windows")] + { + match &self.lc.read().unwrap().keyboard_mode as _ { + "legacy" => rdev::set_get_key_unicode(true), + "translate" => rdev::set_get_key_unicode(true), + _ => {} + } + } + IS_IN.store(true, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { + #[cfg(target_os = "windows")] + { + rdev::set_get_key_unicode(false); + } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); } @@ -403,7 +420,7 @@ impl Session { pub fn handle_flutter_key_event( &self, - name: &str, + _name: &str, keycode: i32, scancode: i32, lock_modes: i32, @@ -428,7 +445,7 @@ impl Session { }; let event = Event { time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), + unicode: None, code: keycode as _, scan_code: scancode as _, event_type: event_type, @@ -653,6 +670,14 @@ impl Session { } } } + + pub fn request_voice_call(&self) { + self.send(Data::NewVoiceCall); + } + + pub fn close_voice_call(&self) { + self.send(Data::CloseVoiceCall); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -693,6 +718,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn on_voice_call_started(&self); + fn on_voice_call_closed(&self, reason: &str); + fn on_voice_call_waiting(&self); + fn on_voice_call_incoming(&self); } impl Deref for Session {