Merge branch 'master' of https://github.com/rustdesk/rustdesk into opt_chat_overlay_and_fix_pageview_2
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
@ -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
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,4 +1,3 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask a question
|
||||
url: https://github.com/rustdesk/rustdesk/discussions/category_choices
|
||||
|
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project ((English only, Please).
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
33
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal 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
@ -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
|
4
.github/workflows/flutter-nightly.yml
vendored
@ -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
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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 |
|
||||
|
3
build.py
@ -322,8 +322,9 @@ def build_flutter_dmg(version, features):
|
||||
os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h')
|
||||
os.chdir('flutter')
|
||||
os.system('flutter build macos --release')
|
||||
os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk')
|
||||
os.system(
|
||||
"create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app")
|
||||
"create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
|
||||
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
|
||||
os.chdir("..")
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 8.7 KiB |
1
flutter/assets/chat.svg
Normal 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 |
24
flutter/assets/record_screen.svg
Normal 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 |
1
flutter/assets/voice_call.svg
Normal 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 |
1
flutter/assets/voice_call_waiting.svg
Normal 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 |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 669 B |
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 779 B After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 969 B |
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 978 B After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 926 B After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 6.3 KiB |
@ -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");
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
|
@ -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 {
|
||||
|
275
flutter/lib/models/peer_tab_model.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 978 B |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.8 KiB |
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 25 KiB |
@ -32,4 +32,4 @@
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 1.9 KiB |
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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() != "" {
|
||||
|
@ -1 +1 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
|
@ -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]
|
||||
|
@ -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>(
|
||||
|
BIN
res/128x128.png
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 75 B |
1
res/128x128.png
Symbolic link
@ -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 |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 10 KiB |
BIN
res/32x32.png
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 74 B |
1
res/32x32.png
Symbolic link
@ -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 |
BIN
res/64x64.png
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 74 B |
1
res/64x64.png
Symbolic link
@ -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
@ -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 |
Before Width: | Height: | Size: 12 KiB |
BIN
res/icon.ico
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 48 B |