Merge branch 'master' of https://github.com/rustdesk/rustdesk into opt_chat_overlay_and_fix_pageview_2

This commit is contained in:
csf 2023-02-08 22:29:51 +09:00
commit d2c02ac85d
161 changed files with 3439 additions and 1118 deletions

View File

@ -1,32 +0,0 @@
---
name: Bug Report
about: Report a bug (English only, Please).
title: ""
labels: bug
assignees: ''
---
<!-- Hey there, thank you for creating an issue! -->
**Describe the bug you encountered:**
...
**What did you expect to happen instead?**
...
**How did you install `RustDesk`?**
<!-- GitHub release, build from source, Windows portable version, etc. -->
---
**RustDesk version and environment**
<!--
In order to reproduce your issue, please add some information about the environment
in which you're running RustDesk.
-->

60
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

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

View File

@ -1,4 +1,3 @@
blank_issues_enabled: true
contact_links:
- name: Ask a question
url: https://github.com/rustdesk/rustdesk/discussions/category_choices

View File

@ -1,10 +0,0 @@
---
name: Feature Request
about: Suggest an idea for this project ((English only, Please).
title: ''
labels: enhancement
assignees: ''
---

View File

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

20
.github/ISSUE_TEMPLATE/task.yaml vendored Normal file
View File

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

View File

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

48
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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("..")

View File

@ -1,6 +1,6 @@
<p dir="rtl" align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#تصاویر-محیط-نرمافزار">تصاویر محیط نرم‌افزار</a>
<a href="#تصاویر-محیط-نرم افزار">تصاویر محیط نرم‌افزار</a>
<a href="#ساختار-پوشه-ها">ساختار</a>
<a href="#نحوه-ساخت-با-داکر">داکر</a>
<a href="#ساخت">ساخت</a>
@ -9,12 +9,12 @@
<p align="center" dir="auto">[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]</p>
<p dir="rtl" align="center"><b>برای ترجمه این سند (README)، <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang" dir="rtl">رابط کاربری RustDesk</a>، <a href="https://github.com/rustdesk/doc.rustdesk.com" dir="rtl">و مستندات آن</a> به زبان مادری شما به کمکتان نیازمندیم. </b></p>
با ما گپ بزنید: [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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

1
flutter/assets/chat.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675159173189" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1697" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512.7 797H292.9c-24 0-47.3 4.7-70 12.5-57.3 19.5-108 50.7-155.5 87.4-13 10-28.3 10.9-40.2 1.7-8.2-6.3-12.6-14.7-12.6-25.1 0-133.2-0.2-266.5 0.1-399.7 0.1-36.9 6.7-73.1 17.3-108.6 10.8-36.1 26.1-70.1 47.4-101.2 32.7-47.8 76.2-81.7 131.7-99.5 18-5.8 36.6-9 55.4-10.5 12.2-1 24.4-1.1 36.6-1.1 26.5 0 52.9-0.3 79.4 0.1 72.8 0.9 145.6 0.6 218.5 0.3 41.6-0.2 83.2-0.3 124.9-0.3 28.3 0 56.1 3.9 83 13.1 34.5 11.7 65.3 29.6 92.2 54.3 16 14.8 30.2 31.1 42.6 49 32.4 46.9 52 98.7 61.1 154.8 3.2 19.8 5.1 39.7 4.7 59.8-0.9 50.3-11.7 98.3-33 144-13 27.9-29.5 53.5-49.7 76.6-30.5 34.8-67.3 60.7-110.9 76.7-23.6 8.7-48 13.6-73 15.2-6.2 0.4-12.4 0.5-18.6 0.5H512.7z m-4.6-580.6c0-0.1 0-0.1 0 0-70.5-0.1-141-0.1-211.5-0.1-10.2 0-20.4 0.2-30.6 1.3-26.9 2.8-52.1 10.8-75.3 24.9-26.8 16.2-47.3 38.8-64.1 64.9-15 23.2-25.7 48.3-33.7 74.7-9.3 30.9-15.1 62.6-15.2 94.8-0.3 110.5-0.1 220.9-0.1 331.4 0 1-0.5 2.4 0.5 3 1 0.6 1.9-0.6 2.7-1.1 28.5-18.3 58.1-34.6 89.3-47.9 41.6-17.8 84.6-28.4 130.3-28.3 136.6 0.4 273.2 0.1 409.8 0.2 13.8 0 27.6-0.1 41.3-1.8 20.1-2.5 39.5-7.6 57.9-16.3 36.9-17.4 66.3-43.5 88.8-77.3 40-60.4 55.1-126.7 45.3-198.5-5.3-38.9-17.3-75.7-36.2-110.2-14.1-25.7-31.8-48.7-54.2-67.8-34.8-29.9-75.5-45.2-121.1-45.6-74.6-0.8-149.3-0.3-223.9-0.3z" p-id="1698"></path><path d="M548.2 673.6c-17.5 0.4-34.7-2.3-51.7-6.4-6.4-1.5-11.5-5-16.1-9.6-24.6-24.3-48.9-48.8-72.3-74.3-21.6-23.5-42.6-47.5-61.8-73.1-13.4-17.9-26.4-36.1-35.1-56.9-8.1-19.4-10.5-39.5-7.4-60.4 4.1-27.4 16.7-50.8 33.5-72.3 6.3-8 13.2-15.3 20.8-22 9.3-8.2 20.2-10.3 31.9-5.9 11.8 4.5 18.7 13.4 20.2 26.1 1.2 10.3-2.1 19.1-9.7 26.2-11.8 11.2-21.8 23.7-28.6 38.6-6.7 14.7-8.8 29.7-2.7 45.1 4 10.2 10.3 19.3 16.5 28.4 17.1 24.9 36.8 47.7 56.8 70.2 22.1 24.9 45.6 48.5 68.9 72.3 2.4 2.5 5.1 4.8 7.5 7.3 2.2 2.2 5.1 1.8 7.7 2.1 16.1 2.2 32.1 2.8 48-1.3 13.2-3.4 23.6-10.4 30.9-22.3 12-19.3 38-20.4 51.9-2.7 7.7 9.9 8.5 24.1 1.8 35.4-16 27.1-40.1 43.2-70.2 50.9-4.2 1.1-8.4 1.9-12.7 2.6-9.2 1.5-18.5 2.4-28.1 2zM532.5 315.7c0.1-10.5 10.4-18.2 20.3-15.1 22.8 7.2 43.9 17.5 63.6 31 21.2 14.6 38.1 33.1 51.9 54.6 16.2 25.1 27.7 52.3 34.8 81.2 1 4 1.8 8 2.5 12.1 1.5 8.1-3.6 16.1-11.5 18.2-7.8 2.1-16.1-2-19-9.7-0.8-2.2-1.2-4.7-1.8-7-8-35.7-22.7-68.2-46-96.7-14.3-17.4-32.4-30-52.2-40.2-10.1-5.2-20.6-9.5-31.5-13-7.1-2.2-11.1-8-11.1-15.4zM615.6 513.1c-8.1-0.1-14.1-5.1-15.8-13.6-3.2-15.8-9.1-30.5-17.6-44.1-14.4-23.1-34.1-39.9-59.3-50.2-1.5-0.6-3-0.9-4.5-1.4-8.7-2.9-13.3-11.7-10.6-20.1 2.9-8.8 11.6-13.1 20.5-10.1 38.1 12.8 65.8 38 85.4 72.5 8.5 15 14.3 31.1 17.5 48.2 1.9 9.8-5.5 18.8-15.6 18.8z" p-id="1699"></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 415 415" xml:space="preserve">
<g>
<path d="M174.848,188.711c2.407-2.068,3.577-5.109,3.577-9.297c0-4.133-1.193-7.307-3.647-9.701
c-2.431-2.369-6.148-3.571-11.048-3.571h-15.915v25.73h15.576C168.552,191.872,172.407,190.809,174.848,188.711z"/>
<path d="M0,65.241v284.518h415V65.241H0z M70.675,238.253c-18.293,0-33.123-14.83-33.123-33.123
c0-18.293,14.83-33.123,33.123-33.123s33.123,14.83,33.123,33.123C103.798,223.423,88.968,238.253,70.675,238.253z
M206.768,249.556h-22.422l-0.411-0.328c-2.099-1.679-3.467-4.433-4.067-8.185c-0.552-3.451-0.832-6.769-0.832-9.859v-6.979
c0-4.492-1.212-8.003-3.602-10.435c-2.417-2.456-5.779-3.65-10.28-3.65h-17.337v39.437h-22.787v-101.66h38.701
c11.546,0,20.743,2.699,27.335,8.023c6.688,5.403,10.078,13.012,10.078,22.614c0,5.404-1.442,10.124-4.285,14.028
c-2.217,3.044-5.267,5.647-9.089,7.767c4.433,1.864,7.785,4.556,9.99,8.029c2.696,4.247,4.063,9.533,4.063,15.711v7.25
c0,2.622,0.361,5.407,1.074,8.278c0.66,2.664,1.774,4.637,3.309,5.864l0.563,0.451V249.556z M287.335,249.556h-70.558v-101.66
h70.422v18.246h-47.636v21.801h40.859v18.246h-40.859v25.121h47.771V249.556z M374.201,183.193l-0.552,1.65h-21.824v-1.5
c0-6.092-1.443-10.781-4.29-13.937c-2.805-3.111-7.331-4.688-13.454-4.688c-5.458,0-9.671,2.155-12.879,6.589
c-3.273,4.522-4.933,10.41-4.933,17.501v19.699c0,7.167,1.743,13.091,5.182,17.607c3.39,4.452,7.854,6.617,13.646,6.617
c5.714,0,9.953-1.507,12.602-4.479c2.693-3.022,4.059-7.668,4.059-13.808v-1.5h21.756l0.552,1.65l0.004,0.23
c0.187,11.009-3.245,19.89-10.199,26.396c-6.92,6.473-16.601,9.755-28.772,9.755c-12.247,0-22.344-4.006-30.01-11.906
c-7.655-7.887-11.537-18.155-11.537-30.521v-19.583c0-12.311,3.785-22.573,11.25-30.504c7.488-7.957,17.34-11.991,29.28-11.991
c12.524,0,22.485,3.289,29.605,9.776c7.166,6.531,10.705,15.519,10.519,26.714L374.201,183.193z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675772071409" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5514" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zM584 328c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z" p-id="5515"></path><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-0.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7 0.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c0.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-0.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66 0.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 0.05 0.01 0.1 0.01 0.15 1.07 12.15-0.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-0.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5 0.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l0.36 0.03c7.97 0.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-0.16 0.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l0.18-0.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z" p-id="5516"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675683991720" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4457" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M561 362.67h-98V463h98V362.67z m200.67 0H661.33V463h100.33V362.67zM911 687c-62.22 0-121.33-9.33-177.33-28-20.22-6.22-37.33-2.34-51.33 11.67L572.67 780.34c-70-35.78-133.39-82.06-190.17-138.84S279.45 521.33 243.67 451.33l109.67-109.67c14-14 17.89-31.11 11.67-51.34-18.67-56-28-115.11-28-177.33 0-14-4.67-25.67-14-35-9.33-9.33-21-14-35-14H113c-14 0-25.67 4.67-35 14-9.33 9.33-14 21-14 35 0 112 21.39 220.11 64.17 324.33 42.78 104.22 103.83 196 183.17 275.34 79.33 79.34 171.1 140.4 275.33 183.17C690.89 938.61 799 960 911 960c14 0 25.67-4.67 35-14 9.33-9.33 14-21 14-35V736c0-14-4.67-25.67-14-35-9.33-9.33-21-14-35-14z m-51.33-224H960V362.67H859.67V463z" p-id="4458"></path></svg>

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -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<NavigatorState>();
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<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
Future<void> initUniLinks() async {
if (!Platform.isWindows && !Platform.isMacOS) {
return;
Future<bool> 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<void> 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<void> 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<void> 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<PermissionAuthorizeType> 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<bool> osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
}

View File

@ -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<String> 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<int> currentTabs() {
var v = List<int>.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<int> _notHiddenTabs() {
var v = List<int>.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<PeerTabPage>
),
() => {}),
];
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<PeerTabPage>
Future<void> 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<PeerTabPage>
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
buildScrollJumper(),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
@ -213,82 +118,115 @@ class _PeerTabPageState extends State<PeerTabPage>
}
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<PeerTabModel>(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<PeerTabModel>(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<PeerTabModel>(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<PeerTabPage>
);
}
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<PeerTabPage>
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
return Obx(() {
final List<MenuEntryBase> 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<PeerTabModel>(context);
final List<MenuEntryBase> menu = List.empty(growable: true);
final List<int> 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<PeerSearchBar> {
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,

View File

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

View File

@ -44,6 +44,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
var watchIsCanRecordAudio = false;
Timer? _updateTimer;
@override
@ -79,7 +80,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
buildHelpCards(),
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
return data.data!;
} else {
return const Offstage();
}
},
),
],
),
),
@ -302,7 +312,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
Widget buildHelpCards() {
Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@ -349,6 +359,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
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<DesktopHomePage>
setState(() {});
}
}
if (watchIsCanRecordAudio) {
if (Platform.isMacOS) {
Future.microtask(() async {
if ((await osxCanRecordAudio() ==
PermissionAuthorizeType.authorized)) {
watchIsCanRecordAudio = false;
setState(() {});
}
});
} else {
watchIsCanRecordAudio = false;
setState(() {});
}
}
});
Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);

View File

@ -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<RemotePage>
class ImagePaint extends StatefulWidget {
final String id;
final Rx<bool> zoomCursor;
final Rx<bool> cursorOverImage;
final Rx<bool> keyboardEnabled;
final Rx<bool> 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<ImagePaint> {
final ScrollController _vertical = ScrollController();
String get id => widget.id;
Rx<bool> get zoomCursor => widget.zoomCursor;
Rx<bool> get cursorOverImage => widget.cursorOverImage;
Rx<bool> get keyboardEnabled => widget.keyboardEnabled;
Rx<bool> 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<ImagePaint> {
var c = Provider.of<CanvasModel>(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<CanvasModel>(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<ImagePaint> {
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;

View File

@ -243,96 +243,35 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
padding: padding,
),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
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<String>());
menu.add(() {
final state = ShowRemoteCursorState.find(key);
return MenuEntrySwitch2<String>(
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<String>(
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<String>(
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<String>(
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));
}
}

View File

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

View File

@ -790,6 +790,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
_PopupMenuRoute({
required this.position,
required this.items,
this.menuWrapper,
this.initialValue,
this.elevation,
required this.barrierLabel,
@ -802,6 +803,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final RelativeRect position;
final List<PopupMenuEntry<T>> items;
final MenuWrapper? menuWrapper;
final List<Size?> itemSizes;
final T? initialValue;
final double? elevation;
@ -844,11 +846,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
}
}
final Widget menu = _PopupMenu<T>(
Widget menu = _PopupMenu<T>(
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<T?> showMenu<T>({
required BuildContext context,
required RelativeRect position,
required List<PopupMenuEntry<T>> items,
MenuWrapper? menuWrapper,
T? initialValue,
double? elevation,
String? semanticLabel,
@ -1062,6 +1068,7 @@ Future<T?> showMenu<T>({
return navigator.push(_PopupMenuRoute<T>(
position: position,
items: items,
menuWrapper: menuWrapper,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
@ -1094,6 +1101,8 @@ typedef PopupMenuCanceled = void Function();
typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> 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<T> extends StatefulWidget {
const PopupMenuButton({
Key? key,
required this.itemBuilder,
this.menuWrapper,
this.initialValue,
this.onHover,
this.onSelected,
@ -1151,6 +1161,9 @@ class PopupMenuButton<T> extends StatefulWidget {
/// Called when the button is pressed to create the items to show in the menu.
final PopupMenuItemBuilder<T> 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<T> extends State<PopupMenuButton<T>> {
context: context,
elevation: widget.elevation ?? popupMenuTheme.elevation,
items: items,
menuWrapper: widget.menuWrapper,
initialValue: widget.initialValue,
position: position,
shape: widget.shape ?? popupMenuTheme.shape,

View File

@ -109,13 +109,17 @@ class MenuConfig {
this.boxWidth});
}
typedef DismissCallback = Function();
abstract class MenuEntryBase<T> {
bool dismissOnClicked;
DismissCallback? dismissCallback;
RxBool? enabled;
MenuEntryBase({
this.dismissOnClicked = false,
this.enabled,
this.dismissCallback,
});
List<mod_menu.PopupMenuEntry<T>> 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<T> extends MenuEntryBase<T> {
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<T> extends MenuEntryBase<T> {
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<T> extends MenuEntryBase<T> {
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<T> extends MenuEntryBase<T> {
this.textStyle,
this.padding,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
dismissCallback,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
);
RxBool get curOption;
Future<void> setOption(bool? option);
@ -463,6 +485,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(v);
},
@ -474,6 +499,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(v);
},
@ -485,6 +513,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
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<T> extends MenuEntrySwitchBase<T> {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
switchType: switchType,
text: text,
@ -515,6 +547,7 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
padding: padding,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
) {
() async {
_curOption.value = await getter();
@ -551,12 +584,15 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
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<T> extends MenuEntryBase<T> {
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<T> extends MenuEntryBase<T> {
? () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
proc();
}

View File

@ -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<String> viewStyle(
String remoteId,
FFI ffi,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
RxString? rxViewStyle,
}) {
return MenuEntryRadios<String>(
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<String> showRemoteCursor(
String remoteId,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
final state = ShowRemoteCursorState.find(remoteId);
final optKey = 'show-remote-cursor';
return MenuEntrySwitch2<String>(
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<String> disableClipboard(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return createSwitchMenuEntry(
remoteId,
'Disable clipboard',
'disable-clipboard',
padding,
true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> createSwitchMenuEntry(
String remoteId,
String text,
String option,
EdgeInsets? padding,
bool dismissOnClicked, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntrySwitch<String>(
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<String> insertLock(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
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<String>(
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<RemoteMenubar> {
}
}
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<Widget> menubarItems = [];
if (!isWebDesktop) {
@ -244,6 +426,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
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<RemoteMenubar> {
);
}
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<RemoteMenubar> {
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<RemoteMenubar> {
mod_menu.PopupMenuItem<String>(
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<RemoteMenubar> {
),
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<RemoteMenubar> {
? 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<RemoteMenubar> {
);
}
final _chatButtonKey = GlobalKey();
Widget _buildChat(BuildContext context) {
FfiModel ffiModel = Provider.of<FfiModel>(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<MenuEntryBase<String>> _getChatMenu(BuildContext context) {
final List<MenuEntryBase<String>> chatMenu = [];
const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
chatMenu.addAll([
MenuEntryButton<String>(
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<String>(
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<MenuEntryBase<String>> _getControlMenu(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
final perms = widget.ffi.ffiModel.permissions;
@ -554,6 +840,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
showSetOSPassword(
widget.id, false, widget.ffi.dialogManager);
@ -566,6 +853,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@ -577,6 +865,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@ -588,6 +877,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
connect(context, widget.id, isTcpTunneling: true);
},
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
]);
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
@ -605,23 +895,15 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
);
}
displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
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<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (perms['keyboard'] != false) {
displayMenu.add(MenuEntryButton<String>(
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<String>(
@ -670,6 +944,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (pi.platform != kPeerPlatformAndroid &&
@ -684,6 +959,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager),
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@ -699,6 +975,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
@ -720,10 +997,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
// },
// padding: padding,
// dismissOnClicked: true,
// dismissCallback: _menuDismissCallback,
// ));
// }
}
return displayMenu;
}
@ -758,33 +1035,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
final peer_version = widget.ffi.ffiModel.pi.version;
final displayMenu = [
MenuEntryRadios<String>(
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<String>(),
MenuEntryRadios<String>(
@ -794,21 +1050,26 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
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<RemoteMenubar> {
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<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
displayMenu.insert(3, MenuEntryDivider<String>());
@ -1061,6 +1325,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
);
}
@ -1087,11 +1352,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
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<RemoteMenubar> {
text: 'H264',
value: 'h264',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (codecs[1]) {
@ -1106,6 +1374,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: 'H265',
value: 'h265',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
return list;
@ -1122,6 +1391,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@ -1129,23 +1399,11 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
/// Show remote cursor
if (!widget.ffi.canvasModel.cursorEmbedded) {
displayMenu.add(() {
final state = ShowRemoteCursorState.find(widget.id);
return MenuEntrySwitch2<String>(
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<RemoteMenubar> {
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<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
final perms = widget.ffi.ffiModel.permissions;
@ -1192,6 +1453,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
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<RemoteMenubar> {
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<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@ -1233,25 +1500,29 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Ratio'),
optionsGetter: () {
List<MenuEntryRadioOption> list = [];
List<String> modes = ["legacy"];
List<KeyboardModeMenu> 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<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
showKBLayoutTypeChooser(
localPlatform, widget.ffi.dialogManager);
@ -1304,6 +1576,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
proc: () {},
padding: EdgeInsets.zero,
dismissOnClicked: false,
dismissCallback: _menuDismissCallback,
),
);
}
@ -1312,18 +1585,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
MenuEntrySwitch<String> _createSwitchMenuEntry(
String text, String option, EdgeInsets? padding, bool dismissOnClicked) {
return MenuEntrySwitch<String>(
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});
}

View File

@ -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<WindowActionPanel>
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<TabbarTheme> {
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(

View File

@ -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<String> kBootArgs;
/// Uni links.
StreamSubscription? _uniLinkSubscription;
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args");
@ -114,7 +118,6 @@ Future<void> 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<App> {
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
ChangeNotifierProvider.value(value: gFFI.peerTabModel),
],
child: GetMaterialApp(
navigatorKey: globalKey,

View File

@ -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<PasswordWidget> {
// 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(() {

View File

@ -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> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
Rx<VoiceCallStatus> 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
}

View File

@ -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();
}
}

View File

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

View File

@ -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<FFI> 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<String> get cachedKeys => _cacheKeys;
@ -1065,9 +1106,9 @@ class CursorModel with ChangeNotifier {
Future<bool> _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<String, dynamic> 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));

View File

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

View File

@ -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<FFI> parent;
int get currentTab => _currentTab;
int _currentTab = 0; // index in tabNames
List<int> get visibleOrderedTabs => _visibleOrderedTabs;
List<int> _visibleOrderedTabs = List.empty(growable: true);
List<int> get tabOrder => _tabOrder;
List<int> _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<bool> _fullyVisible = List.filled(5, false);
bool get leftFullyVisible => _leftFullyVisible;
bool _leftFullyVisible = false;
bool get rightFullyVisible => _rightFullyVisible;
bool _rightFullyVisible = false;
ScrollPosController sc = ScrollPosController();
List<String> 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<int> 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<int> orderedNotFilteredTabs() {
var list = tabOrder.toList();
if (_filterGroupCard()) {
list.remove(groupTabIndex);
}
return list;
}
// return index array of tabNames
List<int> visibleTabs() {
var v = List<int>.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<int> _notHiddenTabs() {
var v = List<int>.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<int> 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);
}
}
}

View File

@ -579,6 +579,26 @@ class ServerModel with ChangeNotifier {
notifyListeners();
}
}
void updateVoiceCallState(Map<String, dynamic> 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<String, dynamic> toJson() {

View File

@ -62,7 +62,7 @@ class UserModel {
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
}
Future<void> _parseAndUpdateUser(UserPayload user) async {

View File

@ -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<dynamic> 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);
}
}
}

View File

@ -31,4 +31,10 @@ class RdPlatformChannel {
return _osxMethodChannel
.invokeMethod("setWindowTheme", {"themeName": theme.name});
}
/// Terminate .app manually.
Future<void> terminate() {
assert(Platform.isMacOS);
return _osxMethodChannel.invokeMethod("terminate");
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -23,8 +23,10 @@
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<key>CFBundleURLIconFile</key>
<string></string>
<key>CFBundleURLName</key>
<string>com.carriez.rustdesk</string>
<key>CFBundleURLSchemes</key>
<array>
<string>rustdesk</string>
@ -35,13 +37,15 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSMicrophoneUsageDescription</key>
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
<string>NSApplication</string>
</dict>
</plist>

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -32,4 +32,4 @@
"purpose": "maskable"
}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

@ -143,32 +143,32 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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!();
}
}
}

View File

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

View File

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

View File

@ -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<u8> = 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<u8> = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6];
let data: Vec<u8> = 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);
}
}

View File

@ -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() != "" {

View File

@ -1 +1 @@
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));

View File

@ -13,22 +13,22 @@ use tokio_socks::{IntoTargetAddr, TargetAddr};
pub fn check_port<T: std::string::ToString>(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<T: std::string::ToString>(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<T: std::string::ToString>(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<T: std::string::ToString>(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<SocketAddr> {
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<SocketAddr> {
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]

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 75 B

1
res/128x128.png Symbolic link
View File

@ -0,0 +1 @@
../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 74 B

1
res/32x32.png Symbolic link
View File

@ -0,0 +1 @@
../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 74 B

1
res/64x64.png Symbolic link
View File

@ -0,0 +1 @@
../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 74 B

374
res/design.svg Normal file
View File

@ -0,0 +1,374 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
style="isolation:isolate"
viewBox="66.993 897.484 600 600.00001"
version="1.1"
id="svg11"
sodipodi:docname="design.svg"
xml:space="preserve"
inkscape:export-filename="216333102-4d10c195-be66-4fa0-97ca-70a71756b25e.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
width="600"
height="600"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs15"><linearGradient
id="linearGradient9947"
x1="0.14773831"
x2="0.84543866"
y1="0.85134232"
y2="0.15443686"
gradientTransform="matrix(26.301,0,0,26.331,90.673535,911.7572)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
stop-color="#004ba6"
id="stop9943"
style="stop-color:#0071ff;stop-opacity:1;" /><stop
offset="1"
stop-color="#00bfe1"
id="stop9945"
style="stop-color:#01aee6;stop-opacity:1;" /></linearGradient><linearGradient
id="linearGradient9775"
x1="0.045312501"
x2="0.95468748"
y1="0.95468748"
y2="0.045312501"
gradientTransform="matrix(32,0,0,32,231.02995,929.98044)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
stop-color="#004ba6"
id="stop9771"
style="stop-color:#0071ff;stop-opacity:1;" /><stop
offset="1"
stop-color="#00bfe1"
id="stop9773"
style="stop-color:#01aee6;stop-opacity:1;" /></linearGradient><linearGradient
id="linearGradient5424"
x1="0.14773831"
x2="0.84543866"
y1="0.85134232"
y2="0.15443686"
gradientTransform="matrix(26.301,0,0,26.331,90.673535,911.7572)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
stop-color="#004ba6"
id="stop5420"
style="stop-color:#0071ff;stop-opacity:1;" /><stop
offset="1"
stop-color="#00bfe1"
id="stop5422"
style="stop-color:#01aee6;stop-opacity:1;" /></linearGradient><linearGradient
id="linearGradient1258"
x1="0.045312501"
x2="0.95468748"
y1="0.95468748"
y2="0.045312501"
gradientTransform="matrix(32,0,0,32,231.02995,929.98044)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
stop-color="#004ba6"
id="stop1254"
style="stop-color:#0071ff;stop-opacity:1;" /><stop
offset="1"
stop-color="#00bfe1"
id="stop1256"
style="stop-color:#01aee6;stop-opacity:1;" /></linearGradient><linearGradient
id="linearGradient1228"
x1="0.045312501"
x2="0.95468748"
y1="0.95468748"
y2="0.045312501"
gradientTransform="matrix(32,0,0,32,231.02995,929.98044)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
stop-color="#004ba6"
id="stop1224"
style="stop-color:#0071ff;stop-opacity:1;" /><stop
offset="100%"
stop-color="#00bfe1"
id="stop1226" /></linearGradient><rect
x="34.072868"
y="7.5261359"
width="62.535347"
height="16.146982"
id="rect9005" /><linearGradient
id="a-3"
x1="0.045312501"
x2="0.95468748"
y1="0.95468748"
y2="0.045312501"
gradientTransform="matrix(32,0,0,32,231.02995,929.98044)"
gradientUnits="userSpaceOnUse"><stop
offset="0%"
stop-color="#004ba6"
id="stop294" /><stop
offset="100%"
stop-color="#00bfe1"
id="stop296" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1228"
id="linearGradient426"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(32,0,0,32,45.096615,851.74489)"
x1="0.045312501"
y1="0.95468748"
x2="0.95468748"
y2="0.045312501" /><linearGradient
inkscape:collect="always"
xlink:href="#a"
id="linearGradient5414"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(26.000477,0,0,25.999345,91.160121,984.42075)"
x1="0.14773831"
y1="0.85134232"
x2="0.84543866"
y2="0.15443686" /><linearGradient
inkscape:collect="always"
xlink:href="#a"
id="linearGradient9753"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(26.000477,0,0,25.999345,66.993017,897.4845)"
x1="0.14773831"
y1="0.85134232"
x2="0.84543866"
y2="0.15443686" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9775"
id="linearGradient9769"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(32,0,0,32,45.096615,851.74489)"
x1="0.045312501"
y1="0.95468748"
x2="0.95468748"
y2="0.045312501" /><linearGradient
inkscape:collect="always"
xlink:href="#a"
id="linearGradient9831"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(22.000402,0,0,21.999458,71.993014,902.48443)"
x1="0.14773831"
y1="0.85134232"
x2="0.84543866"
y2="0.15443686" /><linearGradient
inkscape:collect="always"
xlink:href="#a"
id="linearGradient342"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(26.000475,0,0,25.999358,17.984526,891.74869)"
x1="0.14773831"
y1="0.85134232"
x2="0.84543866"
y2="0.15443686" /><linearGradient
inkscape:collect="always"
xlink:href="#a"
id="linearGradient765"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(26.000475,0,0,25.999358,17.984526,891.74869)"
x1="0.14773831"
y1="0.85134232"
x2="0.84543866"
y2="0.15443686" /></defs><sodipodi:namedview
id="namedview13"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.8436908"
inkscape:cx="11.119001"
inkscape:cy="90.307984"
inkscape:window-width="1440"
inkscape:window-height="847"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg11"
showguides="true" /><linearGradient
id="a"
x1="0.14773831"
x2="0.84543866"
y1="0.85134232"
y2="0.15443686"
gradientTransform="matrix(26.301,0,0,26.331,90.673535,911.7572)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
stop-color="#004ba6"
id="stop4"
style="stop-color:#0071ff;stop-opacity:1;" /><stop
offset="1"
stop-color="#00bfe1"
id="stop6"
style="stop-color:#00bfe1;stop-opacity:1;" /></linearGradient><text
xml:space="preserve"
transform="translate(66.993,897.484)"
id="text9003"
style="white-space:pre;shape-inside:url(#rect9005);display:inline;fill:#0071ff" /><path
fill="url(#a)"
d="m 113.48507,990.48835 -2.13532,2.12189 c -0.37566,0.3367 -0.55732,0.87879 -0.34675,1.33694 1.42256,2.97602 0.88256,6.52382 -1.45146,8.85602 -2.33502,2.3313 -5.88696,2.8707 -8.86652,1.4488 -0.43892,-0.1965 -0.953965,-0.03 -1.292057,0.3127 l -2.16992,2.1664 c -0.255052,0.2498 -0.3806,0.6023 -0.340069,0.9558 0.04053,0.3545 0.243189,0.6695 0.54767,0.8541 5.112896,3.0945 11.678996,2.3056 15.911086,-1.9116 4.23208,-4.2162 5.03876,-10.77258 1.9554,-15.88728 -0.17696,-0.31104 -0.48935,-0.52234 -0.84425,-0.57171 -0.35489,-0.0504 -0.71276,0.0681 -0.96781,0.31794 z m -18.466546,-2.30559 c -4.252844,4.20042 -5.086212,10.75774 -2.019656,15.88534 0.176955,0.312 0.488355,0.5233 0.843253,0.5727 0.354898,0.05 0.712762,-0.068 0.968803,-0.319 l 2.123456,-2.1091 c 0.384555,-0.3367 0.572152,-0.8847 0.35862,-1.3488 -1.422557,-2.976 -0.883553,-6.52373 1.451458,-8.85593 2.334022,-2.33126 5.886942,-2.87084 8.865522,-1.44997 0.43398,0.19452 0.94211,0.033 1.28119,-0.29971 l 2.18178,-2.1792 c 0.25505,-0.24883 0.3806,-0.60133 0.34007,-0.95581 -0.0415,-0.35349 -0.24319,-0.66847 -0.54767,-0.85411 -5.1218,-3.06786 -11.678011,-2.25523 -15.893289,1.97185 z"
id="path5412"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"
style="fill:url(#linearGradient5414);stroke-width:0.987988"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /><g
id="g9521"><g
id="g3226"
transform="translate(42.333011,90.667253)"><path
fill="url(#a)"
d="M 72.096614,883.74485 H 50.096615 c -2.76,0 -5,-2.24 -5,-5 v -21.99996 c 0,-2.759 2.24,-5 5,-5 h 21.999999 c 2.759,0 5,2.241 5,5 v 21.99996 c 0,2.76 -2.241,5 -5,5 z"
id="path422"
style="isolation:isolate;fill:url(#linearGradient426)" /></g><path
fill="url(#a)"
d="m 111.31998,952.54667 -1.8068,1.79545 c -0.31787,0.2849 -0.47158,0.7436 -0.29341,1.13122 1.20371,2.51824 0.74678,5.52022 -1.22816,7.49362 -1.97578,1.97264 -4.98127,2.42905 -7.50244,1.22591 -0.37139,-0.16627 -0.807201,-0.0254 -1.093271,0.26459 l -1.83609,1.8331 c -0.21582,0.21137 -0.32205,0.50964 -0.28775,0.80876 0.0343,0.29996 0.20578,0.5665 0.46341,0.7227 4.326301,2.61842 9.882231,1.95089 13.463231,-1.61751 3.58099,-3.56755 4.26356,-9.11527 1.65457,-13.44309 -0.14974,-0.26324 -0.41407,-0.44203 -0.71437,-0.48375 -0.30029,-0.0423 -0.6031,0.0575 -0.81892,0.269 z m -15.625531,-1.9509 c -3.59857,3.55419 -4.30372,9.10275 -1.70895,13.44149 0.14974,0.264 0.41323,0.44279 0.71352,0.48459 0.3003,0.0423 0.60312,-0.0575 0.81977,-0.26992 l 1.79676,-1.78462 c 0.3254,-0.2849 0.48414,-0.7486 0.30345,-1.1413 -1.2037,-2.51815 -0.74762,-5.52013 1.22816,-7.49353 1.974941,-1.97255 4.981261,-2.42914 7.501591,-1.22684 0.36722,0.16458 0.79717,0.0279 1.08409,-0.25367 l 1.84612,-1.84394 c 0.21581,-0.21052 0.32205,-0.50879 0.28775,-0.80875 -0.0351,-0.29912 -0.20577,-0.56557 -0.46341,-0.7227 -4.33383,-2.59583 -9.881401,-1.90825 -13.448171,1.66853 z"
id="path9011"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.83599"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g><path
fill="url(#a)"
d="m 113.48507,990.48835 -2.13532,2.12189 c -0.37566,0.3367 -0.55732,0.87879 -0.34675,1.33694 1.42256,2.97602 0.88256,6.52382 -1.45146,8.85602 -2.33502,2.3313 -5.88696,2.8707 -8.86652,1.4488 -0.43892,-0.1965 -0.953965,-0.03 -1.292057,0.3127 l -2.16992,2.1664 c -0.255052,0.2498 -0.3806,0.6023 -0.340069,0.9558 0.04053,0.3545 0.243189,0.6695 0.54767,0.8541 5.112896,3.0945 11.678996,2.3056 15.911086,-1.9116 4.23208,-4.2162 5.03876,-10.77258 1.9554,-15.88728 -0.17696,-0.31104 -0.48935,-0.52234 -0.84425,-0.57171 -0.35489,-0.0504 -0.71276,0.0681 -0.96781,0.31794 z m -18.466546,-2.30559 c -4.252844,4.20042 -5.086212,10.75774 -2.019656,15.88534 0.176955,0.312 0.488355,0.5233 0.843253,0.5727 0.354898,0.05 0.712762,-0.068 0.968803,-0.319 l 2.123456,-2.1091 c 0.384555,-0.3367 0.572152,-0.8847 0.35862,-1.3488 -1.422557,-2.976 -0.883553,-6.52373 1.451458,-8.85593 2.334022,-2.33126 5.886942,-2.87084 8.865522,-1.44997 0.43398,0.19452 0.94211,0.033 1.28119,-0.29971 l 2.18178,-2.1792 c 0.25505,-0.24883 0.3806,-0.60133 0.34007,-0.95581 -0.0415,-0.35349 -0.24319,-0.66847 -0.54767,-0.85411 -5.1218,-3.06786 -11.678011,-2.25523 -15.893289,1.97185 z"
id="path9523"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="2599.3848"
inkscape:export-ydpi="2599.3848"
style="fill:url(#linearGradient5414);stroke-width:0.987988"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /><g
id="g9759"
transform="translate(18.35032,5.5050959)"><g
id="g8359"><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Asap;-inkscape-font-specification:Asap;fill:#0071ff"
x="98.192268"
y="917.46295"
id="text9011"><tspan
sodipodi:role="line"
id="tspan9009"
x="98.192268"
y="917.46295"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:21.3333px;font-family:Nunito;-inkscape-font-specification:'Nunito Semi-Bold';fill:#000000">rustdesk</tspan></text></g><path
fill="url(#a)"
d="m 89.317967,903.5521 -2.13532,2.12189 c -0.37566,0.3367 -0.55732,0.87879 -0.34675,1.33694 1.42256,2.97602 0.88256,6.52382 -1.45146,8.85602 -2.33502,2.3313 -5.88696,2.8707 -8.86652,1.4488 -0.43892,-0.1965 -0.95397,-0.03 -1.29206,0.3127 l -2.16992,2.1664 c -0.25505,0.2498 -0.3806,0.6023 -0.34007,0.9558 0.0405,0.3545 0.24319,0.6695 0.54767,0.8541 5.1129,3.0945 11.679,2.3056 15.91109,-1.9116 4.23208,-4.2162 5.03876,-10.77258 1.9554,-15.88728 -0.17696,-0.31104 -0.48935,-0.52234 -0.84425,-0.57171 -0.35489,-0.0504 -0.71276,0.0681 -0.96781,0.31794 z m -18.46655,-2.30559 c -4.25284,4.20042 -5.08621,10.75774 -2.01965,15.88534 0.17695,0.312 0.48835,0.5233 0.84325,0.5727 0.3549,0.05 0.71276,-0.068 0.9688,-0.319 l 2.12346,-2.1091 c 0.38455,-0.3367 0.57215,-0.8847 0.35862,-1.3488 -1.42256,-2.976 -0.88356,-6.52373 1.45146,-8.85593 2.33402,-2.33126 5.88694,-2.87084 8.86552,-1.44997 0.43398,0.19452 0.94211,0.033 1.28119,-0.29971 l 2.18178,-2.1792 c 0.25505,-0.24883 0.3806,-0.60133 0.34007,-0.95581 -0.0415,-0.35349 -0.24319,-0.66847 -0.54767,-0.85411 -5.1218,-3.06786 -11.67801,-2.25523 -15.89329,1.97185 z"
id="path9751"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"
style="fill:url(#linearGradient9753);stroke-width:0.987988"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g><g
id="g9767"
transform="translate(95.04653,2.2707362)"><g
id="g9763"
transform="translate(42.333011,90.667253)"><path
fill="url(#a)"
d="M 72.096614,883.74485 H 50.096615 c -2.76,0 -5,-2.24 -5,-5 v -21.99996 c 0,-2.759 2.24,-5 5,-5 h 21.999999 c 2.759,0 5,2.241 5,5 v 21.99996 c 0,2.76 -2.241,5 -5,5 z"
id="path9761"
style="isolation:isolate;fill:url(#linearGradient9769)" /></g><path
fill="url(#a)"
d="m 111.31998,952.54667 -1.8068,1.79545 c -0.31787,0.2849 -0.47158,0.7436 -0.29341,1.13122 1.20371,2.51824 0.74678,5.52022 -1.22816,7.49362 -1.97578,1.97264 -4.98127,2.42905 -7.50244,1.22591 -0.37139,-0.16627 -0.807201,-0.0254 -1.093271,0.26459 l -1.83609,1.8331 c -0.21582,0.21137 -0.32205,0.50964 -0.28775,0.80876 0.0343,0.29996 0.20578,0.5665 0.46341,0.7227 4.326301,2.61842 9.882231,1.95089 13.463231,-1.61751 3.58099,-3.56755 4.26356,-9.11527 1.65457,-13.44309 -0.14974,-0.26324 -0.41407,-0.44203 -0.71437,-0.48375 -0.30029,-0.0423 -0.6031,0.0575 -0.81892,0.269 z m -15.625531,-1.9509 c -3.59857,3.55419 -4.30372,9.10275 -1.70895,13.44149 0.14974,0.264 0.41323,0.44279 0.71352,0.48459 0.3003,0.0423 0.60312,-0.0575 0.81977,-0.26992 l 1.79676,-1.78462 c 0.3254,-0.2849 0.48414,-0.7486 0.30345,-1.1413 -1.2037,-2.51815 -0.74762,-5.52013 1.22816,-7.49353 1.974941,-1.97255 4.981261,-2.42914 7.501591,-1.22684 0.36722,0.16458 0.79717,0.0279 1.08409,-0.25367 l 1.84612,-1.84394 c 0.21581,-0.21052 0.32205,-0.50879 0.28775,-0.80875 -0.0351,-0.29912 -0.20577,-0.56557 -0.46341,-0.7227 -4.33383,-2.59583 -9.881401,-1.90825 -13.448171,1.66853 z"
id="path9765"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.83599"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g><g
id="g9825"
transform="translate(-74.123318,-7.4609904)"><g
id="g9821"
transform="translate(42.333011,90.667253)" /></g><g
id="g9889"
transform="translate(-59.550549,66.716063)"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="888"
inkscape:export-ydpi="888"><rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="rect9885"
width="32"
height="32"
x="66.992996"
y="897.48401" /><path
fill="url(#a)"
d="m 90.883358,907.61856 -1.806809,1.79544 c -0.317866,0.2849 -0.471579,0.74359 -0.293404,1.13126 1.203704,2.51817 0.746781,5.52016 -1.228158,7.49356 -1.975786,1.97264 -4.981274,2.42905 -7.502443,1.22591 -0.371394,-0.16627 -0.807201,-0.0254 -1.093279,0.26459 l -1.836086,1.83311 c -0.215813,0.21137 -0.322046,0.50964 -0.287751,0.80875 0.0343,0.29996 0.205775,0.5665 0.463413,0.7227 4.3263,2.61843 9.88223,1.9509 13.463229,-1.61751 3.580991,-3.56755 4.263566,-9.11526 1.654569,-13.44309 -0.149735,-0.26318 -0.414065,-0.44198 -0.714365,-0.48375 -0.300292,-0.0426 -0.603105,0.0576 -0.818916,0.26903 z m -15.625541,-1.95089 c -3.59856,3.5542 -4.303718,9.10271 -1.70894,13.44145 0.149731,0.264 0.413224,0.44279 0.713522,0.48459 0.300298,0.0423 0.603106,-0.0575 0.819756,-0.26992 l 1.796771,-1.78463 c 0.325392,-0.2849 0.484128,-0.74859 0.303447,-1.14129 -1.203702,-2.51815 -0.747621,-5.52008 1.228157,-7.49348 1.974941,-1.97261 4.981262,-2.42918 7.501598,-1.2269 0.367214,0.16459 0.79717,0.0279 1.084084,-0.2536 l 1.846121,-1.84394 c 0.215812,-0.21055 0.322047,-0.50882 0.287752,-0.80876 -0.03511,-0.29911 -0.205776,-0.56563 -0.463413,-0.72271 -4.333831,-2.59589 -9.881397,-1.90827 -13.44817,1.66849 z"
id="path9829"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="2599.3848"
inkscape:export-ydpi="2599.3848"
style="fill:url(#linearGradient9831);stroke-width:0.987992"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g><path
fill="url(#a)"
d="m 113.94383,1029.4828 -2.13532,2.1219 c -0.37566,0.3367 -0.55732,0.8788 -0.34675,1.3369 1.42256,2.976 0.88256,6.5238 -1.45146,8.856 -2.33502,2.3313 -5.88696,2.8707 -8.86652,1.4488 -0.43892,-0.1965 -0.95397,-0.03 -1.292059,0.3127 l -2.16992,2.1664 c -0.255052,0.2498 -0.3806,0.6023 -0.340069,0.9558 0.04053,0.3545 0.243189,0.6695 0.54767,0.8541 5.112898,3.0945 11.678998,2.3056 15.911088,-1.9116 4.23208,-4.2162 5.03876,-10.7726 1.9554,-15.8873 -0.17696,-0.311 -0.48935,-0.5223 -0.84425,-0.5717 -0.35489,-0.05 -0.71276,0.068 -0.96781,0.318 z m -18.466548,-2.3056 c -4.252844,4.2004 -5.086212,10.7577 -2.019656,15.8853 0.176955,0.312 0.488355,0.5233 0.843253,0.5727 0.354898,0.05 0.712762,-0.068 0.968803,-0.319 l 2.123456,-2.1091 c 0.384555,-0.3367 0.572152,-0.8847 0.35862,-1.3488 -1.422557,-2.976 -0.883553,-6.5237 1.451458,-8.8559 2.334024,-2.3313 5.886944,-2.8708 8.865524,-1.45 0.43398,0.1945 0.94211,0.033 1.28119,-0.2997 l 2.18178,-2.1792 c 0.25505,-0.2488 0.3806,-0.6013 0.34007,-0.9558 -0.0415,-0.3535 -0.24319,-0.6685 -0.54767,-0.8541 -5.1218,-3.0679 -11.678013,-2.2552 -15.893291,1.9718 z"
id="path357"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="59.076923"
inkscape:export-ydpi="59.076923"
style="fill:#1a1a1a;stroke-width:0.987988"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /><path
fill="url(#a)"
d="m 34.578696,1018.4726 -2.13532,2.1219 c -0.37566,0.3367 -0.55732,0.8788 -0.34675,1.3369 1.42256,2.976 0.88256,6.5238 -1.45146,8.856 -2.33502,2.3313 -5.88696,2.8707 -8.86652,1.4488 -0.43892,-0.1965 -0.95397,-0.03 -1.292059,0.3127 l -2.16992,2.1664 c -0.255052,0.2498 -0.3806,0.6023 -0.340069,0.9558 0.04053,0.3545 0.243189,0.6695 0.54767,0.8541 5.112898,3.0945 11.678998,2.3056 15.911088,-1.9116 4.23208,-4.2162 5.03876,-10.7726 1.9554,-15.8873 -0.17696,-0.311 -0.48935,-0.5223 -0.84425,-0.5717 -0.35489,-0.05 -0.71276,0.068 -0.96781,0.318 z m -18.466548,-2.3056 c -4.252844,4.2004 -5.086212,10.7577 -2.019656,15.8853 0.176955,0.312 0.488355,0.5233 0.843253,0.5727 0.354898,0.05 0.712762,-0.068 0.968803,-0.319 l 2.123456,-2.1091 c 0.384555,-0.3367 0.572152,-0.8847 0.35862,-1.3488 -1.422557,-2.976 -0.883553,-6.5237 1.451458,-8.8559 2.334024,-2.3313 5.886944,-2.8708 8.865524,-1.45 0.43398,0.1945 0.94211,0.033 1.28119,-0.2997 l 2.18178,-2.1792 c 0.25505,-0.2488 0.3806,-0.6013 0.34007,-0.9558 -0.0415,-0.3535 -0.24319,-0.6685 -0.54767,-0.8541 -5.1218,-3.0679 -11.678013,-2.2552 -15.893291,1.9718 z"
id="path408"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="177.23076"
inkscape:export-ydpi="177.23076"
style="fill:#ffffff;stroke-width:0.987988"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /><g
id="g763"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="768"
inkscape:export-ydpi="768"
transform="translate(-49.90362,15.786265)"><g
id="g761"
inkscape:export-filename="./g369.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"><rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="rect757"
width="32"
height="32"
x="14.984506"
y="888.74823"
rx="5"
ry="5" /><path
fill="url(#a)"
d="m 40.309479,897.8163 -2.13532,2.12189 c -0.37566,0.3367 -0.557321,0.87878 -0.34675,1.33694 1.422559,2.97602 0.882559,6.52382 -1.45146,8.85602 -2.33502,2.33131 -5.88696,2.8707 -8.866524,1.44881 -0.43892,-0.1965 -0.953964,-0.03 -1.292057,0.31269 l -2.169919,2.16641 c -0.255052,0.2498 -0.3806,0.6023 -0.34007,0.95579 0.04054,0.3545 0.243189,0.6695 0.54767,0.8541 5.1129,3.09451 11.678999,2.30561 15.911089,-1.9116 4.232081,-4.2162 5.03876,-10.77258 1.9554,-15.88729 -0.17696,-0.31103 -0.48935,-0.52234 -0.84425,-0.5717 -0.35489,-0.0503 -0.71276,0.0681 -0.967809,0.31794 z M 21.84293,895.5107 c -4.252844,4.20042 -5.086212,10.75775 -2.019657,15.88535 0.176955,0.312 0.488356,0.5233 0.843254,0.5727 0.354897,0.05 0.712761,-0.0679 0.968802,-0.319 l 2.123457,-2.1091 c 0.384554,-0.3367 0.572151,-0.8847 0.358619,-1.3488 -1.422557,-2.976 -0.883552,-6.52373 1.451458,-8.85593 2.334022,-2.33127 5.886947,-2.87085 8.865525,-1.44997 0.433981,0.19451 0.94211,0.033 1.281191,-0.29971 l 2.181779,-2.1792 c 0.255051,-0.24884 0.380601,-0.60134 0.340071,-0.95581 -0.04149,-0.35349 -0.24319,-0.66847 -0.54767,-0.85411 -5.121801,-3.06787 -11.678015,-2.25523 -15.893292,1.97185 z"
id="path759"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="2599.3848"
inkscape:export-ydpi="2599.3848"
style="fill:url(#linearGradient765);stroke-width:0.987991"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g></g><path
style="fill:#000000;stroke-width:1"
id="path2240"
d="" /><g
id="g340"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="2496"
inkscape:export-ydpi="2496"
transform="translate(-53.94018,69.130411)"><g
id="g369"
inkscape:export-filename="./g369.png"
inkscape:export-xdpi="3733.2917"
inkscape:export-ydpi="3733.2917"
transform="translate(-2.1094037,-5.1776263)"><rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="rect336"
width="32"
height="32"
x="14.984506"
y="888.74823"
rx="7"
ry="7" /><path
fill="url(#a)"
d="m 40.309479,897.8163 -2.13532,2.12189 c -0.37566,0.3367 -0.557321,0.87878 -0.34675,1.33694 1.422559,2.97602 0.882559,6.52382 -1.45146,8.85602 -2.33502,2.33131 -5.88696,2.8707 -8.866524,1.44881 -0.43892,-0.1965 -0.953964,-0.03 -1.292057,0.31269 l -2.169919,2.16641 c -0.255052,0.2498 -0.3806,0.6023 -0.34007,0.95579 0.04054,0.3545 0.243189,0.6695 0.54767,0.8541 5.1129,3.09451 11.678999,2.30561 15.911089,-1.9116 4.232081,-4.2162 5.03876,-10.77258 1.9554,-15.88729 -0.17696,-0.31103 -0.48935,-0.52234 -0.84425,-0.5717 -0.35489,-0.0503 -0.71276,0.0681 -0.967809,0.31794 z M 21.84293,895.5107 c -4.252844,4.20042 -5.086212,10.75775 -2.019657,15.88535 0.176955,0.312 0.488356,0.5233 0.843254,0.5727 0.354897,0.05 0.712761,-0.0679 0.968802,-0.319 l 2.123457,-2.1091 c 0.384554,-0.3367 0.572151,-0.8847 0.358619,-1.3488 -1.422557,-2.976 -0.883552,-6.52373 1.451458,-8.85593 2.334022,-2.33127 5.886947,-2.87085 8.865525,-1.44997 0.433981,0.19451 0.94211,0.033 1.281191,-0.29971 l 2.181779,-2.1792 c 0.255051,-0.24884 0.380601,-0.60134 0.340071,-0.95581 -0.04149,-0.35349 -0.24319,-0.66847 -0.54767,-0.85411 -5.121801,-3.06787 -11.678015,-2.25523 -15.893292,1.97185 z"
id="path338"
inkscape:export-filename="../../Desktop/path9.png"
inkscape:export-xdpi="2599.3848"
inkscape:export-ydpi="2599.3848"
style="fill:url(#linearGradient342);stroke-width:0.987991"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g></g></svg>

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 48 B

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