mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-21 09:08:35 +08:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d06de00fb | ||
|
|
6f8af9d114 | ||
|
|
0a672f092a | ||
|
|
ef62f1db29 | ||
|
|
7f804a0e45 | ||
|
|
b2dff336ce | ||
|
|
a6571e71e4 | ||
|
|
81f711eb00 | ||
|
|
c8a8e06558 | ||
|
|
2c079f53a9 | ||
|
|
322ffe288e | ||
|
|
c340eb0e57 | ||
|
|
4e953291ed | ||
|
|
1dea5fee0e | ||
|
|
9f24b46fee | ||
|
|
0808c41a1c | ||
|
|
296c6df462 | ||
|
|
13ee3e907d | ||
|
|
ce7d794b4c | ||
|
|
fb10069632 | ||
|
|
43a7677644 | ||
|
|
58fa32d7ea | ||
|
|
934d6c3987 | ||
|
|
2d7c6ef21f | ||
|
|
99a97e6a6c | ||
|
|
017a10e8c8 | ||
|
|
41ffa8ba08 | ||
|
|
e029d00cfa | ||
|
|
268534d5e7 | ||
|
|
a7d2bc63f9 | ||
|
|
559115c43c | ||
|
|
1277c7d60c | ||
|
|
9b69c7e972 | ||
|
|
a903f710ea | ||
|
|
b75f4daa47 | ||
|
|
fef44ffa57 | ||
|
|
5a812e3b2f | ||
|
|
910dcf2036 | ||
|
|
44a28aa5bd | ||
|
|
f7f947beb9 | ||
|
|
d03a9e2baf | ||
|
|
ca22316e95 | ||
|
|
ef99c479aa | ||
|
|
fa9260c763 | ||
|
|
fab11c8ffa | ||
|
|
9bd9658a92 | ||
|
|
213880c14d | ||
|
|
0550397046 | ||
|
|
f7a5a506f6 | ||
|
|
0f34c50bd2 | ||
|
|
055826e26f | ||
|
|
a30582c840 | ||
|
|
d106d97b99 | ||
|
|
d4410e78e2 | ||
|
|
e3fcc6cce3 | ||
|
|
265d08fc3b | ||
|
|
7c8329c5c6 | ||
|
|
d3947c9a19 | ||
|
|
cd99331668 | ||
|
|
472e18b10a | ||
|
|
d443f5de28 | ||
|
|
3242d132f6 | ||
|
|
e66d2facd4 | ||
|
|
f15b8dc0da | ||
|
|
3275824aec | ||
|
|
965cb704ec | ||
|
|
938e165470 | ||
|
|
9058ef3344 | ||
|
|
ed39cc3038 | ||
|
|
a77752c4cb | ||
|
|
c9940957f0 | ||
|
|
c90d72d720 | ||
|
|
2c30bd9d24 | ||
|
|
f2dc8e21a8 | ||
|
|
6a0da9cf09 | ||
|
|
d55974c352 | ||
|
|
a898c22f4b | ||
|
|
b82e8bedfc | ||
|
|
7453cefd94 | ||
|
|
1ed6b958cb | ||
|
|
57896ab176 | ||
|
|
5c370b3914 | ||
|
|
182e35adc7 | ||
|
|
d0a360fd80 | ||
|
|
2fbc0625de | ||
|
|
d3d20a4e20 | ||
|
|
2c088d3504 | ||
|
|
6f9728f2d4 | ||
|
|
30552fd202 | ||
|
|
9826c4e943 | ||
|
|
bb9445bd0f | ||
|
|
1f7e66f4cb | ||
|
|
2a34e918a0 | ||
|
|
21c0d924ab | ||
|
|
c8d5ee6565 | ||
|
|
3d8fc7ca7b | ||
|
|
246b5b93f8 | ||
|
|
2183c0980b | ||
|
|
4ae301710d | ||
|
|
5f9390c210 | ||
|
|
0f3a03aab7 | ||
|
|
02f455b0cc | ||
|
|
ffddf60184 | ||
|
|
482840b8bb | ||
|
|
a3637cf2b6 | ||
|
|
48669cdb34 | ||
|
|
a953845ba7 | ||
|
|
8d71534839 | ||
|
|
d110118961 | ||
|
|
fa1ed2bc0c | ||
|
|
3f28978dad | ||
|
|
02cd121465 | ||
|
|
5481c300b2 | ||
|
|
7b75257a4a | ||
|
|
c02e5cad73 | ||
|
|
dee03c0f9f | ||
|
|
d1159764f6 | ||
|
|
eacb07988d | ||
|
|
a375766ac2 | ||
|
|
9b9276e752 | ||
|
|
753a2ab2b7 | ||
|
|
0cef5f79ee | ||
|
|
b11a8dfe54 | ||
|
|
2d1c94f1ef | ||
|
|
e14e850e10 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-22.04 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
|
||||
30
.github/workflows/flutter-build.yml
vendored
30
.github/workflows/flutter-build.yml
vendored
@@ -39,7 +39,7 @@ env:
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.2"
|
||||
VERSION: "1.4.4"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -392,6 +392,13 @@ jobs:
|
||||
ls -l ./libs/portable/Runner.res;
|
||||
fi
|
||||
|
||||
- name: Upload unsigned
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
||||
path: Release
|
||||
|
||||
- name: Sign rustdesk files
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||
shell: bash
|
||||
@@ -756,6 +763,7 @@ jobs:
|
||||
needs:
|
||||
- build-for-macOS
|
||||
- build-for-windows-flutter
|
||||
- build-for-windows-sciter
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
steps:
|
||||
@@ -777,9 +785,15 @@ jobs:
|
||||
name: rustdesk-unsigned-windows-x86_64
|
||||
path: ./windows-x86_64/
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: rustdesk-unsigned-windows-x86
|
||||
path: ./windows-x86/
|
||||
|
||||
- name: Combine unsigned app
|
||||
run: |
|
||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64
|
||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
|
||||
|
||||
- name: Publish unsigned app
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -987,6 +1001,8 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# Increase Gradle JVM memory for CI builds
|
||||
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
case ${{ matrix.job.target }} in
|
||||
@@ -1194,6 +1210,8 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# Increase Gradle JVM memory for CI builds
|
||||
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
@@ -1429,7 +1447,8 @@ jobs:
|
||||
rpm \
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils
|
||||
xz-utils \
|
||||
libssl-dev
|
||||
# we have libopus compiled by us.
|
||||
apt-get remove -y libopus-dev || true
|
||||
# output devs
|
||||
@@ -1709,12 +1728,13 @@ jobs:
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils \
|
||||
zip
|
||||
zip \
|
||||
libssl-dev
|
||||
# arm-linux needs CMake and vcokg built from source as there
|
||||
# are no prebuilts available from Kitware and Microsoft
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
# install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
|
||||
apt-get install -y gcc-8 g++-8 libssl-dev
|
||||
apt-get install -y gcc-8 g++-8
|
||||
# bootstrap CMake amd add it to PATH
|
||||
git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
|
||||
pushd /tmp/cmake
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.2"
|
||||
VERSION: "1.4.4"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: "1.4.2"
|
||||
release-tag: "1.4.2"
|
||||
version: "1.4.4"
|
||||
release-tag: "1.4.4"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
510
Cargo.lock
generated
510
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.2"
|
||||
version = "1.4.4"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -83,6 +83,8 @@ shutdown_hooks = "0.1"
|
||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||
stunclient = "0.4"
|
||||
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"}
|
||||
reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
|
||||
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||
@@ -165,13 +167,6 @@ fontdb = "0.23"
|
||||
bytemuck = "1.23"
|
||||
ttf-parser = "0.25"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||
@@ -192,6 +187,9 @@ termios = "0.3"
|
||||
terminfo = "0.8"
|
||||
winit = "0.30"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
jni = "0.21"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>]<br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
|
||||
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.2
|
||||
version: 1.4.4
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.2
|
||||
version: 1.4.4
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
85
docs/CODE_OF_CONDUCT-RO.md
Normal file
85
docs/CODE_OF_CONDUCT-RO.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Codul de Conduită al Contributorilor
|
||||
|
||||
## Angajamentul Nostru
|
||||
|
||||
Noi, ca membri, contribuitori și lideri, ne angajăm să facem ca participarea în comunitatea noastră să fie o experiență fără hărțuire pentru toată lumea, indiferent de vârstă, dimensiunea corpului, dizabilități vizibile sau invizibile, etnie, caracteristici sexuale, identitate și exprimare de gen, nivel de experiență, educație, statut socio-economic, naționalitate, aspect personal, rasă, religie sau identitate și orientare sexuală.
|
||||
|
||||
Ne angajăm să acționăm și să interacționăm în moduri care contribuie la o comunitate deschisă, primitoare, diversă, incluzivă și sănătoasă.
|
||||
|
||||
## Standardele Noastre
|
||||
|
||||
Exemple de comportamente care contribuie la un mediu pozitiv pentru comunitatea noastră includ:
|
||||
|
||||
* Demonstrarea empatiei și a bunătății față de ceilalți
|
||||
* Respectarea opiniilor, punctelor de vedere și experiențelor diferite
|
||||
* Oferirea și acceptarea cu grație a feedback-ului constructiv
|
||||
* Asumarea responsabilității și cererea de scuze celor afectați de greșelile noastre și învățarea din experiență
|
||||
* Concentrarea pe ceea ce este cel mai bun nu doar pentru noi ca indivizi, ci pentru întreaga comunitate
|
||||
|
||||
Exemple de comportamente inacceptabile includ:
|
||||
|
||||
* Utilizarea limbajului sau imaginilor sexualizate, precum și atenția sau avansurile sexuale de orice fel
|
||||
* Trollare, insulte sau comentarii denigratoare și atacuri personale sau politice
|
||||
* Hărțuire publică sau privată
|
||||
* Publicarea informațiilor private ale altora, cum ar fi adresa fizică sau de e-mail, fără permisiunea explicită
|
||||
* Alte comportamente care ar putea fi considerate inadecvate într-un cadru profesional
|
||||
|
||||
## Responsabilități de Aplicare
|
||||
|
||||
Liderii comunității sunt responsabili pentru clarificarea și aplicarea standardelor noastre de comportament acceptabil și vor lua măsuri corective adecvate și echitabile ca răspuns la orice comportament pe care îl consideră inadecvat, amenințător, ofensator sau dăunător.
|
||||
|
||||
Liderii comunității au dreptul și responsabilitatea de a elimina, edita sau respinge comentarii, commit-uri, cod, editări wiki, probleme și alte contribuții care nu se aliniază acestui Cod de Conduită și vor comunica motivele pentru deciziile de moderare atunci când este cazul.
|
||||
|
||||
## Domeniu de Aplicare
|
||||
|
||||
Acest Cod de Conduită se aplică în toate spațiile comunității și se aplică și atunci când un individ reprezintă oficial comunitatea în spații publice.
|
||||
Exemple de reprezentare a comunității includ utilizarea unei adrese de e-mail oficiale, postarea printr-un cont oficial de social media sau acționarea ca reprezentant desemnat la un eveniment online sau offline.
|
||||
|
||||
## Aplicare
|
||||
|
||||
Cazurile de comportament abuziv, hărțuitor sau altfel inacceptabil pot fi raportate liderilor comunității responsabili pentru aplicare la [info@rustdesk.com](mailto:info@rustdesk.com).
|
||||
Toate plângerile vor fi revizuite și investigate prompt și corect.
|
||||
|
||||
Toți liderii comunității sunt obligați să respecte confidențialitatea și securitatea persoanei care raportează orice incident.
|
||||
|
||||
## Ghiduri de Aplicare
|
||||
|
||||
Liderii comunității vor urma aceste Ghiduri privind Impactul Comunității pentru a stabili consecințele pentru orice acțiune pe care o consideră o încălcare a acestui Cod de Conduită:
|
||||
|
||||
### 1. Corectare
|
||||
|
||||
**Impact asupra comunității**: Utilizarea limbajului neadecvat sau alte comportamente considerate neprofesionale sau nedorite în comunitate.
|
||||
|
||||
**Consecință**: O avertizare scrisă și privată din partea liderilor comunității, oferind claritate asupra naturii încălcării și o explicație despre motivul pentru care comportamentul a fost inadecvat. Poate fi cerută o scuză publică.
|
||||
|
||||
### 2. Avertisment
|
||||
|
||||
**Impact asupra comunității**: Încălcare printr-un incident singular sau o serie de acțiuni.
|
||||
|
||||
**Consecință**: Un avertisment cu consecințe pentru continuarea comportamentului. Nicio interacțiune cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, pentru o perioadă specificată. Aceasta include evitarea interacțiunilor în spațiile comunității, precum și pe canale externe, cum ar fi rețelele sociale. Încălcarea acestor termeni poate duce la o suspendare temporară sau permanentă.
|
||||
|
||||
### 3. Suspendare Temporară
|
||||
|
||||
**Impact asupra comunității**: O încălcare serioasă a standardelor comunității, inclusiv comportament neadecvat susținut.
|
||||
|
||||
**Consecință**: Suspendare temporară de la orice tip de interacțiune sau comunicare publică cu comunitatea pentru o perioadă specificată. Nicio interacțiune publică sau privată cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, nu este permisă în această perioadă. Încălcarea acestor termeni poate duce la o interdicție permanentă.
|
||||
|
||||
### 4. Interdicție Permanentă
|
||||
|
||||
**Impact asupra comunității**: Demonstrând un tipar de încălcare a standardelor comunității, inclusiv comportament neadecvat susținut, hărțuire a unei persoane sau agresiune față de sau denigrare a unor grupuri de persoane.
|
||||
|
||||
**Consecință**: Interdicție permanentă de la orice tip de interacțiune publică în cadrul comunității.
|
||||
|
||||
## Atribuire
|
||||
|
||||
Acest Cod de Conduită este adaptat din [Contributor Covenant][homepage], versiunea 2.0, disponibil la [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Ghidurile privind Impactul Comunității au fost inspirate de [scara de aplicare a codului de conduită Mozilla][Mozilla CoC].
|
||||
|
||||
Pentru răspunsuri la întrebări frecvente despre acest cod de conduită, vezi FAQ la [https://www.contributor-covenant.org/faq][FAQ]. Traduceri sunt disponibile la [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
31
docs/CONTRIBUTING-RO.md
Normal file
31
docs/CONTRIBUTING-RO.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contribuții la RustDesk
|
||||
|
||||
RustDesk primește cu plăcere contribuții din partea tuturor. Iată ghidurile dacă te gândești să ne ajuți:
|
||||
|
||||
## Contribuții
|
||||
|
||||
Contribuțiile la RustDesk sau la dependențele sale ar trebui făcute sub forma de pull request-uri pe GitHub. Fiecare pull request va fi revizuit de un contributor principal (cineva cu permisiunea de a aplica patch-uri) și fie va fi integrat în arborele principal, fie vor fi oferite sugestii pentru modificările necesare. Toate contribuțiile trebuie să urmeze acest format, chiar și cele ale contributorilor principali.
|
||||
|
||||
Dacă dorești să lucrezi la o problemă, te rugăm să o revendici mai întâi comentând pe GitHub issue-ul pe care vrei să lucrezi. Aceasta previne eforturi duplicate din partea contributorilor asupra aceleiași probleme.
|
||||
|
||||
## Lista de verificare pentru Pull Request
|
||||
|
||||
- Creează un branch din branch-ul `master` și, dacă este necesar, fă rebase la branch-ul `master` curent înainte de a trimite pull request-ul. Dacă nu se poate integra curat cu `master`, ți se poate cere să faci rebase la modificările tale.
|
||||
|
||||
- Commit-urile ar trebui să fie cât mai mici posibil, asigurând totodată că fiecare commit este corect independent (adică fiecare commit ar trebui să compileze și să treacă testele).
|
||||
|
||||
- Commit-urile trebuie să fie însoțite de un semnătura Developer Certificate of Origin (http://developercertificate.org), care indică faptul că tu (și angajatorul tău, dacă este cazul) ești de acord să respecți termenii [licenței proiectului](../LICENCE). În git, aceasta este opțiunea `-s` la `git commit`.
|
||||
|
||||
- Dacă patch-ul tău nu este revizuit sau ai nevoie ca o anumită persoană să-l revizuiască, poți @-reply unui reviewer cerând o revizuire în pull request sau într-un comentariu, sau poți solicita o revizuire prin [email](mailto:info@rustdesk.com).
|
||||
|
||||
- Adaugă teste relevante pentru bug-ul corectat sau pentru funcționalitatea nouă.
|
||||
|
||||
Pentru instrucțiuni specifice git, vezi [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## Conduită
|
||||
|
||||
[Codul de Conduită RustDesk](https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md)
|
||||
|
||||
## Comunicare
|
||||
|
||||
Contributorii RustDesk frecventează [Discord](https://discord.gg/nDceKgxnkV).
|
||||
181
docs/README-RO.md
Normal file
181
docs/README-RO.md
Normal file
@@ -0,0 +1,181 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - desktopul tău la distanță"><br>
|
||||
<a href="../README.md#raw-steps-to-build">Construire</a> •
|
||||
<a href="../README.md#how-to-build-with-docker">Docker</a> •
|
||||
<a href="../README.md#file-structure">Structură</a> •
|
||||
<a href="../README.md#snapshot">Capturi</a><br>
|
||||
[<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-FA.md">فارسی</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>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>] | [<a href="README-RO.md">Română</a>]<br>
|
||||
<b>Avem nevoie de ajutorul tău pentru a traduce acest README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> și <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> în limba ta maternă</b>
|
||||
</p>
|
||||
|
||||
> [!Atenție]
|
||||
> **Declinare de responsabilitate privind utilizarea abuzivă:** <br>
|
||||
> Dezvoltatorii RustDesk nu susțin sau aprobă utilizarea neetică sau ilegală a acestui software. Utilizarea abuzivă, cum ar fi accesul neautorizat, controlul sau invadarea intimității, este strict împotriva regulilor noastre. Autorii nu sunt responsabili pentru utilizarea necorespunzătoare a aplicației.
|
||||
|
||||
|
||||
Conversați cu noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Încă o soluție de desktop la distanță scrisă în Rust. Funcționează imediat, fără configurare necesară. Ai control total asupra datelor tale, fără probleme de securitate. Poți folosi serverul nostru de rendezvous/relay, [să-ți configurezi propriul server](https://rustdesk.com/server) sau [să scrii propriul server de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk primește contribuții de la oricine. Vezi [CONTRIBUTING.md](../docs/CONTRIBUTING.md) pentru ajutor la început.
|
||||
|
||||
[**ÎNTREBĂRI FRECVENTE (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**DESCĂRCARE BINARE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**BUILD NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
[<img src="https://flathub.org/api/badge?svg&locale=en"
|
||||
alt="Get it on Flathub"
|
||||
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
|
||||
|
||||
## Dependențe
|
||||
|
||||
Versiunile desktop folosesc Flutter sau Sciter (depreciat) pentru interfață; acest ghid este pentru Sciter doar, deoarece este mai ușor și mai prietenos pentru început. Vezi [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) pentru construire cu Flutter.
|
||||
|
||||
Te rugăm să descarci singur librăria dinamică Sciter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Pași pentru construire (Raw Steps to build)
|
||||
|
||||
- Pregătește mediul de dezvoltare Rust și mediul de construire C++
|
||||
|
||||
- Instalează [vcpkg](https://github.com/microsoft/vcpkg) și setează corect variabila de mediu `VCPKG_ROOT`
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Linux/macOS: vcpkg install libvpx libyuv opus aom
|
||||
|
||||
- rulează `cargo run`
|
||||
|
||||
## [Construire](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Cum se construiește pe Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Instalează vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2023.04.15
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Repară libvpx (Pentru Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Cum să construiești cu Docker
|
||||
|
||||
Începe prin clonarea repository-ului și construirea imaginii Docker:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Apoi, de fiecare dată când trebuie să construiești aplicația, rulează comanda următoare:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Reține că prima construire poate dura mai mult până când dependențele sunt în cache; construirile ulterioare vor fi mai rapide. De asemenea, dacă trebuie să specifici argumente diferite comenzii de build, le poți adăuga la finalul comenzii în poziția `<OPTIONAL-ARGS>`. De exemplu, pentru a construi o versiune optimizată de release, adaugă `--release`. Executabilul rezultat va fi disponibil în folderul `target` pe sistemul tău, și poate fi rulat cu:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Sau, dacă rulezi un executabil release:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, altfel aplicația poate să nu găsească resursele necesare. De asemenea, reține că alte subcomenzi cargo, cum ar fi `install` sau `run`, nu sunt acceptate în prezent prin această metodă, deoarece ar instala sau rula programul în interiorul containerului în loc de gazdă.
|
||||
|
||||
## Structura fișierelor
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funcții fs pentru transfer de fișiere și alte funcții utilitare
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: capturare ecran
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control tastatură/mouse specific platformei
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementare copy/paste pentru fișiere pentru Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interfață Sciter învechită (depreciată)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servicii audio/clipboard/input/video și conexiuni de rețea
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inițiază o conexiune peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
|
||||
|
||||
## Capturi de ecran
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -7,34 +7,37 @@
|
||||
<a href="#file-structure">Dosya Yapısı</a> •
|
||||
<a href="#snapshot">Ekran Görüntüleri</a><br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ve <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Belge</a>'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
|
||||
<b>README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ve <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Dökümantasyonu</a>'nu ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
|
||||
</p>
|
||||
|
||||
|
||||
> [!Dikkat]
|
||||
> **Yanlış Kullanım Uyarısı:** <br>
|
||||
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
|
||||
|
||||
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kullanıma hazır, hiçbir yapılandırma gerektirmez. Verilerinizin tam kontrolünü elinizde tutarsınız ve güvenlikle ilgili endişeleriniz olmaz. Kendi buluş/iletme sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi buluş/iletme sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Rust dilinde yazılmış, başka bir uzak masaüstü yazılımı daha. Hiçbir yapılandırma gerekmeksizin, hemen kullanıma hazır. Güvenlik konusunda hiçbir endişe duymadan, verileriniz üzerinde tam kontrole sahip olun. Kendi rendezvous/relay sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi rendezvous/relay sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın.
|
||||
RustDesk, herkesin katkısına açıktır. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın.
|
||||
|
||||
[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**BİNARİ İNDİR**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**BINARY İNDİR**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
[**NIGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="F-Droid'de Alın"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Bağımlılıklar
|
||||
## Gereksinimler
|
||||
|
||||
Masaüstü sürümleri GUI için
|
||||
|
||||
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
|
||||
Masaüstü sürümleri GUI için; [Sciter](https://sciter.com/)(kaldırılacak) veya Flutter kullanır. Sciter daha kolay ve başlamak için daha dostcanlısı, bundan dolayı bu kılavuz sadece Sciter içindir. Flutter sürümünü derlemek için [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)'ımıza bakın.
|
||||
|
||||
Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
|
||||
|
||||
@@ -46,7 +49,7 @@ Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
|
||||
|
||||
- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın.
|
||||
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` çevresel değişkenini doğru bir şekilde ayarlayın.
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` ortam değişkenini doğru bir şekilde ayarlayın.
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Linux/macOS: vcpkg install libvpx libyuv opus aom
|
||||
@@ -123,7 +126,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
|
||||
## Docker ile Derleme Nasıl Yapılır
|
||||
|
||||
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
|
||||
Önce repository'i klonlayın ve Docker container'ını oluşturun.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@@ -131,44 +134,40 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
|
||||
Ardından, uygulamayı her derlemeniz gerektiğinde aşağıdaki komutu çalıştırın:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
İlk derleme, bağımlılıklar önbelleğe alınmadan önce daha uzun sürebilir, sonraki derlemeler daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu
|
||||
|
||||
komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
|
||||
Bilin ki ilk derlemeniz gereksinimlerin önbelleği yüklenmesinden ötürü uzun sürebilir, sonraki derlemeleriniz daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu komutun sonunda ki `<OPTIONAL-ARGS>` yerine yazabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan çalıştırılabilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir olacaktır:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
|
||||
Veya, yayım çalıştırılabilir dosyası için:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır ve ana makinede değil.
|
||||
Lütfen bu komutları RustDesk reposunun root klasöründe çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır, ana makinede değil.
|
||||
|
||||
## Dosya Yapısı
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodlayıcı, yapılandırma, tcp/udp sarmalayıcı, protobuf, dosya transferi için fs işlevleri ve diğer bazı yardımcı işlevler
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, dosya transferi için fs fonksiyonları ve diğer bazı yardımcı işlevler
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: platforma özgü kopyala/yapıştır implementasyonları.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Eski Sciter UI (kaldırılacak)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pano/input/video servisleri ve ağ bağlantıları
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Eşli bağlantı başlat
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript
|
||||
|
||||
> [!Dikkat]
|
||||
> **Yanlış Kullanım Uyarısı:** <br>
|
||||
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
|
||||
|
||||
## Ekran Görüntüleri
|
||||
|
||||
|
||||
9
docs/SECURITY-RO.md
Normal file
9
docs/SECURITY-RO.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Politica de Securitate
|
||||
|
||||
## Raportarea unei Vulnerabilități
|
||||
|
||||
Acordăm o mare importanță securității proiectului. Încurajăm toți utilizatorii să ne raporteze orice vulnerabilități pe care le descoperă.
|
||||
Dacă găsești o vulnerabilitate de securitate în proiectul RustDesk, te rugăm să o raportezi responsabil trimițând un e-mail la info@rustdesk.com.
|
||||
|
||||
În acest moment, nu avem un program de recompense pentru descoperirea de bug-uri. Suntem o echipă mică care încearcă să rezolve o problemă mare.
|
||||
Te rugăm să raportezi orice vulnerabilitate în mod responsabil, astfel încât să putem continua să construim o aplicație sigură pentru întreaga comunitate.
|
||||
@@ -55,8 +55,7 @@
|
||||
],
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--filesystem=home",
|
||||
"--device=dri",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id "com.google.protobuf" version "0.9.4"
|
||||
id "com.android.application"
|
||||
@@ -30,8 +32,37 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
// Add rustls-platform-verifier Android support
|
||||
String findRustlsPlatformVerifierMavenDir() {
|
||||
def dependencyText = providers.exec {
|
||||
it.workingDir = new File("../..")
|
||||
commandLine("cargo", "metadata", "--format-version", "1")
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
def dependencyJson = new JsonSlurper().parseText(dependencyText)
|
||||
def pkg = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }
|
||||
|
||||
if (pkg == null) {
|
||||
throw new GradleException("rustls-platform-verifier-android package not found in cargo metadata!")
|
||||
}
|
||||
|
||||
def manifestPath = file(pkg.manifest_path)
|
||||
def mavenDir = new File(manifestPath.parentFile, "maven")
|
||||
|
||||
if (!mavenDir.exists()) {
|
||||
throw new GradleException("Maven directory not found at: ${mavenDir.path}")
|
||||
}
|
||||
|
||||
println("✓ Found rustls-platform-verifier maven repo at: ${mavenDir.path}")
|
||||
return mavenDir.path
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = findRustlsPlatformVerifierMavenDir()
|
||||
metadataSources.artifact()
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -67,7 +98,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.carriez.flutter_hbb"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
@@ -97,8 +128,10 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation "rustls:rustls-platform-verifier:0.1.1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Keep class members from protobuf generated code.
|
||||
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
|
||||
# Keep rustls-platform-verifier classes for JNI
|
||||
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
|
||||
@@ -23,6 +23,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="RustDesk"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
|
||||
@@ -62,7 +62,13 @@ class MainActivity : FlutterActivity() {
|
||||
channelTag
|
||||
)
|
||||
initFlutterChannel(flutterMethodChannel!!)
|
||||
thread { setCodecInfo() }
|
||||
thread {
|
||||
try {
|
||||
setCodecInfo()
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to setCodecInfo: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import ffi.FFI
|
||||
|
||||
class MainApplication : Application() {
|
||||
companion object {
|
||||
private const val TAG = "MainApplication"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "App start")
|
||||
FFI.onAppStart(applicationContext)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ object FFI {
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun onAppStart(ctx: Context)
|
||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -60,6 +62,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -13,11 +13,13 @@ import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
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/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_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:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -42,7 +44,7 @@ import 'package:flutter_hbb/native/win32.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
|
||||
import 'package:flutter_hbb/native/common.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
|
||||
final globalKey = GlobalKey<NavigatorState>();
|
||||
final navigationBarKey = GlobalKey();
|
||||
@@ -75,6 +77,9 @@ bool _ignoreDevicePixelRatio = true;
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
|
||||
// Tolerance used for floating-point position comparisons to avoid precision errors.
|
||||
const double _kPositionEpsilon = 1e-6;
|
||||
|
||||
bool get isMainDesktopWindow =>
|
||||
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
||||
|
||||
@@ -106,6 +111,10 @@ enum DesktopType {
|
||||
portForward,
|
||||
}
|
||||
|
||||
bool isDoubleEqual(double a, double b) {
|
||||
return (a - b).abs() < _kPositionEpsilon;
|
||||
}
|
||||
|
||||
class IconFont {
|
||||
static const _family1 = 'Tabbar';
|
||||
static const _family2 = 'PeerSearchbar';
|
||||
@@ -1622,7 +1631,8 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
|
||||
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
|
||||
// Because all session options use `Y` and `<Empty>` as values.
|
||||
|
||||
Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||
Future<bool> matchPeer(
|
||||
String searchText, Peer peer, PeerTabIndex peerTabIndex) async {
|
||||
if (searchText.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
@@ -1633,11 +1643,14 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||
peer.username.toLowerCase().contains(searchText)) {
|
||||
return true;
|
||||
}
|
||||
final alias = peer.alias;
|
||||
if (alias.isEmpty) {
|
||||
return false;
|
||||
if (peer.alias.toLowerCase().contains(searchText)) {
|
||||
return true;
|
||||
}
|
||||
return alias.toLowerCase().contains(searchText);
|
||||
if (peerTabShowNote(peerTabIndex) &&
|
||||
peer.note.toLowerCase().contains(searchText)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get the image for the current [platform].
|
||||
@@ -1667,6 +1680,15 @@ class LastWindowPosition {
|
||||
LastWindowPosition(this.width, this.height, this.offsetWidth,
|
||||
this.offsetHeight, this.isMaximized, this.isFullscreen);
|
||||
|
||||
bool equals(LastWindowPosition other) {
|
||||
return ((width == other.width) &&
|
||||
(height == other.height) &&
|
||||
(offsetWidth == other.offsetWidth) &&
|
||||
(offsetHeight == other.offsetHeight) &&
|
||||
(isMaximized == other.isMaximized) &&
|
||||
(isFullscreen == other.isFullscreen));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
"width": width,
|
||||
@@ -1706,24 +1728,36 @@ String get windowFramePrefix =>
|
||||
? "incoming_"
|
||||
: (bind.isOutgoingOnly() ? "outgoing_" : ""));
|
||||
|
||||
typedef WindowKey = ({WindowType type, int? windowId});
|
||||
|
||||
LastWindowPosition? _lastWindowPosition = null;
|
||||
final Debouncer _saveWindowDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||
|
||||
/// Save window position and size on exit
|
||||
/// Note that windowId must be provided if it's subwindow
|
||||
Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
Future<void> saveWindowPosition(WindowType type,
|
||||
{int? windowId, bool? flush}) async {
|
||||
if (type != WindowType.Main && windowId == null) {
|
||||
debugPrint(
|
||||
"Error: windowId cannot be null when saving positions for sub window");
|
||||
}
|
||||
|
||||
late Offset position;
|
||||
late Size sz;
|
||||
Offset? position;
|
||||
Size? sz;
|
||||
late bool isMaximized;
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||
|
||||
setPreFrame() {
|
||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
position = Offset(
|
||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||
if (lpos != null) {
|
||||
if (lpos.offsetWidth != null && lpos.offsetHeight != null) {
|
||||
position = Offset(lpos.offsetWidth!, lpos.offsetHeight!);
|
||||
}
|
||||
if (lpos.width != null && lpos.height != null) {
|
||||
sz = Size(lpos.width!, lpos.height!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@@ -1763,30 +1797,56 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isWindows) {
|
||||
if (isWindows && position != null) {
|
||||
const kMinOffset = -10000;
|
||||
const kMaxOffset = 10000;
|
||||
if (position.dx < kMinOffset ||
|
||||
position.dy < kMinOffset ||
|
||||
position.dx > kMaxOffset ||
|
||||
position.dy > kMaxOffset) {
|
||||
if (position!.dx < kMinOffset ||
|
||||
position!.dy < kMinOffset ||
|
||||
position!.dx > kMaxOffset ||
|
||||
position!.dy > kMaxOffset) {
|
||||
debugPrint("Invalid position: $position, ignore saving position");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final pos = LastWindowPosition(
|
||||
sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
|
||||
debugPrint(
|
||||
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
|
||||
final pos = LastWindowPosition(sz?.width, sz?.height, position?.dx,
|
||||
position?.dy, isMaximized, isFullscreen);
|
||||
|
||||
await bind.setLocalFlutterOption(
|
||||
k: windowFramePrefix + type.name, v: pos.toString());
|
||||
final WindowKey key = (type: type, windowId: windowId);
|
||||
|
||||
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
|
||||
windowId != null) {
|
||||
await _saveSessionWindowPosition(
|
||||
type, windowId, isMaximized, isFullscreen, pos);
|
||||
final bool haveNewWindowPosition =
|
||||
(_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
|
||||
final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
|
||||
|
||||
if (haveNewWindowPosition || isPreviousNewWindowPositionPending) {
|
||||
_lastWindowPosition = pos;
|
||||
|
||||
if (flush ?? false) {
|
||||
// If a previous update is pending, replace it.
|
||||
_saveWindowDebounce.cancel();
|
||||
await _saveWindowPositionActual(key);
|
||||
} else if (haveNewWindowPosition) {
|
||||
_saveWindowDebounce.call(() => _saveWindowPositionActual(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveWindowPositionActual(WindowKey key) async {
|
||||
LastWindowPosition? pos = _lastWindowPosition;
|
||||
|
||||
if (pos != null) {
|
||||
debugPrint(
|
||||
"Saving frame: ${key.windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
|
||||
|
||||
await bind.setLocalFlutterOption(
|
||||
k: windowFramePrefix + key.type.name, v: pos.toString());
|
||||
|
||||
if ((key.type == WindowType.RemoteDesktop ||
|
||||
key.type == WindowType.ViewCamera) &&
|
||||
key.windowId != null) {
|
||||
await _saveSessionWindowPosition(key.type, key.windowId!,
|
||||
pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1852,6 +1912,8 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
|
||||
return Size(restoreWidth, restoreHeight);
|
||||
}
|
||||
|
||||
// Consider using Rect.contains() instead,
|
||||
// though the implementation is not exactly the same.
|
||||
bool isPointInRect(Offset point, Rect rect) {
|
||||
return point.dx >= rect.left &&
|
||||
point.dx <= rect.right &&
|
||||
@@ -1949,8 +2011,24 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
if (lpos == null) {
|
||||
debugPrint("no window position saved, ignoring position restoration");
|
||||
return false;
|
||||
debugPrint("No window position saved, trying to center the window.");
|
||||
switch (type) {
|
||||
case WindowType.Main:
|
||||
// Center the main window only if no position is saved (on first run).
|
||||
if (isWindows || isLinux) {
|
||||
await windowManager.center();
|
||||
}
|
||||
// For MacOS, the window is already centered by default.
|
||||
// See https://github.com/rustdesk/rustdesk/blob/9b9276e7524523d7f667fefcd0694d981443df0e/flutter/macos/Runner/Base.lproj/MainMenu.xib#L333
|
||||
// If `<windowPositionMask>` in `<window>` is not set, the window will be centered.
|
||||
break;
|
||||
default:
|
||||
// No need to change the position of a sub window if no position is saved,
|
||||
// since the default position is already centered.
|
||||
// https://github.com/rustdesk/rustdesk/blob/317639169359936f7f9f85ef445ec9774218772d/flutter/lib/utils/multi_window_manager.dart#L163
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
|
||||
if (!isRemotePeerPos && windowId != null) {
|
||||
@@ -2871,7 +2949,7 @@ Future<void> updateSystemWindowTheme() async {
|
||||
///
|
||||
/// Note: not found a general solution for rust based AVFoundation bingding.
|
||||
/// [AVFoundation] crate has compile error.
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host");
|
||||
|
||||
enum PermissionAuthorizeType {
|
||||
undetermined,
|
||||
@@ -3943,3 +4021,7 @@ String decode_http_response(http.Response resp) {
|
||||
return resp.body;
|
||||
}
|
||||
}
|
||||
|
||||
bool peerTabShowNote(PeerTabIndex peerTabIndex) {
|
||||
return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ class PeerPayload {
|
||||
"platform": _platform(p.info['os']),
|
||||
"hostname": p.info['device_name'],
|
||||
"device_group_name": p.device_group_name,
|
||||
"note": p.note,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -466,6 +466,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
IDTextEditingController idController = IDTextEditingController(text: '');
|
||||
TextEditingController aliasController = TextEditingController(text: '');
|
||||
TextEditingController passwordController = TextEditingController(text: '');
|
||||
TextEditingController noteController = TextEditingController(text: '');
|
||||
final tags = List.of(gFFI.abModel.currentAbTags);
|
||||
var selectedTag = List<dynamic>.empty(growable: true).obs;
|
||||
final style = TextStyle(fontSize: 14.0);
|
||||
@@ -494,7 +495,11 @@ class _AddressBookState extends State<AddressBook> {
|
||||
password = passwordController.text;
|
||||
}
|
||||
String? errMsg2 = await gFFI.abModel.addIdToCurrent(
|
||||
id, aliasController.text.trim(), password, selectedTag);
|
||||
id,
|
||||
aliasController.text.trim(),
|
||||
password,
|
||||
selectedTag,
|
||||
noteController.text);
|
||||
if (errMsg2 != null) {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
@@ -600,6 +605,24 @@ class _AddressBookState extends State<AddressBook> {
|
||||
),
|
||||
).workaroundFreezeLinuxMint(),
|
||||
)),
|
||||
row(
|
||||
label: Text(
|
||||
translate('Note'),
|
||||
style: style,
|
||||
),
|
||||
input: Obx(
|
||||
() => TextField(
|
||||
controller: noteController,
|
||||
maxLines: 3,
|
||||
minLines: 1,
|
||||
maxLength: 300,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Note'),
|
||||
),
|
||||
).workaroundFreezeLinuxMint(),
|
||||
)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
||||
156
flutter/lib/common/widgets/custom_scale_base.dart
Normal file
156
flutter/lib/common/widgets/custom_scale_base.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/utils/scale.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
|
||||
/// Base class providing shared custom scale control logic for both mobile and desktop widgets.
|
||||
/// Implementations must provide [ffi] and [onScaleChanged] getters.
|
||||
abstract class CustomScaleControls<T extends StatefulWidget> extends State<T> {
|
||||
/// FFI instance for session interaction
|
||||
FFI get ffi;
|
||||
|
||||
/// Callback invoked when scale value changes
|
||||
ValueChanged<int>? get onScaleChanged;
|
||||
|
||||
late int _scaleValue;
|
||||
late final Debouncer<int> _debouncerScale;
|
||||
// Normalized slider position in [0, 1]. We map it nonlinearly to percent.
|
||||
double _scalePos = 0.0;
|
||||
|
||||
int get scaleValue => _scaleValue;
|
||||
double get scalePos => _scalePos;
|
||||
|
||||
int mapPosToPercent(double p) => _mapPosToPercent(p);
|
||||
|
||||
static const int minPercent = kScaleCustomMinPercent;
|
||||
static const int pivotPercent = kScaleCustomPivotPercent; // 100% should be at 1/3 of track
|
||||
static const int maxPercent = kScaleCustomMaxPercent;
|
||||
static const double pivotPos = kScaleCustomPivotPos; // first 1/3 → up to 100%
|
||||
static const double detentEpsilon = kScaleCustomDetentEpsilon; // snap range around pivot (~0.6%)
|
||||
|
||||
// Clamp helper for local use
|
||||
int _clampScale(int v) => clampCustomScalePercent(v);
|
||||
|
||||
// Map normalized position [0,1] → percent [5,1000] with 100 at 1/3 width.
|
||||
int _mapPosToPercent(double p) {
|
||||
if (p <= 0.0) return minPercent;
|
||||
if (p >= 1.0) return maxPercent;
|
||||
if (p <= pivotPos) {
|
||||
final q = p / pivotPos; // 0..1
|
||||
final v = minPercent + q * (pivotPercent - minPercent);
|
||||
return _clampScale(v.round());
|
||||
} else {
|
||||
final q = (p - pivotPos) / (1.0 - pivotPos); // 0..1
|
||||
final v = pivotPercent + q * (maxPercent - pivotPercent);
|
||||
return _clampScale(v.round());
|
||||
}
|
||||
}
|
||||
|
||||
// Map percent [5,1000] → normalized position [0,1]
|
||||
double _mapPercentToPos(int percent) {
|
||||
final p = _clampScale(percent);
|
||||
if (p <= pivotPercent) {
|
||||
final q = (p - minPercent) / (pivotPercent - minPercent);
|
||||
return q * pivotPos;
|
||||
} else {
|
||||
final q = (p - pivotPercent) / (maxPercent - pivotPercent);
|
||||
return pivotPos + q * (1.0 - pivotPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap normalized position to the pivot when close to it
|
||||
double _snapNormalizedPos(double p) {
|
||||
if ((p - pivotPos).abs() <= detentEpsilon) return pivotPos;
|
||||
if (p < 0.0) return 0.0;
|
||||
if (p > 1.0) return 1.0;
|
||||
return p;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scaleValue = 100;
|
||||
_debouncerScale = Debouncer<int>(
|
||||
kDebounceCustomScaleDuration,
|
||||
onChanged: (v) async {
|
||||
await _applyScale(v);
|
||||
},
|
||||
initialValue: _scaleValue,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
final v = await getSessionCustomScalePercent(ffi.sessionId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_scaleValue = v;
|
||||
_scalePos = _mapPercentToPos(v);
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Failed to get initial value: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyScale(int v) async {
|
||||
v = clampCustomScalePercent(v);
|
||||
setState(() {
|
||||
_scaleValue = v;
|
||||
});
|
||||
try {
|
||||
await bind.sessionSetFlutterOption(
|
||||
sessionId: ffi.sessionId,
|
||||
k: kCustomScalePercentKey,
|
||||
v: v.toString());
|
||||
final curStyle = await bind.sessionGetViewStyle(sessionId: ffi.sessionId);
|
||||
if (curStyle != kRemoteViewStyleCustom) {
|
||||
await bind.sessionSetViewStyle(
|
||||
sessionId: ffi.sessionId, value: kRemoteViewStyleCustom);
|
||||
}
|
||||
await ffi.canvasModel.updateViewStyle();
|
||||
if (isMobile) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
onScaleChanged?.call(v);
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Apply failed: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
void nudgeScale(int delta) {
|
||||
final next = _clampScale(_scaleValue + delta);
|
||||
setState(() {
|
||||
_scaleValue = next;
|
||||
_scalePos = _mapPercentToPos(next);
|
||||
});
|
||||
onScaleChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncerScale.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onSliderChanged(double v) {
|
||||
final snapped = _snapNormalizedPos(v);
|
||||
final next = _mapPosToPercent(snapped);
|
||||
if (next != _scaleValue || snapped != _scalePos) {
|
||||
setState(() {
|
||||
_scalePos = snapped;
|
||||
_scaleValue = next;
|
||||
});
|
||||
onScaleChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,29 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'address_book.dart';
|
||||
|
||||
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
|
||||
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
|
||||
'', dialogManager);
|
||||
void clientClose(SessionID sessionId, FFI ffi) async {
|
||||
if (allowAskForNoteAtEndOfConnection(ffi, true)) {
|
||||
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
|
||||
return;
|
||||
}
|
||||
closeConnection();
|
||||
} else {
|
||||
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
|
||||
'', ffi.dialogManager);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ValidationRule {
|
||||
@@ -1509,56 +1518,71 @@ showSetOSAccount(
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildNoteTextField({
|
||||
required TextEditingController controller,
|
||||
required VoidCallback onEscape,
|
||||
}) {
|
||||
final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
onEscape();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('input note here'),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: EdgeInsets.all(12),
|
||||
),
|
||||
minLines: 5,
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
).workaroundFreezeLinuxMint();
|
||||
}
|
||||
|
||||
showAuditDialog(FFI ffi) async {
|
||||
final controller = TextEditingController(text: ffi.auditNote);
|
||||
final controller = TextEditingController(
|
||||
text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId));
|
||||
ffi.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
var text = controller.text;
|
||||
bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
|
||||
ffi.auditNote = text;
|
||||
close();
|
||||
}
|
||||
|
||||
late final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
close();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Note')),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
height: 120,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: const InputDecoration.collapsed(
|
||||
hintText: 'input note here',
|
||||
),
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
child: buildNoteTextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
).workaroundFreezeLinuxMint()),
|
||||
onEscape: close,
|
||||
)),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit)
|
||||
@@ -1569,6 +1593,223 @@ showAuditDialog(FFI ffi) async {
|
||||
});
|
||||
}
|
||||
|
||||
bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) {
|
||||
if (ffi == null) {
|
||||
return false;
|
||||
}
|
||||
return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) &&
|
||||
bind
|
||||
.sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn")
|
||||
.isNotEmpty &&
|
||||
bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty &&
|
||||
bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty &&
|
||||
(!closedByControlling ||
|
||||
bind.willSessionCloseCloseSession(sessionId: ffi.sessionId));
|
||||
}
|
||||
|
||||
// return value: close canceled
|
||||
// true: return
|
||||
// false: go on
|
||||
Future<bool> desktopTryShowTabAuditDialogCloseCancelled(
|
||||
{required String id, required DesktopTabController tabController}) async {
|
||||
try {
|
||||
final page =
|
||||
tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page;
|
||||
final ffi = (page as dynamic).ffi;
|
||||
final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi);
|
||||
return res;
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show audit dialog: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// return value:
|
||||
// true: return
|
||||
// false: go on
|
||||
Future<bool> showConnEndAuditDialogCloseCanceled(
|
||||
{required FFI ffi, String? type, String? title, String? text}) async {
|
||||
final res = await _showConnEndAuditDialogCloseCanceled(
|
||||
ffi: ffi, type: type, title: title, text: text);
|
||||
if (res == true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// return value:
|
||||
// true: return
|
||||
// false / null: go on
|
||||
Future<bool?> _showConnEndAuditDialogCloseCanceled({
|
||||
required FFI ffi,
|
||||
String? type,
|
||||
String? title,
|
||||
String? text,
|
||||
}) async {
|
||||
final closedByControlling = type == null;
|
||||
final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling);
|
||||
if (!showDialog) {
|
||||
return false;
|
||||
}
|
||||
ffi.dialogManager.dismissAll();
|
||||
|
||||
Future<void> updateAuditNoteByGuid(String auditGuid, String note) async {
|
||||
debugPrint('Updating audit note for GUID: $auditGuid, note: $note');
|
||||
try {
|
||||
final apiServer = await bind.mainGetApiServer();
|
||||
if (apiServer.isEmpty) {
|
||||
debugPrint('API server is empty, cannot update audit note');
|
||||
return;
|
||||
}
|
||||
final url = '$apiServer/api/audit';
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
final body = jsonEncode({
|
||||
'guid': auditGuid,
|
||||
'note': note,
|
||||
});
|
||||
|
||||
final response = await http.put(
|
||||
Uri.parse(url),
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('Successfully updated audit note for GUID: $auditGuid');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error updating audit note: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final controller = TextEditingController();
|
||||
bool askForNote =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
bool isInProgress = false;
|
||||
|
||||
return await ffi.dialogManager.show<bool>((setState, close, context) {
|
||||
cancel() {
|
||||
close(true);
|
||||
}
|
||||
|
||||
set() async {
|
||||
if (isInProgress) return;
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
var text = controller.text;
|
||||
if (text.isNotEmpty) {
|
||||
await updateAuditNoteByGuid(
|
||||
bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text)
|
||||
.timeout(const Duration(seconds: 6), onTimeout: () {
|
||||
debugPrint('updateAuditNoteByGuid timeout after 6s');
|
||||
});
|
||||
}
|
||||
// Save the "ask for note" preference
|
||||
if (!isOptFixed) {
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, askForNote);
|
||||
}
|
||||
}
|
||||
|
||||
submit() async {
|
||||
await set();
|
||||
close(false);
|
||||
}
|
||||
|
||||
final buttons = [
|
||||
dialogButton('OK', onPressed: isInProgress ? null : submit)
|
||||
];
|
||||
if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||
buttons.add(dialogButton('Retry', onPressed: () async {
|
||||
await set();
|
||||
close(true);
|
||||
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false);
|
||||
}));
|
||||
if (type == 'relay-hint2') {
|
||||
buttons.add(dialogButton('Connect via relay', onPressed: () async {
|
||||
await set();
|
||||
close(true);
|
||||
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (closedByControlling) {
|
||||
buttons.add(dialogButton('Cancel',
|
||||
onPressed: isInProgress ? null : cancel, isOutline: true));
|
||||
}
|
||||
|
||||
Widget content;
|
||||
if (closedByControlling) {
|
||||
content = SelectionArea(
|
||||
child: msgboxContent(
|
||||
'info', 'Close', 'Are you sure to close the connection?'));
|
||||
} else {
|
||||
content =
|
||||
SelectionArea(child: msgboxContent(type, title ?? '', text ?? ''));
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: SizedBox(
|
||||
width: 350,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
content,
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: buildNoteTextField(
|
||||
controller: controller,
|
||||
onEscape: cancel,
|
||||
),
|
||||
),
|
||||
if (!isOptFixed) ...[
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
askForNote = !askForNote;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: askForNote,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
askForNote = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate('note-at-conn-end-tip'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isInProgress)
|
||||
const LinearProgressIndicator().marginOnly(top: 4),
|
||||
],
|
||||
)),
|
||||
actions: buttons,
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showConfirmSwitchSidesDialog(
|
||||
SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -1783,6 +2024,49 @@ void editAbTagDialog(
|
||||
});
|
||||
}
|
||||
|
||||
void editAbPeerNoteDialog(String id) {
|
||||
var isInProgress = false;
|
||||
final currentNote = gFFI.abModel.getPeerNote(id);
|
||||
var controller = TextEditingController(text: currentNote);
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
await gFFI.abModel.changeNote(id: id, note: controller.text);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Edit note")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
maxLines: 3,
|
||||
minLines: 1,
|
||||
maxLength: 300,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Note'),
|
||||
),
|
||||
).workaroundFreezeLinuxMint(),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) const LinearProgressIndicator(),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void renameDialog(
|
||||
{required String oldName,
|
||||
FormFieldValidator<String>? validator,
|
||||
@@ -2078,15 +2362,20 @@ void showWindowsSessionsDialog(
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
msgboxContent(type, title, text).marginOnly(bottom: 12),
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
dialogButton('Connect', onPressed: submit, isOutline: false),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/common/widgets/remote_input.dart';
|
||||
|
||||
enum GestureState {
|
||||
none,
|
||||
@@ -96,6 +97,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
if (onTwoFingerScaleEnd != null) {
|
||||
onTwoFingerScaleEnd!(d);
|
||||
}
|
||||
if (isSpecialHoldDragActive) {
|
||||
// If we are in special drag mode, we need to reset the state.
|
||||
// Otherwise, the next `onTwoFingerScaleUpdate()` will handle a wrong `focalPoint`.
|
||||
_currentState = GestureState.none;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
debugPrint("ThreeFingerState.vertical onEnd");
|
||||
|
||||
@@ -400,6 +400,8 @@ Future<bool?> loginDialog() async {
|
||||
String? passwordMsg;
|
||||
var isInProgress = false;
|
||||
final RxString curOP = ''.obs;
|
||||
// Track hover state for the close icon
|
||||
bool isCloseHovered = false;
|
||||
|
||||
final loginOptions = [].obs;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
@@ -557,21 +559,27 @@ Future<bool?> loginDialog() async {
|
||||
Text(
|
||||
translate('Login'),
|
||||
).marginOnly(top: MyTheme.dialogPadding),
|
||||
InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => isCloseHovered = true),
|
||||
onExit: (_) => setState(() => isCloseHovered = false),
|
||||
child: InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: isCloseHovered
|
||||
? Colors.white
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
).marginOnly(top: 10, right: 15),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
checkScreenSize: true,
|
||||
position: draggablePositions.chatWindow,
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -395,7 +396,10 @@ class _DraggableState extends State<Draggable> {
|
||||
_chatModel?.setChatWindowPosition(position);
|
||||
}
|
||||
|
||||
checkScreenSize() {}
|
||||
checkScreenSize() {
|
||||
// Ensure the draggable always stays within current screen bounds
|
||||
widget.position.tryAdjust(widget.width, widget.height, 1);
|
||||
}
|
||||
|
||||
checkKeyboard() {
|
||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
@@ -517,6 +521,12 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
_lastBottomHeight = bottomHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
position.tryAdjust(_width, _height, 1);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
checkKeyboard();
|
||||
|
||||
@@ -127,6 +127,10 @@ class _PeerCardState extends State<_PeerCard>
|
||||
);
|
||||
}
|
||||
|
||||
bool _showNote(Peer peer) {
|
||||
return peerTabShowNote(widget.tab) && peer.note.isNotEmpty;
|
||||
}
|
||||
|
||||
makeChild(bool isPortrait, Peer peer) {
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
@@ -134,6 +138,8 @@ class _PeerCardState extends State<_PeerCard>
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
final showNote = _showNote(peer);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
@@ -185,14 +191,44 @@ class _PeerCardState extends State<_PeerCard>
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: name,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showNote)
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: peer.note,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
peer.note,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).marginOnly(
|
||||
left: peerCardUiType.value ==
|
||||
PeerUiType.list
|
||||
? 32
|
||||
: 4),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
).marginOnly(top: 2),
|
||||
@@ -278,7 +314,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
padding: const EdgeInsets.all(6),
|
||||
child:
|
||||
getPlatformImage(peer.platform, size: 60),
|
||||
).marginOnly(top: 4),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -297,8 +333,26 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showNote(peer))
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: peer.note,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Text(
|
||||
peer.note,
|
||||
style: const TextStyle(
|
||||
color: Colors.white38,
|
||||
fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingAll(4.0),
|
||||
).paddingOnly(top: 4.0, left: 4.0, right: 4.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1134,6 +1188,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty) {
|
||||
menuItems.add(_editTagAction(peer.id));
|
||||
}
|
||||
menuItems.add(_editNoteAction(peer.id));
|
||||
}
|
||||
final addressbooks = gFFI.abModel.addressBooksCanWrite();
|
||||
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
|
||||
@@ -1173,6 +1228,21 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _editNoteAction(String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Edit note'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
editAbPeerNoteDialog(id);
|
||||
},
|
||||
padding: super.menuPadding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
Future<String> _getAlias(String id) async =>
|
||||
|
||||
@@ -71,10 +71,12 @@ class _PeersView extends StatefulWidget {
|
||||
final Peers peers;
|
||||
final PeerFilter? peerFilter;
|
||||
final PeerCardBuilder peerCardBuilder;
|
||||
final PeerTabIndex peerTabIndex;
|
||||
|
||||
const _PeersView(
|
||||
{required this.peers,
|
||||
required this.peerCardBuilder,
|
||||
required this.peerTabIndex,
|
||||
this.peerFilter,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
@@ -395,8 +397,8 @@ class _PeersViewState extends State<_PeersView>
|
||||
return peers;
|
||||
}
|
||||
searchText = searchText.toLowerCase();
|
||||
final matches =
|
||||
await Future.wait(peers.map((peer) => matchPeer(searchText, peer)));
|
||||
final matches = await Future.wait(
|
||||
peers.map((peer) => matchPeer(searchText, peer, widget.peerTabIndex)));
|
||||
final filteredList = List<Peer>.empty(growable: true);
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (matches[i]) {
|
||||
@@ -441,7 +443,10 @@ abstract class BasePeersView extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
return _PeersView(
|
||||
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||
peers: peers,
|
||||
peerFilter: peerFilter,
|
||||
peerCardBuilder: peerCardBuilder,
|
||||
peerTabIndex: peerTabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,13 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// For virtual mouse when using the mouse mode on mobile.
|
||||
// Special hold-drag mode: one finger holds a button (left/right button), another finger pans.
|
||||
// This flag is to override the scale gesture to a pan gesture.
|
||||
bool isSpecialHoldDragActive = false;
|
||||
// Cache the last focal point to calculate deltas in special hold-drag mode.
|
||||
Offset _lastSpecialHoldDragFocalPoint = Offset.zero;
|
||||
|
||||
class RawTouchGestureDetectorRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
final FFI ffi;
|
||||
@@ -97,6 +104,10 @@ class _RawTouchGestureDetectorRegionState
|
||||
bool _touchModePanStarted = false;
|
||||
Offset _doubleFinerTapPosition = Offset.zero;
|
||||
|
||||
// For mouse mode, we need to block the events when the cursor is in a blocked area.
|
||||
// So we need to cache the last tap down position.
|
||||
Offset? _lastTapDownPositionForMouseMode;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
InputModel get inputModel => widget.inputModel;
|
||||
@@ -112,7 +123,15 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
bool isNotTouchBasedDevice() {
|
||||
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
|
||||
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
|
||||
}
|
||||
|
||||
// Mobile, mouse mode.
|
||||
// Check if should block the mouse tap event (`_lastTapDownPositionForMouseMode`).
|
||||
bool shouldBlockMouseModeEvent() {
|
||||
return _lastTapDownPositionForMouseMode != null &&
|
||||
ffi.cursorModel.shouldBlock(_lastTapDownPositionForMouseMode!.dx,
|
||||
_lastTapDownPositionForMouseMode!.dy);
|
||||
}
|
||||
|
||||
onTapDown(TapDownDetails d) async {
|
||||
@@ -124,6 +143,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
// Desktop or mobile "Touch mode"
|
||||
_lastTapDownDetails = d;
|
||||
} else {
|
||||
_lastTapDownPositionForMouseMode = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +171,11 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
|
||||
// Using `_lastTapDownPositionForMouseMode` instead.
|
||||
if (shouldBlockMouseModeEvent()) {
|
||||
return;
|
||||
}
|
||||
// Mobile, "Mouse mode"
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
@@ -163,6 +189,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (handleTouch) {
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
_lastTapDownPositionForMouseMode = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +205,12 @@ class _RawTouchGestureDetectorRegionState
|
||||
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
|
||||
return;
|
||||
}
|
||||
// Check if the position is in a blocked area when using the mouse mode.
|
||||
if (!handleTouch) {
|
||||
if (shouldBlockMouseModeEvent()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
@@ -198,6 +232,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
} else {
|
||||
_lastTapDownPositionForMouseMode = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +258,10 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (!isMoved) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (shouldBlockMouseModeEvent()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
} else {
|
||||
@@ -274,6 +314,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
if (isSpecialHoldDragActive) return;
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
@@ -283,6 +324,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
if (isSpecialHoldDragActive) return;
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
@@ -377,12 +419,26 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (isSpecialHoldDragActive) {
|
||||
// Initialize the last focal point to calculate deltas manually.
|
||||
_lastSpecialHoldDragFocalPoint = d.focalPoint;
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If in special drag mode, perform a pan instead of a scale.
|
||||
if (isSpecialHoldDragActive) {
|
||||
// Calculate delta manually to avoid the jumpy behavior.
|
||||
final delta = d.focalPoint - _lastSpecialHoldDragFocalPoint;
|
||||
_lastSpecialHoldDragFocalPoint = d.focalPoint;
|
||||
await ffi.cursorModel.updatePan(delta * 2.0, d.focalPoint, handleTouch);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
final scale = ((d.scale - _scale) * 1000).toInt();
|
||||
_scale = d.scale;
|
||||
@@ -420,7 +476,9 @@ class _RawTouchGestureDetectorRegionState
|
||||
// No idea why we need to set the view style to "" here.
|
||||
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
}
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
if (!isSpecialHoldDragActive) {
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
get onHoldDragCancel => null;
|
||||
|
||||
@@ -230,7 +230,6 @@ List<(String, String)> otherDefaultSettings() {
|
||||
('Disable clipboard', kOptionDisableClipboard),
|
||||
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||
('Privacy mode', kOptionPrivacyMode),
|
||||
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||
('True color (4:4:4)', kOptionI444),
|
||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||
|
||||
@@ -363,6 +363,11 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
||||
child: Text(translate('Scale adaptive')),
|
||||
value: kRemoteViewStyleAdaptive,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale custom')),
|
||||
value: kRemoteViewStyleCustom,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ const String kWindowActionRebuild = "rebuild";
|
||||
const String kWindowEventHide = "hide";
|
||||
const String kWindowEventShow = "show";
|
||||
const String kWindowConnect = "connect";
|
||||
const String kWindowBumpMouse = "bump_mouse";
|
||||
|
||||
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
@@ -78,6 +79,7 @@ const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionViewStyle = "view_style";
|
||||
const String kOptionScrollStyle = "scroll_style";
|
||||
const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
|
||||
const String kOptionImageQuality = "image_quality";
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionTextureRender = "use-texture-render";
|
||||
@@ -155,11 +157,18 @@ const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||
const String kOptionEnableUdpPunch = "enable-udp-punch";
|
||||
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||
|
||||
// network options
|
||||
const String kOptionAllowWebSocket = "allow-websocket";
|
||||
const String kOptionAllowInsecureTLSFallback = "allow-insecure-tls-fallback";
|
||||
const String kOptionDisableUdp = "disable-udp";
|
||||
const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
|
||||
|
||||
// buildin opitons
|
||||
// builtin options
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||
@@ -313,12 +322,18 @@ const kRemoteViewStyleOriginal = 'original';
|
||||
/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor.
|
||||
const kRemoteViewStyleAdaptive = 'adaptive';
|
||||
|
||||
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
|
||||
const kRemoteViewStyleCustom = 'custom';
|
||||
|
||||
/// [kRemoteScrollStyleAuto] Scroll image auto by position.
|
||||
const kRemoteScrollStyleAuto = 'scrollauto';
|
||||
|
||||
/// [kRemoteScrollStyleBar] Scroll image with scroll bar.
|
||||
const kRemoteScrollStyleBar = 'scrollbar';
|
||||
|
||||
/// [kRemoteScrollStyleEdge] Scroll image auto at edges.
|
||||
const kRemoteScrollStyleEdge = 'scrolledge';
|
||||
|
||||
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
|
||||
const kScrollModeDefault = 'default';
|
||||
|
||||
@@ -345,6 +360,17 @@ const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
|
||||
PointerDeviceKind.invertedStylus,
|
||||
};
|
||||
|
||||
// Scale custom related constants
|
||||
const String kCustomScalePercentKey =
|
||||
'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
|
||||
const int kScaleCustomMinPercent = 5;
|
||||
const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track
|
||||
const int kScaleCustomMaxPercent = 1000;
|
||||
const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100%
|
||||
const double kScaleCustomDetentEpsilon =
|
||||
0.006; // snap range around pivot (~0.6%)
|
||||
const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300);
|
||||
|
||||
// ================================ mobile ================================
|
||||
|
||||
// Magic numbers, maybe need to avoid it or use a better way to get them.
|
||||
|
||||
@@ -374,6 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
note: '',
|
||||
);
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
@@ -536,64 +537,68 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
builder: (context, setState) {
|
||||
var offset = Offset(0, 0);
|
||||
return Obx(() => InkWell(
|
||||
child: _menuOpen.value
|
||||
? Transform.rotate(
|
||||
angle: pi,
|
||||
child: Icon(IconFont.more, size: 14),
|
||||
child: _menuOpen.value
|
||||
? Transform.rotate(
|
||||
angle: pi,
|
||||
child: Icon(IconFont.more, size: 14),
|
||||
)
|
||||
: Icon(IconFont.more, size: 14),
|
||||
onTapDown: (e) {
|
||||
offset = e.globalPosition;
|
||||
},
|
||||
onTap: () async {
|
||||
_menuOpen.value = true;
|
||||
final x = offset.dx;
|
||||
final y = offset.dy;
|
||||
await mod_menu
|
||||
.showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: [
|
||||
(
|
||||
'Transfer file',
|
||||
() => onConnect(isFileTransfer: true)
|
||||
),
|
||||
(
|
||||
'View camera',
|
||||
() => onConnect(isViewCamera: true)
|
||||
),
|
||||
(
|
||||
'${translate('Terminal')} (beta)',
|
||||
() => onConnect(isTerminal: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) =>
|
||||
Text(
|
||||
translate(e.$1),
|
||||
style: style,
|
||||
),
|
||||
proc: () => e.$2(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
))
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: CustomPopupMenuTheme
|
||||
.commonColor,
|
||||
height:
|
||||
CustomPopupMenuTheme.height,
|
||||
dividerHeight:
|
||||
CustomPopupMenuTheme
|
||||
.dividerHeight)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
elevation: 8,
|
||||
)
|
||||
: Icon(IconFont.more, size: 14),
|
||||
onTapDown: (e) {
|
||||
offset = e.globalPosition;
|
||||
},
|
||||
onTap: () async {
|
||||
_menuOpen.value = true;
|
||||
final x = offset.dx;
|
||||
final y = offset.dy;
|
||||
await mod_menu
|
||||
.showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: [
|
||||
(
|
||||
'Transfer file',
|
||||
() => onConnect(isFileTransfer: true)
|
||||
),
|
||||
(
|
||||
'View camera',
|
||||
() => onConnect(isViewCamera: true)
|
||||
),
|
||||
(
|
||||
'${translate('Terminal')} (beta)',
|
||||
() => onConnect(isTerminal: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate(e.$1),
|
||||
style: style,
|
||||
),
|
||||
proc: () => e.$2(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
))
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor:
|
||||
CustomPopupMenuTheme.commonColor,
|
||||
height: CustomPopupMenuTheme.height,
|
||||
dividerHeight: CustomPopupMenuTheme
|
||||
.dividerHeight)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
elevation: 8,
|
||||
)
|
||||
.then((_) {
|
||||
_menuOpen.value = false;
|
||||
});
|
||||
},
|
||||
));
|
||||
.then((_) {
|
||||
_menuOpen.value = false;
|
||||
});
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -760,9 +761,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
'scaleFactor': screen.scaleFactor,
|
||||
};
|
||||
|
||||
bool isChattyMethod(String methodName) {
|
||||
switch (methodName) {
|
||||
case kWindowBumpMouse: return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
if (!isChattyMethod(call.method)) {
|
||||
debugPrint(
|
||||
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
}
|
||||
if (call.method == kWindowMainWindowOnTop) {
|
||||
windowOnTop(null);
|
||||
} else if (call.method == kWindowGetWindowInfo) {
|
||||
@@ -793,6 +804,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
);
|
||||
} else if (call.method == kWindowBumpMouse) {
|
||||
return RdPlatformChannel.instance.bumpMouse(
|
||||
dx: call.arguments['dx'],
|
||||
dy: call.arguments['dy']);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
final args = call.arguments.split(',');
|
||||
int? windowId;
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/printer_model.dart';
|
||||
@@ -560,6 +561,12 @@ class _GeneralState extends State<_General> {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||
}
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'note-at-conn-end-tip',
|
||||
kOptionAllowAskForNoteAtEndOfConnection,
|
||||
isServer: false,
|
||||
));
|
||||
return _Card(title: 'Other', children: children);
|
||||
}
|
||||
|
||||
@@ -1177,7 +1184,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
],
|
||||
),
|
||||
enabled: tmpEnabled && !locked),
|
||||
numericOneTimePassword,
|
||||
if (usePassword) numericOneTimePassword,
|
||||
if (usePassword) radios[1],
|
||||
if (usePassword)
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
@@ -1585,6 +1592,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget switchWidget(IconData icon, String title, String tooltipMessage,
|
||||
String optionKey) =>
|
||||
listTile(
|
||||
icon: icon,
|
||||
title: title,
|
||||
showTooltip: true,
|
||||
tooltipMessage: tooltipMessage,
|
||||
trailing: Switch(
|
||||
value: mainGetBoolOptionSync(optionKey),
|
||||
onChanged: locked || isOptionFixed(optionKey)
|
||||
? null
|
||||
: (value) {
|
||||
mainSetBoolOption(optionKey, value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
|
||||
final divider = const Divider(height: 1, indent: 16, endIndent: 16);
|
||||
return _Card(
|
||||
title: 'Network',
|
||||
children: [
|
||||
@@ -1596,33 +1624,65 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
listTile(
|
||||
icon: Icons.dns_outlined,
|
||||
title: 'ID/Relay Server',
|
||||
onTap: () => showServerSettings(gFFI.dialogManager),
|
||||
onTap: () => showServerSettings(gFFI.dialogManager, setState),
|
||||
),
|
||||
if (!hideServer && (!hideProxy || !hideWebSocket))
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideProxy && !hideServer) divider,
|
||||
if (!hideProxy)
|
||||
listTile(
|
||||
icon: Icons.network_ping_outlined,
|
||||
title: 'Socks5/Http(s) Proxy',
|
||||
onTap: changeSocks5Proxy,
|
||||
),
|
||||
if (!hideProxy && !hideWebSocket)
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideWebSocket && (!hideServer || !hideProxy)) divider,
|
||||
if (!hideWebSocket)
|
||||
listTile(
|
||||
icon: Icons.web_asset_outlined,
|
||||
title: 'Use WebSocket',
|
||||
showTooltip: true,
|
||||
tooltipMessage: 'websocket_tip',
|
||||
trailing: Switch(
|
||||
value: mainGetBoolOptionSync(kOptionAllowWebSocket),
|
||||
onChanged: locked
|
||||
? null
|
||||
: (value) {
|
||||
mainSetBoolOption(kOptionAllowWebSocket, value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
switchWidget(
|
||||
Icons.web_asset_outlined,
|
||||
'Use WebSocket',
|
||||
'${translate('websocket_tip')}\n\n${translate('server-oss-not-support-tip')}',
|
||||
kOptionAllowWebSocket),
|
||||
if (!isWeb)
|
||||
futureBuilder(
|
||||
future: bind.mainIsUsingPublicServer(),
|
||||
hasData: (isUsingPublicServer) {
|
||||
if (isUsingPublicServer) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
if (!hideServer || !hideProxy || !hideWebSocket)
|
||||
divider,
|
||||
switchWidget(
|
||||
Icons.no_encryption_outlined,
|
||||
'Allow insecure TLS fallback',
|
||||
'allow-insecure-tls-fallback-tip',
|
||||
kOptionAllowInsecureTLSFallback),
|
||||
if (!outgoingOnly) divider,
|
||||
if (!outgoingOnly)
|
||||
listTile(
|
||||
icon: Icons.lan_outlined,
|
||||
title: 'Disable UDP',
|
||||
showTooltip: true,
|
||||
tooltipMessage:
|
||||
'${translate('disable-udp-tip')}\n\n${translate('server-oss-not-support-tip')}',
|
||||
trailing: Switch(
|
||||
value: bind.mainGetOptionSync(
|
||||
key: kOptionDisableUdp) ==
|
||||
'Y',
|
||||
onChanged:
|
||||
locked || isOptionFixed(kOptionDisableUdp)
|
||||
? null
|
||||
: (value) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionDisableUdp,
|
||||
value: value ? 'Y' : 'N');
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1685,6 +1745,13 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
|
||||
|
||||
onEdgeScrollEdgeThicknessChanged(double value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: kOptionEdgeScrollEdgeThickness, value: value.round().toString());
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
return _Card(title: 'Default Scroll Style', children: [
|
||||
_Radio(context,
|
||||
value: kRemoteScrollStyleAuto,
|
||||
@@ -1696,6 +1763,23 @@ class _DisplayState extends State<_Display> {
|
||||
groupValue: groupValue,
|
||||
label: 'Scrollbar',
|
||||
onChanged: isOptFixed ? null : onChanged),
|
||||
if (!isWeb) ...[
|
||||
_Radio(context,
|
||||
value: kRemoteScrollStyleEdge,
|
||||
groupValue: groupValue,
|
||||
label: 'ScrollEdge',
|
||||
onChanged: isOptFixed ? null : onChanged),
|
||||
Offstage(
|
||||
offstage: groupValue != kRemoteScrollStyleEdge,
|
||||
child: EdgeThicknessControl(
|
||||
value: double.tryParse(bind.mainGetUserDefaultOption(
|
||||
key: kOptionEdgeScrollEdgeThickness)) ??
|
||||
100.0,
|
||||
onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness)
|
||||
? null
|
||||
: onEdgeScrollEdgeThicknessChanged,
|
||||
)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1737,9 +1821,9 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
Widget trackpadSpeed(BuildContext context) {
|
||||
final initSpeed = (int.tryParse(
|
||||
bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
final initSpeed =
|
||||
(int.tryParse(bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
final curSpeed = SimpleWrapper(initSpeed);
|
||||
void onDebouncer(int v) {
|
||||
bind.mainSetUserDefaultOption(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
@@ -52,7 +53,7 @@ enum MouseFocusScope {
|
||||
}
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
const FileManagerPage(
|
||||
FileManagerPage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -67,9 +68,16 @@ class FileManagerPage extends StatefulWidget {
|
||||
final bool? forceRelay;
|
||||
final String? connToken;
|
||||
final DesktopTabController? tabController;
|
||||
final SimpleWrapper<State<FileManagerPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
State<StatefulWidget> createState() {
|
||||
final state = _FileManagerPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
@@ -139,12 +147,26 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
}
|
||||
|
||||
Widget willPopScope(Widget child) {
|
||||
if (isWeb) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(_ffi.sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Overlay(key: _overlayKeyState.key, initialEntries: [
|
||||
OverlayEntry(builder: (_) {
|
||||
return Scaffold(
|
||||
return willPopScope(Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
@@ -160,7 +182,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Flexible(flex: 2, child: statusList())
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
||||
@@ -40,7 +41,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
label: params['id'],
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(params['id']),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: params['id'],
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(params['id']);
|
||||
},
|
||||
page: FileManagerPage(
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
@@ -69,7 +78,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: FileManagerPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
@@ -132,6 +149,14 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
|
||||
@@ -25,7 +25,7 @@ class _PortForward {
|
||||
}
|
||||
|
||||
class PortForwardPage extends StatefulWidget {
|
||||
const PortForwardPage({
|
||||
PortForwardPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -42,9 +42,16 @@ class PortForwardPage extends StatefulWidget {
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final SimpleWrapper<State<PortForwardPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
State<PortForwardPage> createState() {
|
||||
final state = _PortForwardPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _PortForwardPageState extends State<PortForwardPage>
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -72,7 +73,10 @@ class RemotePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
with AutomaticKeepAliveClientMixin, MultiWindowListener {
|
||||
with
|
||||
AutomaticKeepAliveClientMixin,
|
||||
MultiWindowListener,
|
||||
TickerProviderStateMixin {
|
||||
Timer? _timer;
|
||||
String keyboardMode = "legacy";
|
||||
bool _isWindowBlur = false;
|
||||
@@ -112,11 +116,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
_ffi.canvasModel.activateLocalCursor();
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
_ffi.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||
});
|
||||
_ffi.canvasModel.initializeEdgeScrollFallback(this);
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
@@ -395,7 +401,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi.dialogManager);
|
||||
clientClose(sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(providers: [
|
||||
@@ -408,6 +414,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void enterView(PointerEnterEvent evt) {
|
||||
_ffi.canvasModel.rearmEdgeScroll();
|
||||
|
||||
_cursorOverImage.value = true;
|
||||
_firstEnterImage.value = true;
|
||||
if (_onEnterOrLeaveImage4Toolbar != null) {
|
||||
@@ -427,6 +435,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void leaveView(PointerExitEvent evt) {
|
||||
_ffi.canvasModel.disableEdgeScroll();
|
||||
|
||||
if (_ffi.ffiModel.keyboard) {
|
||||
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
||||
}
|
||||
@@ -625,7 +635,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
onHover: (evt) {},
|
||||
child: child);
|
||||
});
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
@@ -680,9 +690,20 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
Widget _buildScrollAutoNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
double sizeScale = s;
|
||||
if (widget.ffi.ffiModel.isPeerLinux) {
|
||||
final displays = widget.ffi.ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
sizeScale = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
return CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: c.x / sizeScale,
|
||||
y: c.y / sizeScale,
|
||||
scale: sizeScale),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -695,17 +716,19 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
if (rect == null) {
|
||||
return Container();
|
||||
}
|
||||
final isPeerLinux = ffiModel.isPeerLinux;
|
||||
final curDisplay = ffiModel.pi.currentDisplay;
|
||||
for (var i = 0; i < displays.length; i++) {
|
||||
final textureId = widget.ffi.textureModel
|
||||
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
|
||||
if (true) {
|
||||
// both "textureId.value != -1" and "true" seems ok
|
||||
final sizeScale = isPeerLinux ? s / displays[i].scale : s;
|
||||
children.add(Positioned(
|
||||
left: (displays[i].x - rect.left) * s + offset.dx,
|
||||
top: (displays[i].y - rect.top) * s + offset.dy,
|
||||
width: displays[i].width * s,
|
||||
height: displays[i].height * s,
|
||||
width: displays[i].width * sizeScale,
|
||||
height: displays[i].height * sizeScale,
|
||||
child: Obx(() => Texture(
|
||||
textureId: textureId.value,
|
||||
filterQuality:
|
||||
|
||||
@@ -80,7 +80,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: peerId!,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(peerId),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: peerId!,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(peerId!);
|
||||
},
|
||||
page: RemotePage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId!,
|
||||
@@ -316,7 +324,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
translate('Close'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
proc: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(key);
|
||||
cancelFunc();
|
||||
},
|
||||
@@ -369,6 +383,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
@@ -423,7 +445,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:xterm/xterm.dart';
|
||||
import 'terminal_connection_manager.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
TerminalPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -25,15 +25,23 @@ class TerminalPage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() => _TerminalPageState();
|
||||
State<TerminalPage> createState() {
|
||||
final state = _TerminalPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -53,18 +61,30 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||
_cellHeight = ph * 1.0;
|
||||
|
||||
// Schedule the setState for the next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
|
||||
|
||||
// Check if this is a new connection or additional terminal
|
||||
// Note: When a connection exists, the ref count will be > 1 after this terminal is added
|
||||
final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) &&
|
||||
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
|
||||
|
||||
final isExistingConnection =
|
||||
TerminalConnectionManager.hasConnection(widget.id) &&
|
||||
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
|
||||
|
||||
if (!isExistingConnection) {
|
||||
// First terminal - show loading dialog, wait for onReady
|
||||
_ffi.dialogManager
|
||||
@@ -87,30 +107,48 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// This method ensures that the number of visible rows is an integer by computing the
|
||||
// extra space left after dividing the available height by the height of a single
|
||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||
EdgeInsets _calculatePadding(double heightPx) {
|
||||
if (_cellHeight == null) {
|
||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||
}
|
||||
final rows = (heightPx / _cellHeight!).floor();
|
||||
final extraSpace = heightPx - rows * _cellHeight!;
|
||||
final topBottom = extraSpace / 2.0;
|
||||
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final heightPx = constraints.maxHeight;
|
||||
return TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
@@ -61,12 +62,21 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
String? connToken,
|
||||
}) {
|
||||
final tabKey = '${peerId}_$terminalId';
|
||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||
final tabLabel =
|
||||
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
|
||||
return TabInfo(
|
||||
key: tabKey,
|
||||
label: '$peerId #$terminalId',
|
||||
label: tabLabel,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabKey,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
// Close the terminal session first
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi != null) {
|
||||
@@ -407,6 +417,14 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
|
||||
@@ -360,7 +360,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi.dialogManager);
|
||||
clientClose(sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(providers: [
|
||||
@@ -527,7 +527,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -79,7 +80,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
label: peerId!,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(peerId),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: peerId!,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(peerId!);
|
||||
},
|
||||
page: ViewCameraPage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId!,
|
||||
@@ -287,7 +296,13 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
translate('Close'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
proc: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(key);
|
||||
cancelFunc();
|
||||
},
|
||||
@@ -340,6 +355,14 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
@@ -393,7 +416,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: ViewCameraPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
|
||||
@@ -25,6 +25,8 @@ import '../../models/platform_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
import './popup_menu.dart';
|
||||
import './kb_layout_type_chooser.dart';
|
||||
import 'package:flutter_hbb/utils/scale.dart';
|
||||
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
|
||||
|
||||
class ToolbarState {
|
||||
late RxBool _pin;
|
||||
@@ -152,129 +154,6 @@ class _ToolbarTheme {
|
||||
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(sessionId: ffi.sessionId) ?? '';
|
||||
if (rxViewStyle != null) {
|
||||
rxViewStyle.value = viewStyle;
|
||||
}
|
||||
return viewStyle;
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionSetViewStyle(
|
||||
sessionId: ffi.sessionId, 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,
|
||||
SessionID sessionId,
|
||||
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(sessionId: sessionId, value: optKey);
|
||||
state.value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: optKey);
|
||||
if (dismissFunc != null) {
|
||||
dismissFunc();
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntrySwitch<String> disableClipboard(
|
||||
SessionID sessionId,
|
||||
EdgeInsets? padding, {
|
||||
DismissFunc? dismissFunc,
|
||||
DismissCallback? dismissCallback,
|
||||
}) {
|
||||
return createSwitchMenuEntry(
|
||||
sessionId,
|
||||
'Disable clipboard',
|
||||
'disable-clipboard',
|
||||
padding,
|
||||
true,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntrySwitch<String> createSwitchMenuEntry(
|
||||
SessionID sessionId,
|
||||
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(
|
||||
sessionId: sessionId, arg: option);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
if (dismissFunc != null) {
|
||||
dismissFunc();
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntryButton<String> insertLock(
|
||||
SessionID sessionId,
|
||||
EdgeInsets? padding, {
|
||||
@@ -632,7 +511,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
menuStyle: MenuStyle(
|
||||
padding:
|
||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||
menuChildrenGetter: (_) => [buildMonitorSubmenuWidget(context)]);
|
||||
}
|
||||
|
||||
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||
@@ -843,7 +722,7 @@ class _ControlMenu extends StatelessWidget {
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
ffi: ffi,
|
||||
menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) {
|
||||
menuChildrenGetter: (_) => toolbarControls(context, id, ffi).map((e) {
|
||||
if (e.divider) {
|
||||
return Divider();
|
||||
} else {
|
||||
@@ -1024,6 +903,7 @@ class _DisplayMenu extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
final RxInt _customPercent = 100.obs;
|
||||
late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
@@ -1037,14 +917,29 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
FFI get ffi => widget.ffi;
|
||||
String get id => widget.id;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize custom percent from stored option once
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
final v = await getSessionCustomScalePercent(widget.ffi.sessionId);
|
||||
if (_customPercent.value != v) {
|
||||
_customPercent.value = v;
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
_screenAdjustor.updateScreen();
|
||||
menuChildrenGetter() {
|
||||
menuChildrenGetter(_IconSubmenuButtonState state) {
|
||||
final menuChildren = <Widget>[
|
||||
_screenAdjustor.adjustWindow(context),
|
||||
viewStyle(),
|
||||
scrollStyle(),
|
||||
viewStyle(customPercent: _customPercent),
|
||||
scrollStyle(state, colorScheme),
|
||||
imageQuality(),
|
||||
codec(),
|
||||
if (ffi.connType == ConnType.defaultConn)
|
||||
@@ -1108,62 +1003,146 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
);
|
||||
}
|
||||
|
||||
viewStyle() {
|
||||
viewStyle({required RxInt customPercent}) {
|
||||
return futureBuilder(
|
||||
future: toolbarViewStyle(context, widget.id, widget.ffi),
|
||||
hasData: (data) {
|
||||
final v = data as List<TRadioMenu<String>>;
|
||||
final bool isCustomSelected = v.isNotEmpty
|
||||
? v.first.groupValue == kRemoteViewStyleCustom
|
||||
: false;
|
||||
return Column(children: [
|
||||
...v
|
||||
.map((e) => RdoMenuButton<String>(
|
||||
value: e.value,
|
||||
groupValue: e.groupValue,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi))
|
||||
.toList(),
|
||||
Divider(),
|
||||
...v.map((e) {
|
||||
final isCustom = e.value == kRemoteViewStyleCustom;
|
||||
final child =
|
||||
isCustom ? Text(translate('Scale custom')) : e.child;
|
||||
// Whether the current selection is already custom
|
||||
final bool isGroupCustomSelected =
|
||||
e.groupValue == kRemoteViewStyleCustom;
|
||||
// Keep menu open when switching INTO custom so the slider is visible immediately
|
||||
final bool keepOpenForThisItem =
|
||||
isCustom && !isGroupCustomSelected;
|
||||
return RdoMenuButton<String>(
|
||||
value: e.value,
|
||||
groupValue: e.groupValue,
|
||||
onChanged: (value) {
|
||||
// Perform the original change
|
||||
e.onChanged?.call(value);
|
||||
// Only force a rebuild when we keep the menu open to reveal the slider
|
||||
if (keepOpenForThisItem) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
ffi: ffi,
|
||||
// When entering custom, keep submenu open to show the slider controls
|
||||
closeOnActivate: !keepOpenForThisItem);
|
||||
}).toList(),
|
||||
// Only show a divider when custom is NOT selected
|
||||
if (!isCustomSelected) Divider(),
|
||||
_customControlsIfCustomSelected(
|
||||
onChanged: (v) => customPercent.value = v),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
scrollStyle() {
|
||||
Widget _customControlsIfCustomSelected({ValueChanged<int>? onChanged}) {
|
||||
return futureBuilder(future: () async {
|
||||
final current = await bind.sessionGetViewStyle(sessionId: ffi.sessionId);
|
||||
return current == kRemoteViewStyleCustom;
|
||||
}(), hasData: (data) {
|
||||
final isCustom = data as bool;
|
||||
return AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 220),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: isCustom
|
||||
? _CustomScaleMenuControls(ffi: ffi, onChanged: onChanged)
|
||||
: SizedBox.shrink(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
scrollStyle(_IconSubmenuButtonState state, ColorScheme colorScheme) {
|
||||
return futureBuilder(future: () async {
|
||||
final viewStyle =
|
||||
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
||||
final visible = viewStyle == kRemoteViewStyleOriginal;
|
||||
final visible = viewStyle == kRemoteViewStyleOriginal ||
|
||||
viewStyle == kRemoteViewStyleCustom;
|
||||
final scrollStyle =
|
||||
await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
|
||||
return {'visible': visible, 'scrollStyle': scrollStyle};
|
||||
final edgeScrollEdgeThickness = await bind
|
||||
.sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId);
|
||||
return {
|
||||
'visible': visible,
|
||||
'scrollStyle': scrollStyle,
|
||||
'edgeScrollEdgeThickness': edgeScrollEdgeThickness,
|
||||
};
|
||||
}(), hasData: (data) {
|
||||
final visible = data['visible'] as bool;
|
||||
if (!visible) return Offstage();
|
||||
final groupValue = data['scrollStyle'] as String;
|
||||
onChange(String? value) async {
|
||||
final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int;
|
||||
|
||||
onChangeScrollStyle(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionSetScrollStyle(
|
||||
sessionId: ffi.sessionId, value: value);
|
||||
widget.ffi.canvasModel.updateScrollStyle();
|
||||
state.setState(() {});
|
||||
}
|
||||
|
||||
final enabled = widget.ffi.canvasModel.imageOverflow.value;
|
||||
return Column(children: [
|
||||
RdoMenuButton<String>(
|
||||
child: Text(translate('ScrollAuto')),
|
||||
value: kRemoteScrollStyleAuto,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? (value) => onChange(value) : null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
RdoMenuButton<String>(
|
||||
child: Text(translate('Scrollbar')),
|
||||
value: kRemoteScrollStyleBar,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? (value) => onChange(value) : null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
Divider(),
|
||||
]);
|
||||
onChangeEdgeScrollEdgeThickness(double? value) async {
|
||||
if (value == null) return;
|
||||
final newThickness = value.round();
|
||||
await bind.sessionSetEdgeScrollEdgeThickness(
|
||||
sessionId: ffi.sessionId, value: newThickness);
|
||||
widget.ffi.canvasModel.updateEdgeScrollEdgeThickness(newThickness);
|
||||
state.setState(() {});
|
||||
}
|
||||
|
||||
return Obx(() => Column(children: [
|
||||
RdoMenuButton<String>(
|
||||
child: Text(translate('ScrollAuto')),
|
||||
value: kRemoteScrollStyleAuto,
|
||||
groupValue: groupValue,
|
||||
onChanged: widget.ffi.canvasModel.imageOverflow.value
|
||||
? (value) => onChangeScrollStyle(value)
|
||||
: null,
|
||||
closeOnActivate: groupValue != kRemoteScrollStyleEdge,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
RdoMenuButton<String>(
|
||||
child: Text(translate('Scrollbar')),
|
||||
value: kRemoteScrollStyleBar,
|
||||
groupValue: groupValue,
|
||||
onChanged: widget.ffi.canvasModel.imageOverflow.value
|
||||
? (value) => onChangeScrollStyle(value)
|
||||
: null,
|
||||
closeOnActivate: groupValue != kRemoteScrollStyleEdge,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
if (!isWeb) ...[
|
||||
RdoMenuButton<String>(
|
||||
child: Text(translate('ScrollEdge')),
|
||||
value: kRemoteScrollStyleEdge,
|
||||
groupValue: groupValue,
|
||||
closeOnActivate: false,
|
||||
onChanged: widget.ffi.canvasModel.imageOverflow.value
|
||||
? (value) => onChangeScrollStyle(value)
|
||||
: null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
Offstage(
|
||||
offstage: groupValue != kRemoteScrollStyleEdge,
|
||||
child: EdgeThicknessControl(
|
||||
value: edgeScrollEdgeThickness.toDouble(),
|
||||
onChanged: onChangeEdgeScrollEdgeThickness,
|
||||
colorScheme: colorScheme,
|
||||
)),
|
||||
],
|
||||
Divider(),
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1245,6 +1224,178 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomScaleMenuControls extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final ValueChanged<int>? onChanged;
|
||||
const _CustomScaleMenuControls({Key? key, required this.ffi, this.onChanged})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_CustomScaleMenuControls> createState() =>
|
||||
_CustomScaleMenuControlsState();
|
||||
}
|
||||
|
||||
class _CustomScaleMenuControlsState
|
||||
extends CustomScaleControls<_CustomScaleMenuControls> {
|
||||
@override
|
||||
FFI get ffi => widget.ffi;
|
||||
|
||||
@override
|
||||
ValueChanged<int>? get onScaleChanged => widget.onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
const smallBtnConstraints = BoxConstraints(minWidth: 28, minHeight: 28);
|
||||
|
||||
final sliderControl = Semantics(
|
||||
label: translate('Custom scale slider'),
|
||||
value: '$scaleValue%',
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: colorScheme.primary,
|
||||
thumbColor: colorScheme.primary,
|
||||
overlayColor: colorScheme.primary.withOpacity(0.1),
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
thumbShape: _RectValueThumbShape(
|
||||
min: CustomScaleControls.minPercent.toDouble(),
|
||||
max: CustomScaleControls.maxPercent.toDouble(),
|
||||
width: 52,
|
||||
height: 24,
|
||||
radius: 4,
|
||||
displayValueForNormalized: (t) => mapPosToPercent(t),
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: scalePos,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
// Use a wide range of divisions (calculated as (CustomScaleControls.maxPercent - CustomScaleControls.minPercent)) to provide ~1% precision increments.
|
||||
// This allows users to set precise scale values. Lower values would require more fine-tuning via the +/- buttons, which is undesirable for big ranges.
|
||||
divisions:
|
||||
(CustomScaleControls.maxPercent - CustomScaleControls.minPercent)
|
||||
.round(),
|
||||
onChanged: onSliderChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(children: [
|
||||
Tooltip(
|
||||
message: translate('Decrease'),
|
||||
child: IconButton(
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.all(1),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => nudgeScale(-1),
|
||||
),
|
||||
),
|
||||
Expanded(child: sliderControl),
|
||||
Tooltip(
|
||||
message: translate('Increase'),
|
||||
child: IconButton(
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.all(1),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => nudgeScale(1),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
Divider(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Lightweight rectangular thumb that paints the current percentage.
|
||||
// Stateless and uses only SliderTheme colors; avoids allocations beyond a TextPainter per frame.
|
||||
class _RectValueThumbShape extends SliderComponentShape {
|
||||
final double min;
|
||||
final double max;
|
||||
final double width;
|
||||
final double height;
|
||||
final double radius;
|
||||
final String unit;
|
||||
// Optional mapper to compute display value from normalized position [0,1]
|
||||
// If null, falls back to linear interpolation between min and max.
|
||||
final int Function(double normalized)? displayValueForNormalized;
|
||||
|
||||
const _RectValueThumbShape({
|
||||
required this.min,
|
||||
required this.max,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.radius,
|
||||
this.displayValueForNormalized,
|
||||
this.unit = '%',
|
||||
});
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return Size(width, height);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
|
||||
// Resolve color based on enabled/disabled animation, with safe fallbacks.
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
final Color? evaluatedColor = colorTween.evaluate(enableAnimation);
|
||||
final Color? thumbColor = sliderTheme.thumbColor;
|
||||
final Color fillColor = evaluatedColor ?? thumbColor ?? Colors.blueAccent;
|
||||
|
||||
final RRect rrect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: center, width: width, height: height),
|
||||
Radius.circular(radius),
|
||||
);
|
||||
final Paint paint = Paint()..color = fillColor;
|
||||
canvas.drawRRect(rrect, paint);
|
||||
|
||||
// Compute displayed value from normalized slider value.
|
||||
final int displayValue = displayValueForNormalized != null
|
||||
? displayValueForNormalized!(value)
|
||||
: (min + value * (max - min)).round();
|
||||
final TextSpan span = TextSpan(
|
||||
text: '$displayValue$unit',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
final TextPainter tp = TextPainter(
|
||||
text: span,
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: textDirection,
|
||||
);
|
||||
tp.layout(maxWidth: width - 4);
|
||||
tp.paint(
|
||||
canvas, Offset(center.dx - tp.width / 2, center.dy - tp.height / 2));
|
||||
}
|
||||
}
|
||||
|
||||
class _ResolutionsMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
@@ -1587,7 +1738,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
ffi: ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [
|
||||
menuChildrenGetter: (_) => [
|
||||
keyboardMode(),
|
||||
localKeyboardType(),
|
||||
inputSource(),
|
||||
@@ -1852,7 +2003,7 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
menuChildrenGetter: (_) => [textChat(), voiceCall()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,7 +2059,7 @@ class _VoiceCallMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
menuChildrenGetter() {
|
||||
menuChildrenGetter(_IconSubmenuButtonState state) {
|
||||
final audioInput = AudioInput(
|
||||
builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
@@ -2014,7 +2165,12 @@ class _CloseMenu extends StatelessWidget {
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/close.svg',
|
||||
tooltip: 'Close',
|
||||
onPressed: () => closeConnection(id: id),
|
||||
onPressed: () async {
|
||||
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
|
||||
return;
|
||||
}
|
||||
closeConnection(id: id);
|
||||
},
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor,
|
||||
);
|
||||
@@ -2108,7 +2264,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
final Widget? icon;
|
||||
final Color color;
|
||||
final Color hoverColor;
|
||||
final List<Widget> Function() menuChildrenGetter;
|
||||
final List<Widget> Function(_IconSubmenuButtonState state) menuChildrenGetter;
|
||||
final MenuStyle? menuStyle;
|
||||
final FFI? ffi;
|
||||
final double? width;
|
||||
@@ -2133,6 +2289,11 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
bool hover = false;
|
||||
|
||||
@override // discard @protected
|
||||
void setState(VoidCallback fn) {
|
||||
super.setState(fn);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(widget.svg != null || widget.icon != null);
|
||||
@@ -2165,7 +2326,7 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
),
|
||||
child: icon))),
|
||||
menuChildren: widget
|
||||
.menuChildrenGetter()
|
||||
.menuChildrenGetter(this)
|
||||
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
|
||||
.toList()));
|
||||
return MenuBar(children: [
|
||||
@@ -2266,6 +2427,8 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI? ffi;
|
||||
// When true, submenu will be dismissed on activate; when false, it stays open.
|
||||
final bool closeOnActivate;
|
||||
const RdoMenuButton({
|
||||
Key? key,
|
||||
required this.value,
|
||||
@@ -2273,6 +2436,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
required this.child,
|
||||
this.ffi,
|
||||
this.onChanged,
|
||||
this.closeOnActivate = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2281,9 +2445,10 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
child: child,
|
||||
closeOnActivate: closeOnActivate,
|
||||
onChanged: onChanged != null
|
||||
? (T? value) {
|
||||
if (ffi != null) {
|
||||
if (ffi != null && closeOnActivate) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
@@ -2524,3 +2689,56 @@ Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class EdgeThicknessControl extends StatelessWidget {
|
||||
final double value;
|
||||
final ValueChanged<double>? onChanged;
|
||||
final ColorScheme? colorScheme;
|
||||
|
||||
const EdgeThicknessControl({
|
||||
Key? key,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
this.colorScheme,
|
||||
}) : super(key: key);
|
||||
|
||||
static const double kMin = 20;
|
||||
static const double kMax = 150;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = this.colorScheme ?? Theme.of(context).colorScheme;
|
||||
|
||||
final slider = SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: colorScheme.primary,
|
||||
thumbColor: colorScheme.primary,
|
||||
overlayColor: colorScheme.primary.withOpacity(0.1),
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
thumbShape: _RectValueThumbShape(
|
||||
min: EdgeThicknessControl.kMin,
|
||||
max: EdgeThicknessControl.kMax,
|
||||
width: 52,
|
||||
height: 24,
|
||||
radius: 4,
|
||||
unit: 'px',
|
||||
),
|
||||
),
|
||||
child: Semantics(
|
||||
value: value.toInt().toString(),
|
||||
child: Slider(
|
||||
value: value,
|
||||
min: EdgeThicknessControl.kMin,
|
||||
max: EdgeThicknessControl.kMax,
|
||||
divisions:
|
||||
(EdgeThicknessControl.kMax - EdgeThicknessControl.kMin).round(),
|
||||
semanticFormatterCallback: (double newValue) =>
|
||||
"${newValue.round()}px",
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return slider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,6 @@ class DesktopTab extends StatefulWidget {
|
||||
// ignore: must_be_immutable
|
||||
class _DesktopTabState extends State<DesktopTab>
|
||||
with MultiWindowListener, WindowListener {
|
||||
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||
Timer? _macOSCheckRestoreTimer;
|
||||
int _macOSCheckRestoreCounter = 0;
|
||||
|
||||
@@ -370,7 +369,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
|
||||
void _setMaximized(bool maximize) {
|
||||
stateGlobal.setMaximized(maximize);
|
||||
_saveFrameDebounce.call(_saveFrame);
|
||||
_saveFrame();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -405,24 +404,29 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
super.onWindowUnmaximize();
|
||||
}
|
||||
|
||||
_saveFrame() async {
|
||||
if (tabType == DesktopTabType.main) {
|
||||
await saveWindowPosition(WindowType.Main);
|
||||
} else if (kWindowType != null && kWindowId != null) {
|
||||
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
||||
_saveFrame({bool? flush}) async {
|
||||
try {
|
||||
if (tabType == DesktopTabType.main) {
|
||||
await saveWindowPosition(WindowType.Main, flush: flush);
|
||||
} else if (kWindowType != null && kWindowId != null) {
|
||||
await saveWindowPosition(kWindowType!,
|
||||
windowId: kWindowId, flush: flush);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error saving window position: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMoved() {
|
||||
_saveFrameDebounce.call(_saveFrame);
|
||||
_saveFrame();
|
||||
super.onWindowMoved();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResized() {
|
||||
_saveFrameDebounce.call(_saveFrame);
|
||||
super.onWindowMoved();
|
||||
_saveFrame();
|
||||
super.onWindowResized();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -460,6 +464,8 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
});
|
||||
}
|
||||
|
||||
await _saveFrame(flush: true);
|
||||
|
||||
// hide window on close
|
||||
if (isMainWindow) {
|
||||
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
||||
@@ -1079,11 +1085,12 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
||||
child: Tooltip(
|
||||
message: widget.tabType == DesktopTabType.main
|
||||
? ''
|
||||
: translate(widget.label.value),
|
||||
message:
|
||||
widget.tabType == DesktopTabType.main ? '' : widget.label.value,
|
||||
child: Text(
|
||||
translate(widget.label.value),
|
||||
widget.tabType == DesktopTabType.main
|
||||
? translate(widget.label.value)
|
||||
: widget.label.value,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
|
||||
@@ -182,6 +182,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
note: '',
|
||||
);
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,11 @@ import '../../common/widgets/dialog.dart';
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
{Key? key,
|
||||
required this.id,
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
@@ -92,6 +96,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
WakelockPlus.disable();
|
||||
});
|
||||
model.jobController.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -112,8 +117,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
leading: Row(children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () =>
|
||||
clientClose(gFFI.sessionId, gFFI.dialogManager)),
|
||||
onPressed: () => clientClose(gFFI.sessionId, gFFI)),
|
||||
]),
|
||||
centerTitle: true,
|
||||
title: ToggleSwitch(
|
||||
@@ -424,6 +428,7 @@ class FileManagerView extends StatefulWidget {
|
||||
class _FileManagerViewState extends State<FileManagerView> {
|
||||
final _listScrollController = ScrollController();
|
||||
final _breadCrumbScroller = ScrollController();
|
||||
late final ascending = Rx<bool>(controller.sortAscending);
|
||||
|
||||
bool get isLocal => widget.controller.isLocal;
|
||||
FileController get controller => widget.controller;
|
||||
@@ -635,7 +640,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
onSelected: controller.changeSortStyle),
|
||||
onSelected: (sortBy) {
|
||||
// If selecting the same sort option, flip the order
|
||||
// If selecting a different sort option, use ascending order
|
||||
if (controller.sortBy.value == sortBy) {
|
||||
ascending.value = !controller.sortAscending;
|
||||
} else {
|
||||
ascending.value = true;
|
||||
}
|
||||
controller.changeSortStyle(sortBy,
|
||||
ascending: ascending.value);
|
||||
}),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/floating_mouse.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/floating_mouse_widgets.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
@@ -23,6 +25,7 @@ import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../utils/image.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
import '../widgets/custom_scale_widget.dart';
|
||||
|
||||
final initText = '1' * 1024;
|
||||
|
||||
@@ -363,7 +366,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -481,7 +484,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -574,7 +577,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
color: MyTheme.canvasColor,
|
||||
child: Stack(children: () {
|
||||
final paints = [
|
||||
ImagePaint(),
|
||||
ImagePaint(ffiModel: gFFI.ffiModel),
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
@@ -617,13 +620,22 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
if (showCursorPaint) {
|
||||
paints.add(CursorPaint(widget.id));
|
||||
}
|
||||
if (gFFI.ffiModel.touchMode) {
|
||||
paints.add(FloatingMouse(
|
||||
ffi: gFFI,
|
||||
));
|
||||
} else {
|
||||
paints.add(FloatingMouseWidgets(
|
||||
ffi: gFFI,
|
||||
));
|
||||
}
|
||||
return paints;
|
||||
}()));
|
||||
}
|
||||
|
||||
Widget getBodyForDesktopWithListener() {
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
var paints = <Widget>[ImagePaint()];
|
||||
var paints = <Widget>[ImagePaint(ffiModel: ffiModel)];
|
||||
if (showCursorPaint) {
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
@@ -789,13 +801,14 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
controller: ScrollController(),
|
||||
padding: EdgeInsets.symmetric(vertical: 10),
|
||||
child: GestureHelp(
|
||||
touchMode: gFFI.ffiModel.touchMode,
|
||||
onTouchModeChange: (t) {
|
||||
gFFI.ffiModel.toggleTouchMode();
|
||||
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||
bind.sessionPeerOption(
|
||||
sessionId: sessionId, name: kOptionTouchMode, value: v);
|
||||
})));
|
||||
touchMode: gFFI.ffiModel.touchMode,
|
||||
onTouchModeChange: (t) {
|
||||
gFFI.ffiModel.toggleTouchMode();
|
||||
final v = gFFI.ffiModel.touchMode ? 'Y' : 'N';
|
||||
bind.mainSetLocalOption(key: kOptionTouchMode, value: v);
|
||||
},
|
||||
virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
|
||||
)));
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
@@ -1042,11 +1055,20 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
}
|
||||
|
||||
class ImagePaint extends StatelessWidget {
|
||||
final FfiModel ffiModel;
|
||||
ImagePaint({Key? key, required this.ffiModel}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
var s = c.scale;
|
||||
if (ffiModel.isPeerLinux) {
|
||||
final displays = ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
s = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
final adjust = c.getAdjustY();
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
@@ -1117,6 +1139,14 @@ void showOptions(
|
||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||
final numColorSelected = Colors.white;
|
||||
final numColorUnselected = isDarkTheme ? Colors.grey : Colors.black87;
|
||||
// We can't use `Theme.of(context).primaryColor` here, the color is:
|
||||
// - light theme: 0xff2196f3 (Colors.blue)
|
||||
// - dark theme: 0xff212121 (the canvas color?)
|
||||
final numBgSelected =
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.6);
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
@@ -1130,13 +1160,12 @@ void showOptions(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).hintColor),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
color: i == cur
|
||||
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||
: null),
|
||||
color: i == cur ? numBgSelected : null),
|
||||
child: Center(
|
||||
child: Text((i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: i == cur ? Colors.white : Colors.black87,
|
||||
color:
|
||||
i == cur ? numColorSelected : numColorUnselected,
|
||||
fontWeight: FontWeight.bold))))));
|
||||
}
|
||||
displays.add(Padding(
|
||||
@@ -1189,6 +1218,10 @@ void showOptions(
|
||||
if (v != null) viewStyle.value = v;
|
||||
}
|
||||
: null)),
|
||||
// Show custom scale controls when custom view style is selected
|
||||
Obx(() => viewStyle.value == kRemoteViewStyleCustom
|
||||
? MobileCustomScaleControls(ffi: gFFI)
|
||||
: const SizedBox.shrink()),
|
||||
const Divider(color: MyTheme.border),
|
||||
for (var e in imageQualityRadios)
|
||||
Obx(() => getRadio<String>(
|
||||
|
||||
@@ -156,7 +156,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
try {
|
||||
final sc = ServerConfig.decode(data.substring(7));
|
||||
Timer(Duration(milliseconds: 60), () {
|
||||
showServerSettingsWithValue(sc, gFFI.dialogManager);
|
||||
showServerSettingsWithValue(sc, gFFI.dialogManager, null);
|
||||
});
|
||||
} catch (e) {
|
||||
showToast('Invalid QR code');
|
||||
|
||||
@@ -94,7 +94,11 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _hideWebSocket = false;
|
||||
var _enableTrustedDevices = false;
|
||||
var _enableUdpPunch = false;
|
||||
var _allowInsecureTlsFallback = false;
|
||||
var _disableUdp = false;
|
||||
var _enableIpv6Punch = false;
|
||||
var _isUsingPublicServer = false;
|
||||
var _allowAskForNoteAtEndOfConnection = false;
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
@@ -109,6 +113,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||
_allowWebSocket = mainGetBoolOptionSync(kOptionAllowWebSocket);
|
||||
_allowInsecureTlsFallback =
|
||||
mainGetBoolOptionSync(kOptionAllowInsecureTLSFallback);
|
||||
_disableUdp = bind.mainGetOptionSync(key: kOptionDisableUdp) == 'Y';
|
||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||
@@ -130,6 +137,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
||||
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||
_allowAskForNoteAtEndOfConnection =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -200,6 +209,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
update = true;
|
||||
_buildDate = buildDate;
|
||||
}
|
||||
|
||||
final isUsingPublicServer = await bind.mainIsUsingPublicServer();
|
||||
if (_isUsingPublicServer != isUsingPublicServer) {
|
||||
update = true;
|
||||
_isUsingPublicServer = isUsingPublicServer;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
setState(() {});
|
||||
}
|
||||
@@ -667,9 +683,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
showServerSettings(gFFI.dialogManager, (callback) async {
|
||||
_isUsingPublicServer = await bind.mainIsUsingPublicServer();
|
||||
setState(callback);
|
||||
});
|
||||
}),
|
||||
if (!isIOS && !_hideNetwork && !_hideProxy)
|
||||
if (!_hideNetwork && !_hideProxy)
|
||||
SettingsTile(
|
||||
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||
leading: Icon(Icons.network_ping),
|
||||
@@ -691,6 +710,38 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!_isUsingPublicServer)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Allow insecure TLS fallback')),
|
||||
initialValue: _allowInsecureTlsFallback,
|
||||
onToggle: isOptionFixed(kOptionAllowInsecureTLSFallback)
|
||||
? null
|
||||
: (v) async {
|
||||
await mainSetBoolOption(
|
||||
kOptionAllowInsecureTLSFallback, v);
|
||||
final newValue = mainGetBoolOptionSync(
|
||||
kOptionAllowInsecureTLSFallback);
|
||||
setState(() {
|
||||
_allowInsecureTlsFallback = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isAndroid && !outgoingOnly && !_isUsingPublicServer)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Disable UDP')),
|
||||
initialValue: _disableUdp,
|
||||
onToggle: isOptionFixed(kOptionDisableUdp)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionDisableUdp, value: v ? 'Y' : 'N');
|
||||
final newValue =
|
||||
bind.mainGetOptionSync(key: kOptionDisableUdp) == 'Y';
|
||||
setState(() {
|
||||
_disableUdp = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable UDP hole punching')),
|
||||
@@ -734,6 +785,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
onPressed: (context) {
|
||||
showThemeSettings(gFFI.dialogManager);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('note-at-conn-end-tip')),
|
||||
initialValue: _allowAskForNoteAtEndOfConnection,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, v);
|
||||
final newValue = mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection);
|
||||
setState(() {
|
||||
_allowAskForNoteAtEndOfConnection = newValue;
|
||||
});
|
||||
},
|
||||
)
|
||||
]),
|
||||
if (isAndroid)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -38,6 +39,8 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
? (GoogleFonts.robotoMono().fontFamily ?? 'monospace')
|
||||
: 'monospace';
|
||||
|
||||
SessionID get sessionId => _ffi.sessionId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -82,6 +85,16 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi);
|
||||
return false; // Prevent default back behavior
|
||||
},
|
||||
child: buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
|
||||
@@ -197,7 +197,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -310,7 +310,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -590,6 +590,14 @@ void showOptions(
|
||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||
final numColorSelected = Colors.white;
|
||||
final numColorUnselected = isDarkTheme ? Colors.grey : Colors.black87;
|
||||
// We can't use `Theme.of(context).primaryColor` here, the color is:
|
||||
// - light theme: 0xff2196f3 (Colors.blue)
|
||||
// - dark theme: 0xff212121 (the canvas color?)
|
||||
final numBgSelected =
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.6);
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
@@ -603,13 +611,12 @@ void showOptions(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).hintColor),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
color: i == cur
|
||||
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||
: null),
|
||||
color: i == cur ? numBgSelected : null),
|
||||
child: Center(
|
||||
child: Text((i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: i == cur ? Colors.white : Colors.black87,
|
||||
color:
|
||||
i == cur ? numColorSelected : numColorUnselected,
|
||||
fontWeight: FontWeight.bold))))));
|
||||
}
|
||||
displays.add(Padding(
|
||||
|
||||
71
flutter/lib/mobile/widgets/custom_scale_widget.dart
Normal file
71
flutter/lib/mobile/widgets/custom_scale_widget.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
|
||||
|
||||
class MobileCustomScaleControls extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final ValueChanged<int>? onChanged;
|
||||
const MobileCustomScaleControls({super.key, required this.ffi, this.onChanged});
|
||||
|
||||
@override
|
||||
State<MobileCustomScaleControls> createState() => _MobileCustomScaleControlsState();
|
||||
}
|
||||
|
||||
class _MobileCustomScaleControlsState extends CustomScaleControls<MobileCustomScaleControls> {
|
||||
@override
|
||||
FFI get ffi => widget.ffi;
|
||||
|
||||
@override
|
||||
ValueChanged<int>? get onScaleChanged => widget.onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Smaller button size for mobile
|
||||
const smallBtnConstraints = BoxConstraints(minWidth: 32, minHeight: 32);
|
||||
|
||||
final sliderControl = Slider(
|
||||
value: scalePos,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: (CustomScaleControls.maxPercent - CustomScaleControls.minPercent).round(),
|
||||
label: '$scaleValue%',
|
||||
onChanged: onSliderChanged,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${translate("Scale custom")}: $scaleValue%',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 20,
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: translate('Decrease'),
|
||||
onPressed: () => nudgeScale(-1),
|
||||
),
|
||||
Expanded(child: sliderControl),
|
||||
IconButton(
|
||||
iconSize: 20,
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: translate('Increase'),
|
||||
onPressed: () => nudgeScale(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -147,18 +147,22 @@ void setTemporaryPasswordLengthDialog(
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
void showServerSettings(OverlayDialogManager dialogManager,
|
||||
void Function(VoidCallback) setState) async {
|
||||
Map<String, dynamic> options = {};
|
||||
try {
|
||||
options = jsonDecode(await bind.mainGetOptions());
|
||||
} catch (e) {
|
||||
print("Invalid server config: $e");
|
||||
}
|
||||
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||
showServerSettingsWithValue(
|
||||
ServerConfig.fromOptions(options), dialogManager, setState);
|
||||
}
|
||||
|
||||
void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||
ServerConfig serverConfig,
|
||||
OverlayDialogManager dialogManager,
|
||||
void Function(VoidCallback)? upSetState) async {
|
||||
var isInProgress = false;
|
||||
final idCtrl = TextEditingController(text: serverConfig.idServer);
|
||||
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
|
||||
@@ -288,6 +292,7 @@ void showServerSettingsWithValue(
|
||||
if (await submit()) {
|
||||
close();
|
||||
showToast(translate('Successful'));
|
||||
upSetState?.call(() {});
|
||||
} else {
|
||||
showToast(translate('Failed'));
|
||||
}
|
||||
|
||||
1209
flutter/lib/mobile/widgets/floating_mouse.dart
Normal file
1209
flutter/lib/mobile/widgets/floating_mouse.dart
Normal file
File diff suppressed because it is too large
Load Diff
880
flutter/lib/mobile/widgets/floating_mouse_widgets.dart
Normal file
880
flutter/lib/mobile/widgets/floating_mouse_widgets.dart
Normal file
@@ -0,0 +1,880 @@
|
||||
// These floating mouse widgets are used to simulate a physical mouse
|
||||
// when "mobile" -> "desktop" in mouse mode.
|
||||
// This file does not contain whole mouse widgets, it only contains
|
||||
// parts that help to control, such as wheel scroll and wheel button.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/remote_input.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
// Used for the wheel button and wheel scroll widgets
|
||||
const double _kSpaceToHorizontalEdge = 25;
|
||||
const double _wheelWidth = 50;
|
||||
const double _wheelHeight = 162;
|
||||
// Used for the left/right button widgets
|
||||
const double _kSpaceToVerticalEdge = 15;
|
||||
const double _kSpaceBetweenLeftRightButtons = 40;
|
||||
const double _kLeftRightButtonWidth = 55;
|
||||
const double _kLeftRightButtonHeight = 40;
|
||||
const double _kBorderWidth = 1;
|
||||
final Color _kDefaultBorderColor = Colors.white.withOpacity(0.7);
|
||||
final Color _kDefaultColor = Colors.black.withOpacity(0.4);
|
||||
final Color _kTapDownColor = Colors.blue.withOpacity(0.7);
|
||||
final Color _kWidgetHighlightColor = Colors.white.withOpacity(0.9);
|
||||
const int _kInputTimerIntervalMillis = 100;
|
||||
|
||||
class FloatingMouseWidgets extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
const FloatingMouseWidgets({
|
||||
super.key,
|
||||
required this.ffi,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FloatingMouseWidgets> createState() => _FloatingMouseWidgetsState();
|
||||
}
|
||||
|
||||
class _FloatingMouseWidgetsState extends State<FloatingMouseWidgets> {
|
||||
InputModel get _inputModel => widget.ffi.inputModel;
|
||||
CursorModel get _cursorModel => widget.ffi.cursorModel;
|
||||
late final VirtualMouseMode _virtualMouseMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_virtualMouseMode = widget.ffi.ffiModel.virtualMouseMode;
|
||||
_virtualMouseMode.addListener(_onVirtualMouseModeChanged);
|
||||
_cursorModel.blockEvents = false;
|
||||
isSpecialHoldDragActive = false;
|
||||
}
|
||||
|
||||
void _onVirtualMouseModeChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_virtualMouseMode.removeListener(_onVirtualMouseModeChanged);
|
||||
super.dispose();
|
||||
_cursorModel.blockEvents = false;
|
||||
isSpecialHoldDragActive = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final virtualMouseMode = _virtualMouseMode;
|
||||
if (!virtualMouseMode.showVirtualMouse) {
|
||||
return const Offstage();
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
FloatingWheel(
|
||||
inputModel: _inputModel,
|
||||
cursorModel: _cursorModel,
|
||||
),
|
||||
if (virtualMouseMode.showVirtualJoystick)
|
||||
VirtualJoystick(cursorModel: _cursorModel),
|
||||
FloatingLeftRightButton(
|
||||
isLeft: true,
|
||||
inputModel: _inputModel,
|
||||
cursorModel: _cursorModel,
|
||||
),
|
||||
FloatingLeftRightButton(
|
||||
isLeft: false,
|
||||
inputModel: _inputModel,
|
||||
cursorModel: _cursorModel,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingWheel extends StatefulWidget {
|
||||
final InputModel inputModel;
|
||||
final CursorModel cursorModel;
|
||||
const FloatingWheel(
|
||||
{super.key, required this.inputModel, required this.cursorModel});
|
||||
|
||||
@override
|
||||
State<FloatingWheel> createState() => _FloatingWheelState();
|
||||
}
|
||||
|
||||
class _FloatingWheelState extends State<FloatingWheel> {
|
||||
Offset _position = Offset.zero;
|
||||
bool _isInitialized = false;
|
||||
Rect? _lastBlockedRect;
|
||||
|
||||
bool _isUpDown = false;
|
||||
bool _isMidDown = false;
|
||||
bool _isDownDown = false;
|
||||
|
||||
Orientation? _previousOrientation;
|
||||
|
||||
Timer? _scrollTimer;
|
||||
|
||||
InputModel get _inputModel => widget.inputModel;
|
||||
CursorModel get _cursorModel => widget.cursorModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_resetPosition();
|
||||
});
|
||||
}
|
||||
|
||||
void _resetPosition() {
|
||||
final size = MediaQuery.of(context).size;
|
||||
setState(() {
|
||||
_position = Offset(
|
||||
size.width - _wheelWidth - _kSpaceToHorizontalEdge,
|
||||
(size.height - _wheelHeight) / 2,
|
||||
);
|
||||
_isInitialized = true;
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _updateBlockedRect();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateBlockedRect() {
|
||||
if (_lastBlockedRect != null) {
|
||||
_cursorModel.removeBlockedRect(_lastBlockedRect!);
|
||||
}
|
||||
final newRect =
|
||||
Rect.fromLTWH(_position.dx, _position.dy, _wheelWidth, _wheelHeight);
|
||||
_cursorModel.addBlockedRect(newRect);
|
||||
_lastBlockedRect = newRect;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollTimer?.cancel();
|
||||
if (_lastBlockedRect != null) {
|
||||
_cursorModel.removeBlockedRect(_lastBlockedRect!);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final currentOrientation = MediaQuery.of(context).orientation;
|
||||
if (_previousOrientation != null &&
|
||||
_previousOrientation != currentOrientation) {
|
||||
_resetPosition();
|
||||
}
|
||||
_previousOrientation = currentOrientation;
|
||||
}
|
||||
|
||||
Widget _buildUpDownButton(
|
||||
void Function(PointerDownEvent) onPointerDown,
|
||||
void Function(PointerUpEvent) onPointerUp,
|
||||
void Function(PointerCancelEvent) onPointerCancel,
|
||||
bool Function() flagGetter,
|
||||
BorderRadiusGeometry borderRadius,
|
||||
IconData iconData) {
|
||||
return Listener(
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerCancel: onPointerCancel,
|
||||
child: Container(
|
||||
width: _wheelWidth,
|
||||
height: 55,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _kDefaultColor,
|
||||
border: Border.all(
|
||||
color: flagGetter() ? _kTapDownColor : _kDefaultBorderColor,
|
||||
width: 1),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Icon(iconData, color: _kDefaultBorderColor, size: 32),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isInitialized) {
|
||||
return Positioned(child: Offstage());
|
||||
}
|
||||
return Positioned(
|
||||
left: _position.dx,
|
||||
top: _position.dy,
|
||||
child: _buildWidget(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWidget(BuildContext context) {
|
||||
return Container(
|
||||
width: _wheelWidth,
|
||||
height: _wheelHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildUpDownButton(
|
||||
(event) {
|
||||
setState(() {
|
||||
_isUpDown = true;
|
||||
});
|
||||
_startScrollTimer(1);
|
||||
},
|
||||
(event) {
|
||||
setState(() {
|
||||
_isUpDown = false;
|
||||
});
|
||||
_stopScrollTimer();
|
||||
},
|
||||
(event) {
|
||||
setState(() {
|
||||
_isUpDown = false;
|
||||
});
|
||||
_stopScrollTimer();
|
||||
},
|
||||
() => _isUpDown,
|
||||
BorderRadius.vertical(top: Radius.circular(_wheelWidth * 0.5)),
|
||||
Icons.keyboard_arrow_up,
|
||||
),
|
||||
Listener(
|
||||
onPointerDown: (event) {
|
||||
setState(() {
|
||||
_isMidDown = true;
|
||||
});
|
||||
_inputModel.tapDown(MouseButtons.wheel);
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
setState(() {
|
||||
_isMidDown = false;
|
||||
});
|
||||
_inputModel.tapUp(MouseButtons.wheel);
|
||||
},
|
||||
onPointerCancel: (event) {
|
||||
setState(() {
|
||||
_isMidDown = false;
|
||||
});
|
||||
_inputModel.tapUp(MouseButtons.wheel);
|
||||
},
|
||||
child: Container(
|
||||
width: _wheelWidth,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: _kDefaultColor,
|
||||
border: Border.symmetric(
|
||||
vertical: BorderSide(
|
||||
color:
|
||||
_isMidDown ? _kTapDownColor : _kDefaultBorderColor,
|
||||
width: _kBorderWidth)),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: _wheelWidth - 10,
|
||||
height: _wheelWidth - 10,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 18,
|
||||
height: 2,
|
||||
color: _kDefaultBorderColor,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Container(
|
||||
width: 24,
|
||||
height: 2,
|
||||
color: _kDefaultBorderColor,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Container(
|
||||
width: 18,
|
||||
height: 2,
|
||||
color: _kDefaultBorderColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildUpDownButton(
|
||||
(event) {
|
||||
setState(() {
|
||||
_isDownDown = true;
|
||||
});
|
||||
_startScrollTimer(-1);
|
||||
},
|
||||
(event) {
|
||||
setState(() {
|
||||
_isDownDown = false;
|
||||
});
|
||||
_stopScrollTimer();
|
||||
},
|
||||
(event) {
|
||||
setState(() {
|
||||
_isDownDown = false;
|
||||
});
|
||||
_stopScrollTimer();
|
||||
},
|
||||
() => _isDownDown,
|
||||
BorderRadius.vertical(bottom: Radius.circular(_wheelWidth * 0.5)),
|
||||
Icons.keyboard_arrow_down,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _startScrollTimer(int direction) {
|
||||
_scrollTimer?.cancel();
|
||||
_inputModel.scroll(direction);
|
||||
_scrollTimer = Timer.periodic(
|
||||
Duration(milliseconds: _kInputTimerIntervalMillis), (timer) {
|
||||
_inputModel.scroll(direction);
|
||||
});
|
||||
}
|
||||
|
||||
void _stopScrollTimer() {
|
||||
_scrollTimer?.cancel();
|
||||
_scrollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingLeftRightButton extends StatefulWidget {
|
||||
final bool isLeft;
|
||||
final InputModel inputModel;
|
||||
final CursorModel cursorModel;
|
||||
const FloatingLeftRightButton(
|
||||
{super.key,
|
||||
required this.isLeft,
|
||||
required this.inputModel,
|
||||
required this.cursorModel});
|
||||
|
||||
@override
|
||||
State<FloatingLeftRightButton> createState() =>
|
||||
_FloatingLeftRightButtonState();
|
||||
}
|
||||
|
||||
class _FloatingLeftRightButtonState extends State<FloatingLeftRightButton> {
|
||||
Offset _position = Offset.zero;
|
||||
bool _isInitialized = false;
|
||||
bool _isDown = false;
|
||||
Rect? _lastBlockedRect;
|
||||
|
||||
Orientation? _previousOrientation;
|
||||
Offset _preSavedPos = Offset.zero;
|
||||
|
||||
// Gesture ambiguity resolution
|
||||
Timer? _tapDownTimer;
|
||||
final Duration _pressTimeout = const Duration(milliseconds: 200);
|
||||
bool _isDragging = false;
|
||||
|
||||
bool get _isLeft => widget.isLeft;
|
||||
InputModel get _inputModel => widget.inputModel;
|
||||
CursorModel get _cursorModel => widget.cursorModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final currentOrientation = MediaQuery.of(context).orientation;
|
||||
_previousOrientation = currentOrientation;
|
||||
_resetPosition(currentOrientation);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_lastBlockedRect != null) {
|
||||
_cursorModel.removeBlockedRect(_lastBlockedRect!);
|
||||
}
|
||||
_tapDownTimer?.cancel();
|
||||
_trySavePosition();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final currentOrientation = MediaQuery.of(context).orientation;
|
||||
if (_previousOrientation == null ||
|
||||
_previousOrientation != currentOrientation) {
|
||||
_resetPosition(currentOrientation);
|
||||
}
|
||||
_previousOrientation = currentOrientation;
|
||||
}
|
||||
|
||||
double _getOffsetX(double w) {
|
||||
if (_isLeft) {
|
||||
return (w - _kLeftRightButtonWidth * 2 - _kSpaceBetweenLeftRightButtons) *
|
||||
0.5;
|
||||
} else {
|
||||
return (w + _kSpaceBetweenLeftRightButtons) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
String _getPositionKey(Orientation ori) {
|
||||
final strLeftRight = _isLeft ? 'l' : 'r';
|
||||
final strOri = ori == Orientation.landscape ? 'l' : 'p';
|
||||
return '$strLeftRight$strOri-mouse-btn-pos';
|
||||
}
|
||||
|
||||
static Offset? _loadPositionFromString(String s) {
|
||||
if (s.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final m = jsonDecode(s);
|
||||
return Offset(m['x'], m['y']);
|
||||
} catch (e) {
|
||||
debugPrintStack(label: 'Failed to load position "$s" $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _trySavePosition() {
|
||||
if (_previousOrientation == null) return;
|
||||
if (((_position - _preSavedPos)).distanceSquared < 0.1) return;
|
||||
final pos = jsonEncode({
|
||||
'x': _position.dx,
|
||||
'y': _position.dy,
|
||||
});
|
||||
bind.setLocalFlutterOption(
|
||||
k: _getPositionKey(_previousOrientation!), v: pos);
|
||||
_preSavedPos = _position;
|
||||
}
|
||||
|
||||
void _restorePosition(Orientation ori) {
|
||||
final ps = bind.getLocalFlutterOption(k: _getPositionKey(ori));
|
||||
final pos = _loadPositionFromString(ps);
|
||||
if (pos == null) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
_position = Offset(_getOffsetX(size.width),
|
||||
size.height - _kSpaceToVerticalEdge - _kLeftRightButtonHeight);
|
||||
} else {
|
||||
_position = pos;
|
||||
_preSavedPos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetPosition(Orientation ori) {
|
||||
setState(() {
|
||||
_restorePosition(ori);
|
||||
_isInitialized = true;
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _updateBlockedRect();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateBlockedRect() {
|
||||
if (_lastBlockedRect != null) {
|
||||
_cursorModel.removeBlockedRect(_lastBlockedRect!);
|
||||
}
|
||||
final newRect = Rect.fromLTWH(_position.dx, _position.dy,
|
||||
_kLeftRightButtonWidth, _kLeftRightButtonHeight);
|
||||
_cursorModel.addBlockedRect(newRect);
|
||||
_lastBlockedRect = newRect;
|
||||
}
|
||||
|
||||
void _onMoveUpdateDelta(Offset delta) {
|
||||
final context = this.context;
|
||||
final size = MediaQuery.of(context).size;
|
||||
Offset newPosition = _position + delta;
|
||||
double minX = _kSpaceToHorizontalEdge;
|
||||
double minY = _kSpaceToVerticalEdge;
|
||||
double maxX = size.width - _kLeftRightButtonWidth - _kSpaceToHorizontalEdge;
|
||||
double maxY = size.height - _kLeftRightButtonHeight - _kSpaceToVerticalEdge;
|
||||
newPosition = Offset(
|
||||
newPosition.dx.clamp(minX, maxX),
|
||||
newPosition.dy.clamp(minY, maxY),
|
||||
);
|
||||
final isPositionChanged = !(isDoubleEqual(newPosition.dx, _position.dx) &&
|
||||
isDoubleEqual(newPosition.dy, _position.dy));
|
||||
setState(() {
|
||||
_position = newPosition;
|
||||
});
|
||||
if (isPositionChanged) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _updateBlockedRect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onBodyPointerMoveUpdate(PointerMoveEvent event) {
|
||||
_cursorModel.blockEvents = true;
|
||||
// If move, it's a drag, not a tap.
|
||||
_isDragging = true;
|
||||
// Cancel the timer to prevent it from being recognized as a tap/hold.
|
||||
_tapDownTimer?.cancel();
|
||||
_tapDownTimer = null;
|
||||
_onMoveUpdateDelta(event.delta);
|
||||
}
|
||||
|
||||
Widget _buildButtonIcon() {
|
||||
final double w = _kLeftRightButtonWidth * 0.45;
|
||||
final double h = _kLeftRightButtonHeight * 0.75;
|
||||
final double borderRadius = w * 0.5;
|
||||
final double quarterCircleRadius = borderRadius * 0.9;
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: w,
|
||||
height: h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_kLeftRightButtonWidth * 0.225),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: _isLeft ? quarterCircleRadius * 0.25 : null,
|
||||
right: _isLeft ? null : quarterCircleRadius * 0.25,
|
||||
top: quarterCircleRadius * 0.25,
|
||||
child: CustomPaint(
|
||||
size: Size(quarterCircleRadius * 2, quarterCircleRadius * 2),
|
||||
painter: _QuarterCirclePainter(
|
||||
color: _kDefaultColor,
|
||||
isLeft: _isLeft,
|
||||
radius: quarterCircleRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isInitialized) {
|
||||
return Positioned(child: Offstage());
|
||||
}
|
||||
return Positioned(
|
||||
left: _position.dx,
|
||||
top: _position.dy,
|
||||
// We can't use the GestureDetector here, because `onTapDown` may be
|
||||
// triggered sometimes when dragging.
|
||||
child: Listener(
|
||||
onPointerMove: _onBodyPointerMoveUpdate,
|
||||
onPointerDown: (event) async {
|
||||
_isDragging = false;
|
||||
setState(() {
|
||||
_isDown = true;
|
||||
});
|
||||
// Start a timer. If it fires, it's a hold.
|
||||
_tapDownTimer?.cancel();
|
||||
_tapDownTimer = Timer(_pressTimeout, () {
|
||||
isSpecialHoldDragActive = true;
|
||||
() async {
|
||||
await _cursorModel.syncCursorPosition();
|
||||
await _inputModel
|
||||
.tapDown(_isLeft ? MouseButtons.left : MouseButtons.right);
|
||||
}();
|
||||
_tapDownTimer = null;
|
||||
});
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
_cursorModel.blockEvents = false;
|
||||
setState(() {
|
||||
_isDown = false;
|
||||
});
|
||||
// If timer is active, it's a quick tap.
|
||||
if (_tapDownTimer != null) {
|
||||
_tapDownTimer!.cancel();
|
||||
_tapDownTimer = null;
|
||||
// Fire tap down and up quickly.
|
||||
_inputModel
|
||||
.tapDown(_isLeft ? MouseButtons.left : MouseButtons.right)
|
||||
.then(
|
||||
(_) => Future.delayed(const Duration(milliseconds: 50), () {
|
||||
_inputModel.tapUp(
|
||||
_isLeft ? MouseButtons.left : MouseButtons.right);
|
||||
}));
|
||||
} else {
|
||||
// If it's not a quick tap, it could be a hold or drag.
|
||||
// If it was a hold, isSpecialHoldDragActive is true.
|
||||
if (isSpecialHoldDragActive) {
|
||||
_inputModel
|
||||
.tapUp(_isLeft ? MouseButtons.left : MouseButtons.right);
|
||||
}
|
||||
}
|
||||
|
||||
if (_isDragging) {
|
||||
_trySavePosition();
|
||||
}
|
||||
isSpecialHoldDragActive = false;
|
||||
},
|
||||
onPointerCancel: (event) {
|
||||
_cursorModel.blockEvents = false;
|
||||
setState(() {
|
||||
_isDown = false;
|
||||
});
|
||||
_tapDownTimer?.cancel();
|
||||
_tapDownTimer = null;
|
||||
if (isSpecialHoldDragActive) {
|
||||
_inputModel.tapUp(_isLeft ? MouseButtons.left : MouseButtons.right);
|
||||
}
|
||||
isSpecialHoldDragActive = false;
|
||||
if (_isDragging) {
|
||||
_trySavePosition();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: _kLeftRightButtonWidth,
|
||||
height: _kLeftRightButtonHeight,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _kDefaultColor,
|
||||
border: Border.all(
|
||||
color: _isDown ? _kTapDownColor : _kDefaultBorderColor,
|
||||
width: _kBorderWidth),
|
||||
borderRadius: _isLeft
|
||||
? BorderRadius.horizontal(
|
||||
left: Radius.circular(_kLeftRightButtonHeight * 0.5))
|
||||
: BorderRadius.horizontal(
|
||||
right: Radius.circular(_kLeftRightButtonHeight * 0.5)),
|
||||
),
|
||||
child: _buildButtonIcon(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuarterCirclePainter extends CustomPainter {
|
||||
final Color color;
|
||||
final bool isLeft;
|
||||
final double radius;
|
||||
_QuarterCirclePainter(
|
||||
{required this.color, required this.isLeft, required this.radius});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
final rect = Rect.fromLTWH(0, 0, radius * 2, radius * 2);
|
||||
if (isLeft) {
|
||||
canvas.drawArc(rect, -pi, pi / 2, true, paint);
|
||||
} else {
|
||||
canvas.drawArc(rect, -pi / 2, pi / 2, true, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
// Virtual joystick sends the absolute movement for now.
|
||||
// Maybe we need to change it to relative movement in the future.
|
||||
class VirtualJoystick extends StatefulWidget {
|
||||
final CursorModel cursorModel;
|
||||
|
||||
const VirtualJoystick({super.key, required this.cursorModel});
|
||||
|
||||
@override
|
||||
State<VirtualJoystick> createState() => _VirtualJoystickState();
|
||||
}
|
||||
|
||||
class _VirtualJoystickState extends State<VirtualJoystick> {
|
||||
Offset _position = Offset.zero;
|
||||
bool _isInitialized = false;
|
||||
Offset _offset = Offset.zero;
|
||||
final double _joystickRadius = 50.0;
|
||||
final double _thumbRadius = 20.0;
|
||||
final double _moveStep = 3.0;
|
||||
final double _speed = 1.0;
|
||||
|
||||
// One-shot timer to detect a drag gesture
|
||||
Timer? _dragStartTimer;
|
||||
// Periodic timer for continuous movement
|
||||
Timer? _continuousMoveTimer;
|
||||
Size? _lastScreenSize;
|
||||
bool _isPressed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.cursorModel.blockEvents = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_lastScreenSize = MediaQuery.of(context).size;
|
||||
_resetPosition();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopSendEventTimer();
|
||||
widget.cursorModel.blockEvents = false;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final currentScreenSize = MediaQuery.of(context).size;
|
||||
if (_lastScreenSize != null && _lastScreenSize != currentScreenSize) {
|
||||
_resetPosition();
|
||||
}
|
||||
_lastScreenSize = currentScreenSize;
|
||||
}
|
||||
|
||||
void _resetPosition() {
|
||||
final size = MediaQuery.of(context).size;
|
||||
setState(() {
|
||||
_position = Offset(
|
||||
_kSpaceToHorizontalEdge + _joystickRadius,
|
||||
size.height * 0.5 + _joystickRadius * 1.5,
|
||||
);
|
||||
_isInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
Offset _offsetToPanDelta(Offset offset) {
|
||||
return Offset(
|
||||
offset.dx / _joystickRadius,
|
||||
offset.dy / _joystickRadius,
|
||||
);
|
||||
}
|
||||
|
||||
void _stopSendEventTimer() {
|
||||
_dragStartTimer?.cancel();
|
||||
_continuousMoveTimer?.cancel();
|
||||
_dragStartTimer = null;
|
||||
_continuousMoveTimer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isInitialized) {
|
||||
return Positioned(child: Offstage());
|
||||
}
|
||||
return Positioned(
|
||||
left: _position.dx - _joystickRadius,
|
||||
top: _position.dy - _joystickRadius,
|
||||
child: GestureDetector(
|
||||
onPanStart: (details) {
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
widget.cursorModel.blockEvents = true;
|
||||
_updateOffset(details.localPosition);
|
||||
|
||||
// 1. Send a single, small pan event immediately for responsiveness.
|
||||
// The movement is small for a gentle start.
|
||||
final initialDelta = _offsetToPanDelta(_offset);
|
||||
if (initialDelta.distance > 0) {
|
||||
widget.cursorModel.updatePan(initialDelta, Offset.zero, false);
|
||||
}
|
||||
|
||||
// 2. Start a one-shot timer to check if the user is holding for a drag.
|
||||
_dragStartTimer?.cancel();
|
||||
_dragStartTimer = Timer(const Duration(milliseconds: 120), () {
|
||||
// 3. If the timer fires, it's a drag. Start the continuous movement timer.
|
||||
_continuousMoveTimer?.cancel();
|
||||
_continuousMoveTimer =
|
||||
periodic_immediate(const Duration(milliseconds: 20), () async {
|
||||
if (_offset != Offset.zero) {
|
||||
widget.cursorModel.updatePan(
|
||||
_offsetToPanDelta(_offset) * _moveStep * _speed,
|
||||
Offset.zero,
|
||||
false);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
onPanUpdate: (details) {
|
||||
_updateOffset(details.localPosition);
|
||||
},
|
||||
onPanEnd: (details) {
|
||||
setState(() {
|
||||
_offset = Offset.zero;
|
||||
_isPressed = false;
|
||||
});
|
||||
widget.cursorModel.blockEvents = false;
|
||||
|
||||
// 4. Critical step: On pan end, cancel all timers.
|
||||
// If it was a flick, this cancels the drag detection before it fires.
|
||||
// If it was a drag, this stops the continuous movement.
|
||||
_stopSendEventTimer();
|
||||
},
|
||||
child: CustomPaint(
|
||||
size: Size(_joystickRadius * 2, _joystickRadius * 2),
|
||||
painter: _JoystickPainter(
|
||||
_offset, _joystickRadius, _thumbRadius, _isPressed),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateOffset(Offset localPosition) {
|
||||
final center = Offset(_joystickRadius, _joystickRadius);
|
||||
final offset = localPosition - center;
|
||||
final distance = offset.distance;
|
||||
|
||||
if (distance <= _joystickRadius) {
|
||||
setState(() {
|
||||
_offset = offset;
|
||||
});
|
||||
} else {
|
||||
final clampedOffset = offset / distance * _joystickRadius;
|
||||
setState(() {
|
||||
_offset = clampedOffset;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _JoystickPainter extends CustomPainter {
|
||||
final Offset _offset;
|
||||
final double _joystickRadius;
|
||||
final double _thumbRadius;
|
||||
final bool _isPressed;
|
||||
|
||||
_JoystickPainter(
|
||||
this._offset, this._joystickRadius, this._thumbRadius, this._isPressed);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final joystickColor = _kDefaultColor;
|
||||
final borderColor = _isPressed ? _kTapDownColor : _kDefaultBorderColor;
|
||||
final thumbColor = _kWidgetHighlightColor;
|
||||
|
||||
final joystickPaint = Paint()
|
||||
..color = joystickColor
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final borderPaint = Paint()
|
||||
..color = borderColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
|
||||
final thumbPaint = Paint()
|
||||
..color = thumbColor
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Draw joystick base and border
|
||||
canvas.drawCircle(center, _joystickRadius, joystickPaint);
|
||||
canvas.drawCircle(center, _joystickRadius, borderPaint);
|
||||
|
||||
// Draw thumb
|
||||
final thumbCenter = center + _offset;
|
||||
canvas.drawCircle(thumbCenter, _thumbRadius, thumbPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _JoystickPainter oldDelegate) {
|
||||
return oldDelegate._offset != _offset ||
|
||||
oldDelegate._isPressed != _isPressed;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
class GestureIcons {
|
||||
@@ -35,20 +36,27 @@ typedef OnTouchModeChange = void Function(bool);
|
||||
|
||||
class GestureHelp extends StatefulWidget {
|
||||
GestureHelp(
|
||||
{Key? key, required this.touchMode, required this.onTouchModeChange})
|
||||
{Key? key,
|
||||
required this.touchMode,
|
||||
required this.onTouchModeChange,
|
||||
required this.virtualMouseMode})
|
||||
: super(key: key);
|
||||
final bool touchMode;
|
||||
final OnTouchModeChange onTouchModeChange;
|
||||
final VirtualMouseMode virtualMouseMode;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
|
||||
State<StatefulWidget> createState() =>
|
||||
_GestureHelpState(touchMode, virtualMouseMode);
|
||||
}
|
||||
|
||||
class _GestureHelpState extends State<GestureHelp> {
|
||||
late int _selectedIndex;
|
||||
late bool _touchMode;
|
||||
final VirtualMouseMode _virtualMouseMode;
|
||||
|
||||
_GestureHelpState(bool touchMode) {
|
||||
_GestureHelpState(bool touchMode, VirtualMouseMode virtualMouseMode)
|
||||
: _virtualMouseMode = virtualMouseMode {
|
||||
_touchMode = touchMode;
|
||||
_selectedIndex = _touchMode ? 1 : 0;
|
||||
}
|
||||
@@ -68,31 +76,144 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
ToggleSwitch(
|
||||
initialLabelIndex: _selectedIndex,
|
||||
activeFgColor: Colors.white,
|
||||
inactiveFgColor: Colors.white60,
|
||||
activeBgColor: [MyTheme.accent],
|
||||
inactiveBgColor: Theme.of(context).hintColor,
|
||||
totalSwitches: 2,
|
||||
minWidth: 150,
|
||||
fontSize: 15,
|
||||
iconSize: 18,
|
||||
labels: [translate("Mouse mode"), translate("Touch mode")],
|
||||
icons: [Icons.mouse, Icons.touch_app],
|
||||
onToggle: (index) {
|
||||
setState(() {
|
||||
if (_selectedIndex != index) {
|
||||
_selectedIndex = index ?? 0;
|
||||
_touchMode = index == 0 ? false : true;
|
||||
widget.onTouchModeChange(_touchMode);
|
||||
}
|
||||
});
|
||||
},
|
||||
Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ToggleSwitch(
|
||||
initialLabelIndex: _selectedIndex,
|
||||
activeFgColor: Colors.white,
|
||||
inactiveFgColor: Colors.white60,
|
||||
activeBgColor: [MyTheme.accent],
|
||||
inactiveBgColor: Theme.of(context).hintColor,
|
||||
totalSwitches: 2,
|
||||
minWidth: 150,
|
||||
fontSize: 15,
|
||||
iconSize: 18,
|
||||
labels: [
|
||||
translate("Mouse mode"),
|
||||
translate("Touch mode")
|
||||
],
|
||||
icons: [Icons.mouse, Icons.touch_app],
|
||||
onToggle: (index) {
|
||||
setState(() {
|
||||
if (_selectedIndex != index) {
|
||||
_selectedIndex = index ?? 0;
|
||||
_touchMode = index == 0 ? false : true;
|
||||
widget.onTouchModeChange(_touchMode);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(-10.0, 0.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _virtualMouseMode.showVirtualMouse,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await _virtualMouseMode.toggleVirtualMouse();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
await _virtualMouseMode.toggleVirtualMouse();
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(translate('Show virtual mouse')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_touchMode && _virtualMouseMode.showVirtualMouse)
|
||||
Padding(
|
||||
// Indent "Virtual mouse size"
|
||||
padding: const EdgeInsets.only(left: 24.0),
|
||||
child: SizedBox(
|
||||
width: 260,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0.0, bottom: 0),
|
||||
child: Text(translate('Virtual mouse size')),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(-0.0, -6.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 0.0),
|
||||
child: Text(translate('Small')),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _virtualMouseMode
|
||||
.virtualMouseScale,
|
||||
min: 0.8,
|
||||
max: 1.8,
|
||||
divisions: 10,
|
||||
onChanged: (value) {
|
||||
_virtualMouseMode
|
||||
.setVirtualMouseScale(value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(right: 16.0),
|
||||
child: Text(translate('Large')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_touchMode && _virtualMouseMode.showVirtualMouse)
|
||||
Transform.translate(
|
||||
offset: const Offset(-10.0, -12.0),
|
||||
child: Padding(
|
||||
// Indent "Show virtual joystick"
|
||||
padding: const EdgeInsets.only(left: 24.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(
|
||||
value:
|
||||
_virtualMouseMode.showVirtualJoystick,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await _virtualMouseMode
|
||||
.toggleVirtualJoystick();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
await _virtualMouseMode
|
||||
.toggleVirtualJoystick();
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
translate("Show virtual joystick")),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
child: Wrap(
|
||||
spacing: space,
|
||||
|
||||
@@ -319,8 +319,8 @@ class AbModel {
|
||||
// #endregion
|
||||
|
||||
// #region peer
|
||||
Future<String?> addIdToCurrent(
|
||||
String id, String alias, String password, List<dynamic> tags) async {
|
||||
Future<String?> addIdToCurrent(String id, String alias, String password,
|
||||
List<dynamic> tags, String note) async {
|
||||
if (currentAbPeers.where((element) => element.id == id).isNotEmpty) {
|
||||
return "$id already exists in address book $_currentName";
|
||||
}
|
||||
@@ -333,6 +333,9 @@ class AbModel {
|
||||
if (password.isNotEmpty) {
|
||||
peer['password'] = password;
|
||||
}
|
||||
if (note.isNotEmpty) {
|
||||
peer['note'] = note;
|
||||
}
|
||||
final ret = await addPeersTo([peer], _currentName.value);
|
||||
_syncAllFromRecent = true;
|
||||
return ret;
|
||||
@@ -376,6 +379,14 @@ class AbModel {
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool> changeNote({required String id, required String note}) async {
|
||||
bool res = await current.changeNote(id: id, note: note);
|
||||
await pullNonLegacyAfterChange();
|
||||
currentAbPeers.refresh();
|
||||
// no need to save cache
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool> changePersonalHashPassword(String id, String hash) async {
|
||||
var ret = false;
|
||||
final personalAb = addressbooks[_personalAddressBookName];
|
||||
@@ -658,6 +669,15 @@ class AbModel {
|
||||
}
|
||||
}
|
||||
|
||||
String getPeerNote(String id) {
|
||||
final it = currentAbPeers.where((p0) => p0.id == id);
|
||||
if (it.isEmpty) {
|
||||
return '';
|
||||
} else {
|
||||
return it.first.note;
|
||||
}
|
||||
}
|
||||
|
||||
Color getCurrentAbTagColor(String tag) {
|
||||
if (tag == kUntagged) {
|
||||
return MyTheme.accent;
|
||||
@@ -863,6 +883,8 @@ abstract class BaseAb {
|
||||
|
||||
Future<bool> changeAlias({required String id, required String alias});
|
||||
|
||||
Future<bool> changeNote({required String id, required String note});
|
||||
|
||||
Future<bool> changePersonalHashPassword(String id, String hash);
|
||||
|
||||
Future<bool> changeSharedPassword(String id, String password);
|
||||
@@ -1090,6 +1112,12 @@ class LegacyAb extends BaseAb {
|
||||
return await pushAb();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> changeNote({required String id, required String note}) async {
|
||||
// no need to implement
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> changeSharedPassword(String id, String password) async {
|
||||
// no need to implement
|
||||
@@ -1549,6 +1577,27 @@ class Ab extends BaseAb {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> changeNote({required String id, required String note}) async {
|
||||
try {
|
||||
final api =
|
||||
"${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}";
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
final body = jsonEncode({"id": id, "note": note});
|
||||
final resp = await http.put(Uri.parse(api), headers: headers, body: body);
|
||||
final errMsg = _jsonDecodeActionResp(resp);
|
||||
if (errMsg.isNotEmpty) {
|
||||
BotToast.showText(contentColor: Colors.red, text: errMsg);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
debugPrint('changeNote err: ${err.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _setPassword(Object bodyContent) async {
|
||||
try {
|
||||
final api =
|
||||
@@ -1815,6 +1864,11 @@ class DummyAb extends BaseAb {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> changeNote({required String id, required String note}) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> changePersonalHashPassword(String id, String hash) async {
|
||||
return false;
|
||||
|
||||
@@ -1033,30 +1033,54 @@ class JobController {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
|
||||
void loadLastJob(Map<String, dynamic> evt) {
|
||||
Future<void> loadLastJob(Map<String, dynamic> evt) async {
|
||||
debugPrint("load last job: $evt");
|
||||
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
||||
// int id = int.parse(jobDetail['id']);
|
||||
String remote = jobDetail['remote'];
|
||||
String to = jobDetail['to'];
|
||||
bool showHidden = jobDetail['show_hidden'];
|
||||
int fileNum = jobDetail['file_num'];
|
||||
bool isRemote = jobDetail['is_remote'];
|
||||
final currJobId = JobController.jobID.next();
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
var jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
..isRemoteToLocal = isRemote
|
||||
..fileNum = fileNum
|
||||
..remote = remote
|
||||
..to = to
|
||||
..showHidden = showHidden
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
bind.sessionAddJob(
|
||||
bool isAutoStart = jobDetail['auto_start'] == true;
|
||||
int currJobId = -1;
|
||||
if (isAutoStart) {
|
||||
// Ensure jobDetail['id'] exists and is an int
|
||||
if (jobDetail.containsKey('id') &&
|
||||
jobDetail['id'] != null &&
|
||||
jobDetail['id'] is int) {
|
||||
currJobId = jobDetail['id'];
|
||||
}
|
||||
}
|
||||
if (currJobId < 0) {
|
||||
// If id is missing or invalid, disable auto-start and assign a new job id
|
||||
isAutoStart = false;
|
||||
currJobId = JobController.jobID.next();
|
||||
}
|
||||
|
||||
if (!isAutoStart) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
// Don't add to job table if not auto start on mobile.
|
||||
// Because mobile does not support job list view now.
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to job table if not auto start on desktop.
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
final jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
..isRemoteToLocal = isRemote
|
||||
..fileNum = fileNum
|
||||
..remote = remote
|
||||
..to = to
|
||||
..showHidden = showHidden
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
}
|
||||
|
||||
await bind.sessionAddJob(
|
||||
sessionId: sessionId,
|
||||
isRemote: isRemote,
|
||||
includeHidden: showHidden,
|
||||
@@ -1065,6 +1089,11 @@ class JobController {
|
||||
to: isRemote ? to : remote,
|
||||
fileNum: fileNum,
|
||||
);
|
||||
|
||||
if (isAutoStart) {
|
||||
await bind.sessionResumeJob(
|
||||
sessionId: sessionId, actId: currJobId, isRemote: isRemote);
|
||||
}
|
||||
}
|
||||
|
||||
void resumeJob(int jobId) {
|
||||
@@ -1095,6 +1124,11 @@ class JobController {
|
||||
}
|
||||
debugPrint("update folder files: $info");
|
||||
}
|
||||
|
||||
void clear() {
|
||||
jobTable.clear();
|
||||
jobResultListener.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class JobResultListener<T> {
|
||||
|
||||
@@ -42,8 +42,7 @@ class CanvasCoords {
|
||||
'scale': scale,
|
||||
'scrollX': scrollX,
|
||||
'scrollY': scrollY,
|
||||
'scrollStyle':
|
||||
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
|
||||
'scrollStyle': scrollStyle.toJson(),
|
||||
'size': {
|
||||
'w': size.width,
|
||||
'h': size.height,
|
||||
@@ -58,9 +57,7 @@ class CanvasCoords {
|
||||
model.scale = json['scale'];
|
||||
model.scrollX = json['scrollX'];
|
||||
model.scrollY = json['scrollY'];
|
||||
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
|
||||
? ScrollStyle.scrollauto
|
||||
: ScrollStyle.scrollbar;
|
||||
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.size = Size(json['size']['w'], json['size']['h']);
|
||||
return model;
|
||||
}
|
||||
@@ -375,6 +372,7 @@ class InputModel {
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||
int get trackpadSpeed => _trackpadSpeed;
|
||||
bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
||||
|
||||
InputModel(this.parent) {
|
||||
sessionId = parent.target!.sessionId;
|
||||
@@ -766,6 +764,11 @@ class InputModel {
|
||||
command: command);
|
||||
}
|
||||
|
||||
static Map<String, dynamic> getMouseEventMove() => {
|
||||
'type': _kMouseEventMove,
|
||||
'buttons': 0,
|
||||
};
|
||||
|
||||
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
|
||||
final Map<String, dynamic> out = {};
|
||||
|
||||
@@ -883,7 +886,7 @@ class InputModel {
|
||||
isPhysicalMouse.value = true;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,7 +1074,7 @@ class InputModel {
|
||||
_queryOtherWindowCoords = false;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,7 +1123,7 @@ class InputModel {
|
||||
void refreshMousePos() => handleMouse({
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
}, lastMousePos);
|
||||
}, lastMousePos, edgeScroll: useEdgeScroll);
|
||||
|
||||
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
|
||||
{
|
||||
@@ -1222,16 +1225,18 @@ class InputModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
void handleMouse(
|
||||
Map<String, dynamic>? processEventToPeer(
|
||||
Map<String, dynamic> evt,
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
if (isViewCamera) return;
|
||||
if (isViewCamera) return null;
|
||||
double x = offset.dx;
|
||||
double y = max(0.0, offset.dy);
|
||||
if (_checkPeerControlProtected(x, y)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var type = kMouseEventTypeDefault;
|
||||
@@ -1248,7 +1253,7 @@ class InputModel {
|
||||
isMove = true;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
evt['type'] = type;
|
||||
|
||||
@@ -1266,9 +1271,11 @@ class InputModel {
|
||||
type,
|
||||
onExit: onExit,
|
||||
buttons: evt['buttons'],
|
||||
moveCanvas: moveCanvas,
|
||||
edgeScroll: edgeScroll,
|
||||
);
|
||||
if (pos == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (type != '') {
|
||||
evt['x'] = '0';
|
||||
@@ -1286,7 +1293,23 @@ class InputModel {
|
||||
kForwardMouseButton: 'forward'
|
||||
};
|
||||
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
|
||||
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
|
||||
return evt;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? handleMouse(
|
||||
Map<String, dynamic> evt,
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final evtToPeer =
|
||||
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
||||
if (evtToPeer != null) {
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
|
||||
}
|
||||
return evtToPeer;
|
||||
}
|
||||
|
||||
Point? handlePointerDevicePos(
|
||||
@@ -1297,6 +1320,8 @@ class InputModel {
|
||||
String evtType, {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final ffiModel = parent.target!.ffiModel;
|
||||
CanvasCoords canvas =
|
||||
@@ -1326,7 +1351,15 @@ class InputModel {
|
||||
y -= CanvasModel.topToEdge;
|
||||
x -= CanvasModel.leftToEdge;
|
||||
if (isMove) {
|
||||
parent.target!.canvasModel.moveDesktopMouse(x, y);
|
||||
final canvasModel = parent.target!.canvasModel;
|
||||
|
||||
if (edgeScroll) {
|
||||
canvasModel.edgeScrollMouse(x, y);
|
||||
} else if (moveCanvas) {
|
||||
canvasModel.moveDesktopMouse(x, y);
|
||||
}
|
||||
|
||||
canvasModel.updateLocalCursor(x, y);
|
||||
}
|
||||
|
||||
return _handlePointerDevicePos(
|
||||
@@ -1389,7 +1422,7 @@ class InputModel {
|
||||
var nearBottom = (canvas.size.height - y) < nearThr;
|
||||
final imageWidth = rect.width * canvas.scale;
|
||||
final imageHeight = rect.height * canvas.scale;
|
||||
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (canvas.scrollStyle != ScrollStyle.scrollauto) {
|
||||
x += imageWidth * canvas.scrollX;
|
||||
y += imageHeight * canvas.scrollY;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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/scheduler.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
@@ -29,6 +30,7 @@ import 'package:flutter_hbb/plugin/manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:image/image.dart' as img2;
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -36,12 +38,14 @@ import 'package:get/get.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:vector_math/vector_math.dart' show Vector2;
|
||||
|
||||
import '../common.dart';
|
||||
import '../utils/image.dart' as img;
|
||||
import '../common/widgets/dialog.dart';
|
||||
import 'input_model.dart';
|
||||
import 'platform_model.dart';
|
||||
import 'package:flutter_hbb/utils/scale.dart';
|
||||
|
||||
import 'package:flutter_hbb/generated_bridge.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/bridge.dart';
|
||||
@@ -113,6 +117,7 @@ class FfiModel with ChangeNotifier {
|
||||
bool? _secure;
|
||||
bool? _direct;
|
||||
bool _touchMode = false;
|
||||
late VirtualMouseMode virtualMouseMode;
|
||||
Timer? _timer;
|
||||
var _reconnects = 1;
|
||||
bool _viewOnly = false;
|
||||
@@ -154,6 +159,8 @@ class FfiModel with ChangeNotifier {
|
||||
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
|
||||
bool get isPeerMobile => isPeerAndroid;
|
||||
|
||||
bool get isPeerLinux => _pi.platform == kPeerPlatformLinux;
|
||||
|
||||
bool get viewOnly => _viewOnly;
|
||||
bool get showMyCursor => _showMyCursor;
|
||||
|
||||
@@ -165,6 +172,7 @@ class FfiModel with ChangeNotifier {
|
||||
clear();
|
||||
sessionId = parent.target!.sessionId;
|
||||
cachedPeerData.permissions = _permissions;
|
||||
virtualMouseMode = VirtualMouseMode(this);
|
||||
}
|
||||
|
||||
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
|
||||
@@ -173,6 +181,9 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (isPeerLinux) {
|
||||
useDisplayScale = true;
|
||||
}
|
||||
int scale(int len, double s) {
|
||||
if (useDisplayScale) {
|
||||
return len.toDouble() ~/ s;
|
||||
@@ -928,11 +939,21 @@ class FfiModel with ChangeNotifier {
|
||||
/// Show a message box with [type], [title] and [text].
|
||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) {
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
{bool? hasCancel}) async {
|
||||
final showNoteEdit = parent.target != null &&
|
||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||
(title == "Connection Error" || type == "restarting") &&
|
||||
!hasRetry;
|
||||
if (showNoteEdit) {
|
||||
await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: parent.target!, type: type, title: title, text: text);
|
||||
closeConnection();
|
||||
} else {
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
}
|
||||
_timer?.cancel();
|
||||
if (hasRetry) {
|
||||
_timer = Timer(Duration(seconds: _reconnects), () {
|
||||
@@ -953,8 +974,30 @@ class FfiModel with ChangeNotifier {
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
|
||||
void showRelayHintDialog(SessionID sessionId, String type, String title,
|
||||
String text, OverlayDialogManager dialogManager, String peerId) {
|
||||
Future<void> showRelayHintDialog(
|
||||
SessionID sessionId,
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
OverlayDialogManager dialogManager,
|
||||
String peerId) async {
|
||||
var hint = "\n\n${translate('relay_hint_tip')}";
|
||||
if (text.contains("10054") || text.contains("104")) {
|
||||
hint = "";
|
||||
}
|
||||
final text2 = "${translate(text)}$hint";
|
||||
|
||||
if (parent.target != null &&
|
||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||
pi.isSet.isTrue) {
|
||||
if (await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: parent.target!, type: type, title: title, text: text2)) {
|
||||
return;
|
||||
}
|
||||
closeConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
|
||||
onClose() {
|
||||
closeConnection();
|
||||
@@ -963,13 +1006,10 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
final style =
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
|
||||
var hint = "\n\n${translate('relay_hint_tip')}";
|
||||
if (text.contains("10054") || text.contains("104")) {
|
||||
hint = "";
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, "${translate(text)}$hint"),
|
||||
content: msgboxContent(type, title, text2),
|
||||
actions: [
|
||||
dialogButton('Close', onPressed: onClose, isOutline: true),
|
||||
if (type == 'relay-hint')
|
||||
@@ -1041,28 +1081,108 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.length == 1) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display:
|
||||
pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: _rect!.width.toInt(),
|
||||
height: _rect!.height.toInt(),
|
||||
display: pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: displays[0].width,
|
||||
height: displays[0].height,
|
||||
);
|
||||
} else {
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display: i,
|
||||
width: displays[i].width.toInt(),
|
||||
height: displays[i].height.toInt(),
|
||||
width: displays[i].width,
|
||||
height: displays[i].height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _queryAuditGuid(String peerId) async {
|
||||
try {
|
||||
if (!mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection)) {
|
||||
return;
|
||||
}
|
||||
if (bind.sessionGetAuditGuid(sessionId: sessionId).isNotEmpty) {
|
||||
debugPrint('Get cached audit GUID');
|
||||
return;
|
||||
}
|
||||
final url = bind.sessionGetAuditServerSync(
|
||||
sessionId: sessionId, typ: "conn/active");
|
||||
if (url.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final initialConnSessionId =
|
||||
bind.sessionGetConnSessionId(sessionId: sessionId);
|
||||
final connType = switch (parent.target?.connType) {
|
||||
ConnType.defaultConn => 0,
|
||||
ConnType.fileTransfer => 1,
|
||||
ConnType.portForward => 2,
|
||||
ConnType.rdp => 2,
|
||||
ConnType.viewCamera => 3,
|
||||
ConnType.terminal => 4,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
const retryIntervals = [1, 1, 2, 2, 3, 3];
|
||||
|
||||
for (int attempt = 1; attempt <= retryIntervals.length; attempt++) {
|
||||
final currentConnSessionId =
|
||||
bind.sessionGetConnSessionId(sessionId: sessionId);
|
||||
if (currentConnSessionId != initialConnSessionId) {
|
||||
debugPrint('connSessionId changed, stopping audit GUID query');
|
||||
return;
|
||||
}
|
||||
|
||||
final fullUrl =
|
||||
'$url?id=$peerId&session_id=$currentConnSessionId&conn_type=$connType';
|
||||
|
||||
debugPrint(
|
||||
'Querying audit GUID, attempt $attempt/${retryIntervals.length}');
|
||||
try {
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse(fullUrl),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final guid = jsonDecode(response.body) as String?;
|
||||
if (guid != null && guid.isNotEmpty) {
|
||||
bind.sessionSetAuditGuid(sessionId: sessionId, guid: guid);
|
||||
debugPrint('Successfully retrieved audit GUID');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'Failed to query audit GUID. Status: ${response.statusCode}, Body: ${response.body}');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error querying audit GUID (attempt $attempt): $e');
|
||||
}
|
||||
|
||||
if (attempt < retryIntervals.length) {
|
||||
await Future.delayed(Duration(seconds: retryIntervals[attempt - 1]));
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Failed to retrieve audit GUID after ${retryIntervals.length} attempts');
|
||||
} catch (e) {
|
||||
debugPrint('Error in _queryAuditGuid: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
|
||||
_queryAuditGuid(peerId);
|
||||
|
||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
|
||||
@@ -1104,9 +1224,23 @@ class FfiModel with ChangeNotifier {
|
||||
if (isPeerAndroid) {
|
||||
_touchMode = true;
|
||||
} else {
|
||||
_touchMode = await bind.sessionGetOption(
|
||||
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||
'';
|
||||
// `kOptionTouchMode` is originally peer option, but it is moved to local option later.
|
||||
// We check local option first, if not set, then check peer option.
|
||||
// Because if local option is not empty:
|
||||
// 1. User has set the touch mode explicitly.
|
||||
// 2. The advanced option (custom client) is set.
|
||||
// Then we choose to use the local option.
|
||||
final optLocal = bind.mainGetLocalOption(key: kOptionTouchMode);
|
||||
if (optLocal != '') {
|
||||
_touchMode = optLocal == 'Y';
|
||||
} else {
|
||||
final optSession = await bind.sessionGetOption(
|
||||
sessionId: sessionId, arg: kOptionTouchMode);
|
||||
_touchMode = optSession != '';
|
||||
}
|
||||
}
|
||||
if (isMobile) {
|
||||
virtualMouseMode.loadOptions();
|
||||
}
|
||||
if (connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
@@ -1306,8 +1440,17 @@ class FfiModel with ChangeNotifier {
|
||||
d.cursorEmbedded = evt['cursor_embedded'] == 1;
|
||||
d.originalWidth = evt['original_width'] ?? kInvalidResolutionValue;
|
||||
d.originalHeight = evt['original_height'] ?? kInvalidResolutionValue;
|
||||
double v = (evt['scale']?.toDouble() ?? 100.0) / 100;
|
||||
d._scale = v > 1.0 ? v : 1.0;
|
||||
d._scale = 1.0;
|
||||
final scaledWidth = evt['scaled_width'];
|
||||
if (scaledWidth != null) {
|
||||
final sw = int.tryParse(scaledWidth.toString());
|
||||
if (sw != null && sw > 0 && d.width > 0) {
|
||||
d._scale = max(d.width.toDouble() / sw, 1.0);
|
||||
} else {
|
||||
debugPrint(
|
||||
"Invalid scaled_width ($scaledWidth) or width (${d.width}), using default scale 1.0");
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1507,6 +1650,72 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class VirtualMouseMode with ChangeNotifier {
|
||||
bool _showVirtualMouse = false;
|
||||
double _virtualMouseScale = 1.0;
|
||||
bool _showVirtualJoystick = false;
|
||||
|
||||
bool get showVirtualMouse => _showVirtualMouse;
|
||||
double get virtualMouseScale => _virtualMouseScale;
|
||||
bool get showVirtualJoystick => _showVirtualJoystick;
|
||||
|
||||
FfiModel ffiModel;
|
||||
|
||||
VirtualMouseMode(this.ffiModel);
|
||||
|
||||
bool _shouldShow() => !ffiModel.isPeerAndroid;
|
||||
|
||||
setShowVirtualMouse(bool b) {
|
||||
if (b == _showVirtualMouse) return;
|
||||
if (_shouldShow()) {
|
||||
_showVirtualMouse = b;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
setVirtualMouseScale(double s) {
|
||||
if (s <= 0) return;
|
||||
if (s == _virtualMouseScale) return;
|
||||
_virtualMouseScale = s;
|
||||
bind.mainSetLocalOption(key: kOptionVirtualMouseScale, value: s.toString());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setShowVirtualJoystick(bool b) {
|
||||
if (b == _showVirtualJoystick) return;
|
||||
if (_shouldShow()) {
|
||||
_showVirtualJoystick = b;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void loadOptions() {
|
||||
_showVirtualMouse =
|
||||
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y';
|
||||
_virtualMouseScale = double.tryParse(
|
||||
bind.mainGetLocalOption(key: kOptionVirtualMouseScale)) ??
|
||||
1.0;
|
||||
_showVirtualJoystick =
|
||||
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleVirtualMouse() async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionShowVirtualMouse, value: showVirtualMouse ? 'N' : 'Y');
|
||||
setShowVirtualMouse(
|
||||
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y');
|
||||
}
|
||||
|
||||
Future<void> toggleVirtualJoystick() async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionShowVirtualJoystick,
|
||||
value: showVirtualJoystick ? 'N' : 'Y');
|
||||
setShowVirtualJoystick(
|
||||
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y');
|
||||
}
|
||||
}
|
||||
|
||||
class ImageModel with ChangeNotifier {
|
||||
ui.Image? _image;
|
||||
|
||||
@@ -1582,6 +1791,7 @@ class ImageModel with ChangeNotifier {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
await parent.target?.canvasModel.updateViewStyle();
|
||||
await parent.target?.canvasModel.updateScrollStyle();
|
||||
await parent.target?.canvasModel.initializeEdgeScrollEdgeThickness();
|
||||
}
|
||||
if (parent.target != null) {
|
||||
await initializeCursorAndCanvas(parent.target!);
|
||||
@@ -1630,8 +1840,56 @@ class ImageModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
enum ScrollStyle {
|
||||
scrollbar,
|
||||
scrollauto,
|
||||
scrollbar(kRemoteScrollStyleBar),
|
||||
scrollauto(kRemoteScrollStyleAuto),
|
||||
scrolledge(kRemoteScrollStyleEdge);
|
||||
|
||||
const ScrollStyle(this.stringValue);
|
||||
|
||||
final String stringValue;
|
||||
|
||||
String toJson() {
|
||||
return name;
|
||||
}
|
||||
|
||||
static ScrollStyle fromJson(String json, [ScrollStyle? fallbackValue]) {
|
||||
switch (json) {
|
||||
case 'scrollbar':
|
||||
return scrollbar;
|
||||
case 'scrollauto':
|
||||
return scrollauto;
|
||||
case 'scrolledge':
|
||||
return scrolledge;
|
||||
}
|
||||
|
||||
if (fallbackValue != null) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
throw ArgumentError("Unknown ScrollStyle JSON value: '$json'");
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
static ScrollStyle fromString(String string, [ScrollStyle? fallbackValue]) {
|
||||
switch (string) {
|
||||
case kRemoteScrollStyleBar:
|
||||
return scrollbar;
|
||||
case kRemoteScrollStyleAuto:
|
||||
return scrollauto;
|
||||
case kRemoteScrollStyleEdge:
|
||||
return scrolledge;
|
||||
}
|
||||
|
||||
if (fallbackValue != null) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
throw ArgumentError("Unknown ScrollStyle string value: '$string'");
|
||||
}
|
||||
}
|
||||
|
||||
class ViewStyle {
|
||||
@@ -1699,11 +1957,67 @@ class ViewStyle {
|
||||
final s2 = height / displayHeight;
|
||||
s = s1 < s2 ? s1 : s2;
|
||||
}
|
||||
} else if (style == kRemoteViewStyleCustom) {
|
||||
// Custom scale is session-scoped and applied in CanvasModel.updateViewStyle()
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
enum EdgeScrollState {
|
||||
inactive,
|
||||
armed,
|
||||
active,
|
||||
}
|
||||
|
||||
class EdgeScrollFallbackState {
|
||||
final CanvasModel _owner;
|
||||
|
||||
late Ticker _ticker;
|
||||
|
||||
Duration _lastTotalElapsed = Duration.zero;
|
||||
bool _nextEventIsFirst = true;
|
||||
Vector2 _encroachment = Vector2.zero();
|
||||
|
||||
EdgeScrollFallbackState(this._owner, TickerProvider tickerProvider) {
|
||||
_ticker = tickerProvider.createTicker(emitTick);
|
||||
}
|
||||
|
||||
void setEncroachment(Vector2 encroachment) {
|
||||
_encroachment = encroachment;
|
||||
}
|
||||
|
||||
void emitTick(Duration totalElapsed) {
|
||||
if (_nextEventIsFirst) {
|
||||
_lastTotalElapsed = totalElapsed;
|
||||
_nextEventIsFirst = false;
|
||||
} else {
|
||||
final thisTickElapsed = totalElapsed - _lastTotalElapsed;
|
||||
|
||||
const double kFrameTime = 1000.0 / 60.0;
|
||||
const double kSpeedFactor = 0.1;
|
||||
|
||||
var delta = _encroachment *
|
||||
(kSpeedFactor * thisTickElapsed.inMilliseconds / kFrameTime);
|
||||
|
||||
_owner.performEdgeScroll(delta);
|
||||
|
||||
_lastTotalElapsed = totalElapsed;
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (!_ticker.isActive) {
|
||||
_nextEventIsFirst = true;
|
||||
_ticker.start();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_ticker.stop();
|
||||
}
|
||||
}
|
||||
|
||||
class CanvasModel with ChangeNotifier {
|
||||
// image offset of canvas
|
||||
double _x = 0;
|
||||
@@ -1725,6 +2039,15 @@ class CanvasModel with ChangeNotifier {
|
||||
// scroll offset y percent
|
||||
double _scrollY = 0.0;
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
// edge scroll mode: trigger scrolling when the cursor is close to the edge of the view
|
||||
int _edgeScrollEdgeThickness = 100;
|
||||
// tracks whether edge scroll should be active, prevents spurious
|
||||
// scrolling when the cursor enters the view from outside
|
||||
EdgeScrollState _edgeScrollState = EdgeScrollState.inactive;
|
||||
// fallback strategy for when Bump Mouse isn't available
|
||||
late EdgeScrollFallbackState _edgeScrollFallbackState;
|
||||
// to avoid hammering a non-functional Bump Mouse
|
||||
bool _bumpMouseIsWorking = true;
|
||||
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
|
||||
Timer? _timerMobileFocusCanvasCursor;
|
||||
@@ -1755,9 +2078,18 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
_resetScroll() => setScrollPercent(0.0, 0.0);
|
||||
|
||||
setScrollPercent(double x, double y) {
|
||||
_scrollX = x;
|
||||
_scrollY = y;
|
||||
void setScrollPercent(double x, double y) {
|
||||
_scrollX = x.isFinite ? x : 0.0;
|
||||
_scrollY = y.isFinite ? y : 0.0;
|
||||
}
|
||||
|
||||
void pushScrollPositionToUI(double scrollPixelX, double scrollPixelY) {
|
||||
if (_horizontal.hasClients) {
|
||||
_horizontal.jumpTo(scrollPixelX);
|
||||
}
|
||||
if (_vertical.hasClients) {
|
||||
_vertical.jumpTo(scrollPixelY);
|
||||
}
|
||||
}
|
||||
|
||||
ScrollController get scrollHorizontal => _horizontal;
|
||||
@@ -1815,7 +2147,13 @@ class CanvasModel with ChangeNotifier {
|
||||
displayWidth: displayWidth,
|
||||
displayHeight: displayHeight,
|
||||
);
|
||||
if (_lastViewStyle == viewStyle) {
|
||||
// If only the Custom scale percent changed, proceed to update even if
|
||||
// the basic ViewStyle fields are equal.
|
||||
// In Custom scale mode, the scale percent can change independently of the other
|
||||
// ViewStyle fields and is not captured by the equality check. Therefore, we must
|
||||
// allow updates to proceed when style == kRemoteViewStyleCustom, even if the
|
||||
// rest of the ViewStyle fields are unchanged.
|
||||
if (_lastViewStyle == viewStyle && style != kRemoteViewStyleCustom) {
|
||||
return;
|
||||
}
|
||||
if (_lastViewStyle.style != viewStyle.style) {
|
||||
@@ -1824,12 +2162,30 @@ class CanvasModel with ChangeNotifier {
|
||||
_lastViewStyle = viewStyle;
|
||||
_scale = viewStyle.scale;
|
||||
|
||||
// Apply custom scale percent when in Custom mode
|
||||
if (style == kRemoteViewStyleCustom) {
|
||||
try {
|
||||
_scale = await getSessionCustomScale(sessionId);
|
||||
} catch (e, stack) {
|
||||
debugPrint('Error in getSessionCustomScale: $e');
|
||||
debugPrintStack(stackTrace: stack);
|
||||
_scale = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
_devicePixelRatio = ui.window.devicePixelRatio;
|
||||
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / _devicePixelRatio;
|
||||
if (kIgnoreDpi) {
|
||||
if (style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / _devicePixelRatio;
|
||||
} else if (_scale != 0 && style == kRemoteViewStyleCustom) {
|
||||
_scale /= _devicePixelRatio;
|
||||
}
|
||||
}
|
||||
_resetCanvasOffset(displayWidth, displayHeight);
|
||||
_imageOverflow.value = _x < 0 || y < 0;
|
||||
final overflow = _x < 0 || y < 0;
|
||||
if (_imageOverflow.value != overflow) {
|
||||
_imageOverflow.value = overflow;
|
||||
}
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -1848,30 +2204,47 @@ class CanvasModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
tryUpdateScrollStyle(Duration duration, String? style) async {
|
||||
if (_scrollStyle != ScrollStyle.scrollbar) return;
|
||||
if (_scrollStyle == ScrollStyle.scrollauto) return;
|
||||
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||
if (style != kRemoteViewStyleOriginal) {
|
||||
if (style != kRemoteViewStyleOriginal && style != kRemoteViewStyleCustom) {
|
||||
return;
|
||||
}
|
||||
|
||||
_resetScroll();
|
||||
|
||||
Future.delayed(duration, () async {
|
||||
updateScrollPercent();
|
||||
});
|
||||
}
|
||||
|
||||
updateScrollStyle() async {
|
||||
Future<void> updateScrollStyle() async {
|
||||
final style = await bind.sessionGetScrollStyle(sessionId: sessionId);
|
||||
if (style == kRemoteScrollStyleBar) {
|
||||
_scrollStyle = ScrollStyle.scrollbar;
|
||||
|
||||
_scrollStyle =
|
||||
style != null ? ScrollStyle.fromString(style) : ScrollStyle.scrollauto;
|
||||
|
||||
if (_scrollStyle != ScrollStyle.scrollauto) {
|
||||
_resetScroll();
|
||||
} else {
|
||||
_scrollStyle = ScrollStyle.scrollauto;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
update(double x, double y, double scale) {
|
||||
Future<void> initializeEdgeScrollEdgeThickness() async {
|
||||
final savedValue =
|
||||
await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId);
|
||||
|
||||
if (savedValue != null) {
|
||||
_edgeScrollEdgeThickness = savedValue;
|
||||
}
|
||||
}
|
||||
|
||||
void updateEdgeScrollEdgeThickness(int newThickness) {
|
||||
_edgeScrollEdgeThickness = newThickness;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void update(double x, double y, double scale) {
|
||||
_x = x;
|
||||
_y = y;
|
||||
_scale = scale;
|
||||
@@ -1898,7 +2271,33 @@ class CanvasModel with ChangeNotifier {
|
||||
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||
static double get tabBarHeight => stateGlobal.tabBarHeight;
|
||||
|
||||
moveDesktopMouse(double x, double y) {
|
||||
void activateLocalCursor() {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = false;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateLocalCursor(double x, double y) {
|
||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||
// Draw cursor if is not desktop.
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
parent.target!.cursorModel.moveLocal(x, y);
|
||||
} else {
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = false;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void moveDesktopMouse(double x, double y) {
|
||||
if (size.width == 0 || size.height == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1927,24 +2326,128 @@ class CanvasModel with ChangeNotifier {
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||
// Draw cursor if is not desktop.
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
parent.target!.cursorModel.moveLocal(x, y);
|
||||
void initializeEdgeScrollFallback(TickerProvider tickerProvider) {
|
||||
_edgeScrollFallbackState = EdgeScrollFallbackState(this, tickerProvider);
|
||||
}
|
||||
|
||||
void disableEdgeScroll() {
|
||||
_edgeScrollState = EdgeScrollState.inactive;
|
||||
cancelEdgeScroll();
|
||||
}
|
||||
|
||||
void rearmEdgeScroll() {
|
||||
_edgeScrollState = EdgeScrollState.armed;
|
||||
}
|
||||
|
||||
void cancelEdgeScroll() {
|
||||
_edgeScrollFallbackState.stop();
|
||||
}
|
||||
|
||||
(Vector2, Vector2) getScrollInfo() {
|
||||
final scrollPixel = Vector2(
|
||||
_horizontal.hasClients ? _horizontal.position.pixels : 0,
|
||||
_vertical.hasClients ? _vertical.position.pixels : 0);
|
||||
|
||||
final max = Vector2(
|
||||
_horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0,
|
||||
_vertical.hasClients ? _vertical.position.maxScrollExtent : 0);
|
||||
|
||||
return (scrollPixel, max);
|
||||
}
|
||||
|
||||
void edgeScrollMouse(double x, double y) async {
|
||||
if ((_edgeScrollState == EdgeScrollState.inactive) ||
|
||||
(size.width == 0 || size.height == 0) ||
|
||||
!(_horizontal.hasClients || _vertical.hasClients)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_edgeScrollState == EdgeScrollState.armed) {
|
||||
// Edge scroll is armed to become active once the cursor
|
||||
// is observed within the rectangle interior to the
|
||||
// edge scroll regions. If the user has just moved the
|
||||
// cursor in from outside of the window, edge scrolling
|
||||
// doesn't happen yet.
|
||||
final clientArea = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
|
||||
final innerZone = clientArea.deflate(_edgeScrollEdgeThickness.toDouble());
|
||||
|
||||
if (innerZone.contains(Offset(x, y))) {
|
||||
_edgeScrollState = EdgeScrollState.active;
|
||||
} else {
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = false;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
// Not yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var dxOffset = 0.0;
|
||||
var dyOffset = 0.0;
|
||||
|
||||
if (x < _edgeScrollEdgeThickness) {
|
||||
dxOffset = x - _edgeScrollEdgeThickness;
|
||||
} else if (x >= size.width - _edgeScrollEdgeThickness) {
|
||||
dxOffset = x - (size.width - _edgeScrollEdgeThickness);
|
||||
}
|
||||
|
||||
if (y < _edgeScrollEdgeThickness) {
|
||||
dyOffset = y - _edgeScrollEdgeThickness;
|
||||
} else if (y >= size.height - _edgeScrollEdgeThickness) {
|
||||
dyOffset = y - (size.height - _edgeScrollEdgeThickness);
|
||||
}
|
||||
|
||||
var encroachment = Vector2(dxOffset, dyOffset);
|
||||
|
||||
var (scrollPixel, max) = getScrollInfo();
|
||||
|
||||
encroachment.clamp(-scrollPixel, max - scrollPixel);
|
||||
|
||||
if (encroachment.length2 == 0) {
|
||||
_edgeScrollFallbackState.stop();
|
||||
} else {
|
||||
var bumpAmount = -encroachment;
|
||||
|
||||
// Round away from 0: this ensures that the mouse will be bumped clear of
|
||||
// whichever edge scroll zone(s) it is in
|
||||
bumpAmount.x += bumpAmount.x.sign * 0.5;
|
||||
bumpAmount.y += bumpAmount.y.sign * 0.5;
|
||||
|
||||
var bumpMouseSucceeded = _bumpMouseIsWorking &&
|
||||
(await rustDeskWinManager.call(WindowType.Main, kWindowBumpMouse,
|
||||
{"dx": bumpAmount.x.round(), "dy": bumpAmount.y.round()}))
|
||||
.result;
|
||||
|
||||
if (bumpMouseSucceeded) {
|
||||
performEdgeScroll(encroachment);
|
||||
} else {
|
||||
// If we can't BumpMouse, then we switch to slower scrolling with autorepeat
|
||||
|
||||
// Don't keep hammering BumpMouse if it's not working.
|
||||
_bumpMouseIsWorking = false;
|
||||
|
||||
// Keep scrolling as long as the user is overtop of an edge.
|
||||
_edgeScrollFallbackState.setEncroachment(encroachment);
|
||||
_edgeScrollFallbackState.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set scale(v) {
|
||||
_scale = v;
|
||||
void performEdgeScroll(Vector2 delta) {
|
||||
var (scrollPixel, max) = getScrollInfo();
|
||||
|
||||
scrollPixel += delta;
|
||||
|
||||
scrollPixel.clamp(Vector2.zero(), max);
|
||||
|
||||
var scrollPixelPercent = scrollPixel.clone();
|
||||
|
||||
scrollPixelPercent.divide(max);
|
||||
scrollPixelPercent.scale(100.0);
|
||||
|
||||
setScrollPercent(scrollPixelPercent.x, scrollPixelPercent.y);
|
||||
pushScrollPositionToUI(scrollPixel.x, scrollPixel.y);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -2266,9 +2769,25 @@ class CursorModel with ChangeNotifier {
|
||||
|
||||
Rect? get keyHelpToolsRectToAdjustCanvas =>
|
||||
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
|
||||
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
|
||||
_keyHelpToolsRect = r;
|
||||
if (r == null) {
|
||||
// The blocked rect is used to block the pointer/touch events in the remote page.
|
||||
final List<Rect> _blockedRects = [];
|
||||
// Used in shouldBlock().
|
||||
// _blockEvents is a flag to block pointer/touch events on the remote image.
|
||||
// It is set to true to prevent accidental touch events in the following scenarios:
|
||||
// 1. In floating mouse mode, when the scroll circle is shown.
|
||||
// 2. In floating mouse widgets mode, when the left/right buttons are moving.
|
||||
// 3. In floating mouse widgets mode, when using the virtual joystick.
|
||||
// When _blockEvents is true, all pointer/touch events are blocked regardless of the contents of _blockedRects.
|
||||
// _blockedRects contains specific rectangular regions where events are blocked; these are checked when _blockEvents is false.
|
||||
// In summary: _blockEvents acts as a global block, while _blockedRects provides fine-grained blocking.
|
||||
bool _blockEvents = false;
|
||||
List<Rect> get blockedRects => List.unmodifiable(_blockedRects);
|
||||
|
||||
set blockEvents(bool v) => _blockEvents = v;
|
||||
|
||||
keyHelpToolsVisibilityChanged(Rect? rect, bool keyboardIsVisible) {
|
||||
_keyHelpToolsRect = rect;
|
||||
if (rect == null) {
|
||||
_lastIsBlocked = false;
|
||||
} else {
|
||||
// Block the touch event is safe here.
|
||||
@@ -2283,6 +2802,14 @@ class CursorModel with ChangeNotifier {
|
||||
_lastKeyboardIsVisible = keyboardIsVisible;
|
||||
}
|
||||
|
||||
addBlockedRect(Rect rect) {
|
||||
_blockedRects.add(rect);
|
||||
}
|
||||
|
||||
removeBlockedRect(Rect rect) {
|
||||
_blockedRects.remove(rect);
|
||||
}
|
||||
|
||||
get lastIsBlocked => _lastIsBlocked;
|
||||
|
||||
ui.Image? get image => _image;
|
||||
@@ -2349,13 +2876,22 @@ class CursorModel with ChangeNotifier {
|
||||
|
||||
// mobile Soft keyboard, block touch event from the KeyHelpTools
|
||||
shouldBlock(double x, double y) {
|
||||
if (_blockEvents) {
|
||||
return true;
|
||||
}
|
||||
final offset = Offset(x, y);
|
||||
for (final rect in _blockedRects) {
|
||||
if (isPointInRect(offset, rect)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For help tools rectangle, only block touch event when in touch mode.
|
||||
if (!(parent.target?.ffiModel.touchMode ?? false)) {
|
||||
return false;
|
||||
}
|
||||
if (_keyHelpToolsRect == null) {
|
||||
return false;
|
||||
}
|
||||
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
|
||||
if (_keyHelpToolsRect != null &&
|
||||
isPointInRect(offset, _keyHelpToolsRect!)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -2375,6 +2911,10 @@ class CursorModel with ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> syncCursorPosition() async {
|
||||
await parent.target?.inputModel.moveMouse(_x, _y);
|
||||
}
|
||||
|
||||
bool isInRemoteRect(Offset offset) {
|
||||
return getRemotePosInRect(offset) != null;
|
||||
}
|
||||
@@ -2444,9 +2984,10 @@ class CursorModel with ChangeNotifier {
|
||||
var cx = r.center.dx;
|
||||
var cy = r.center.dy;
|
||||
var tryMoveCanvasX = false;
|
||||
final displayRect = parent.target?.ffiModel.rect;
|
||||
if (dx > 0) {
|
||||
final maxCanvasCanMove = _displayOriginX +
|
||||
(parent.target?.imageModel.image!.width ?? 1280) -
|
||||
(displayRect?.width ?? 1280) -
|
||||
r.right.roundToDouble();
|
||||
tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0;
|
||||
if (tryMoveCanvasX) {
|
||||
@@ -2468,7 +3009,7 @@ class CursorModel with ChangeNotifier {
|
||||
var tryMoveCanvasY = false;
|
||||
if (dy > 0) {
|
||||
final mayCanvasCanMove = _displayOriginY +
|
||||
(parent.target?.imageModel.image!.height ?? 720) -
|
||||
(displayRect?.height ?? 720) -
|
||||
r.bottom.roundToDouble();
|
||||
tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0;
|
||||
if (tryMoveCanvasY) {
|
||||
@@ -2889,7 +3430,6 @@ class FFI {
|
||||
var version = '';
|
||||
var connType = ConnType.defaultConn;
|
||||
var closed = false;
|
||||
var auditNote = '';
|
||||
|
||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||
late final dialogManager = OverlayDialogManager();
|
||||
@@ -2980,7 +3520,6 @@ class FFI {
|
||||
List<int>? displays,
|
||||
}) {
|
||||
closed = false;
|
||||
auditNote = '';
|
||||
if (isMobile) mobileReset();
|
||||
assert(
|
||||
(!(isPortForward && isViewCamera)) &&
|
||||
@@ -3172,6 +3711,7 @@ class FFI {
|
||||
dialogManager.dismissAll();
|
||||
await canvasModel.updateViewStyle();
|
||||
await canvasModel.updateScrollStyle();
|
||||
await canvasModel.initializeEdgeScrollEdgeThickness();
|
||||
for (final cb in imageModel.callbacksOnFirstImage) {
|
||||
cb(id);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class Peer {
|
||||
bool online = false;
|
||||
String loginName; //login username
|
||||
String device_group_name;
|
||||
String note;
|
||||
bool? sameServer;
|
||||
|
||||
String getId() {
|
||||
@@ -43,6 +44,7 @@ class Peer {
|
||||
rdpUsername = json['rdpUsername'] ?? '',
|
||||
loginName = json['loginName'] ?? '',
|
||||
device_group_name = json['device_group_name'] ?? '',
|
||||
note = json['note'] is String ? json['note'] : '',
|
||||
sameServer = json['same_server'];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -60,6 +62,7 @@ class Peer {
|
||||
"rdpUsername": rdpUsername,
|
||||
'loginName': loginName,
|
||||
'device_group_name': device_group_name,
|
||||
'note': note,
|
||||
'same_server': sameServer,
|
||||
};
|
||||
}
|
||||
@@ -104,6 +107,7 @@ class Peer {
|
||||
required this.rdpUsername,
|
||||
required this.loginName,
|
||||
required this.device_group_name,
|
||||
required this.note,
|
||||
this.sameServer,
|
||||
});
|
||||
|
||||
@@ -122,6 +126,7 @@ class Peer {
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
note: '',
|
||||
);
|
||||
bool equal(Peer other) {
|
||||
return id == other.id &&
|
||||
@@ -136,7 +141,8 @@ class Peer {
|
||||
rdpPort == other.rdpPort &&
|
||||
rdpUsername == other.rdpUsername &&
|
||||
device_group_name == other.device_group_name &&
|
||||
loginName == other.loginName;
|
||||
loginName == other.loginName &&
|
||||
note == other.note;
|
||||
}
|
||||
|
||||
Peer.copy(Peer other)
|
||||
@@ -154,6 +160,7 @@ class Peer {
|
||||
rdpUsername: other.rdpUsername,
|
||||
loginName: other.loginName,
|
||||
device_group_name: other.device_group_name,
|
||||
note: other.note,
|
||||
sameServer: other.sameServer);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class TerminalModel with ChangeNotifier {
|
||||
|
||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||
|
||||
Future<void> _handleInput(String data) async {
|
||||
// If we press the `Enter` button on Android,
|
||||
// `data` can be '\r' or '\n' when using different keyboards.
|
||||
@@ -68,6 +70,10 @@ class TerminalModel with ChangeNotifier {
|
||||
if (w > 0 && h > 0 && pw > 0 && ph > 0) {
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal resized to ${w}x$h (pixel: ${pw}x$ph)');
|
||||
|
||||
// This piece of code must be placed before the conditional check in order to initialize properly.
|
||||
onResizeExternal?.call(w, h, pw, ph);
|
||||
|
||||
if (_terminalOpened) {
|
||||
// Notify remote terminal of resize
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/platform_model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
export 'package:http/http.dart' show Response;
|
||||
|
||||
enum HttpMethod { get, post, put, delete }
|
||||
@@ -15,11 +17,19 @@ class HttpService {
|
||||
}) async {
|
||||
headers ??= {'Content-Type': 'application/json'};
|
||||
|
||||
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
|
||||
final isProxy = await bind.mainGetProxyStatus();
|
||||
// Use Rust HTTP implementation for non-web platforms for consistency.
|
||||
var useFlutterHttp = (isWeb || kIsWeb);
|
||||
if (!useFlutterHttp) {
|
||||
final enableFlutterHttpOnRust =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableFlutterHttpOnRust);
|
||||
// Use flutter http if:
|
||||
// Not `enableFlutterHttpOnRust` and no proxy is set
|
||||
useFlutterHttp =
|
||||
!(enableFlutterHttpOnRust || await bind.mainGetProxyStatus());
|
||||
}
|
||||
|
||||
if (!isProxy) {
|
||||
return await _pollFultterHttp(url, method, headers: headers, body: body);
|
||||
if (useFlutterHttp) {
|
||||
return await _pollFlutterHttp(url, method, headers: headers, body: body);
|
||||
}
|
||||
|
||||
String headersJson = jsonEncode(headers);
|
||||
@@ -34,7 +44,7 @@ class HttpService {
|
||||
return _parseHttpResponse(resJson);
|
||||
}
|
||||
|
||||
Future<http.Response> _pollFultterHttp(
|
||||
Future<http.Response> _pollFlutterHttp(
|
||||
Uri url,
|
||||
HttpMethod method, {
|
||||
Map<String, String>? headers,
|
||||
@@ -87,7 +97,8 @@ class HttpService {
|
||||
int statusCode = parsedJson['status_code'];
|
||||
return http.Response(body, statusCode, headers: headers);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to parse response: $e');
|
||||
print('Failed to parse response\n$responseJson\nError:\n$e');
|
||||
throw Exception('Failed to parse response.\n$responseJson');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +475,11 @@ class RustDeskMultiWindowManager {
|
||||
final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1;
|
||||
if (shouldSavePos) {
|
||||
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
try {
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to save window position of $wId, $e');
|
||||
}
|
||||
}
|
||||
try {
|
||||
await WindowController.fromWindowId(wId).setPreventClose(false);
|
||||
|
||||
@@ -13,8 +13,18 @@ class RdPlatformChannel {
|
||||
|
||||
static RdPlatformChannel get instance => _windowUtil;
|
||||
|
||||
final MethodChannel _osxMethodChannel =
|
||||
MethodChannel("org.rustdesk.rustdesk/macos");
|
||||
final MethodChannel _hostMethodChannel =
|
||||
MethodChannel("org.rustdesk.rustdesk/host");
|
||||
|
||||
/// Bump the position of the mouse cursor, if applicable
|
||||
Future<bool> bumpMouse({required int dx, required int dy}) async {
|
||||
// No debug output; this call is too chatty.
|
||||
|
||||
bool? result = await _hostMethodChannel
|
||||
.invokeMethod("bumpMouse", {"dx": dx, "dy": dy});
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
/// Change the theme of the system window
|
||||
Future<void> changeSystemWindowTheme(SystemWindowTheme theme) {
|
||||
@@ -23,13 +33,13 @@ class RdPlatformChannel {
|
||||
print(
|
||||
"[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}");
|
||||
}
|
||||
return _osxMethodChannel
|
||||
return _hostMethodChannel
|
||||
.invokeMethod("setWindowTheme", {"themeName": theme.name});
|
||||
}
|
||||
|
||||
/// Terminate .app manually.
|
||||
Future<void> terminate() {
|
||||
assert(isMacOS);
|
||||
return _osxMethodChannel.invokeMethod("terminate");
|
||||
return _hostMethodChannel.invokeMethod("terminate");
|
||||
}
|
||||
}
|
||||
|
||||
34
flutter/lib/utils/scale.dart
Normal file
34
flutter/lib/utils/scale.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Clamp custom scale percent to supported bounds.
|
||||
/// Keep this in sync with the slider's minimum in the desktop toolbar UI.
|
||||
///
|
||||
/// This function exists to ensure consistent clamping behavior across the app
|
||||
/// and to provide a single point of reference for the valid scale range.
|
||||
int clampCustomScalePercent(int percent) {
|
||||
return percent.clamp(kScaleCustomMinPercent, kScaleCustomMaxPercent);
|
||||
}
|
||||
|
||||
/// Parse a string percent and clamp. Defaults to 100 when invalid.
|
||||
int parseCustomScalePercent(String? s, {int defaultPercent = 100}) {
|
||||
final parsed = int.tryParse(s ?? '') ?? defaultPercent;
|
||||
return clampCustomScalePercent(parsed);
|
||||
}
|
||||
|
||||
/// Convert a percent value to scale factor after clamping.
|
||||
double percentToScale(int percent) => clampCustomScalePercent(percent) / 100.0;
|
||||
|
||||
/// Fetch, parse and clamp the custom scale percent for a session.
|
||||
Future<int> getSessionCustomScalePercent(UuidValue sessionId) async {
|
||||
final opt = await bind.sessionGetFlutterOption(
|
||||
sessionId: sessionId, k: kCustomScalePercentKey);
|
||||
return parseCustomScalePercent(opt);
|
||||
}
|
||||
|
||||
/// Fetch and compute the custom scale factor for a session.
|
||||
Future<double> getSessionCustomScale(UuidValue sessionId) async {
|
||||
final p = await getSessionCustomScalePercent(sessionId);
|
||||
return percentToScale(p);
|
||||
}
|
||||
@@ -812,7 +812,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetAppNameSync({dynamic hint}) {
|
||||
return 'RustDesk';
|
||||
return js.context.callMethod('getByName', ['app-name']);
|
||||
}
|
||||
|
||||
String mainUriPrefixSync({dynamic hint}) {
|
||||
@@ -1609,23 +1609,28 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
bool isCustomClient({dynamic hint}) {
|
||||
return false;
|
||||
// is_custom_client() checks if app name is not "RustDesk"
|
||||
return mainGetAppNameSync(hint: hint) != "RustDesk";
|
||||
}
|
||||
|
||||
bool isDisableSettings({dynamic hint}) {
|
||||
return false;
|
||||
// Checks HARD_SETTINGS["disable-settings"] == "Y"
|
||||
return mainGetHardOption(key: "disable-settings", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableAb({dynamic hint}) {
|
||||
return false;
|
||||
// Checks HARD_SETTINGS["disable-ab"] == "Y"
|
||||
return mainGetHardOption(key: "disable-ab", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableGroupPanel({dynamic hint}) {
|
||||
return false;
|
||||
// Checks LocalConfig::get_option("disable-group-panel") == "Y"
|
||||
return mainGetLocalOption(key: "disable-group-panel", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableAccount({dynamic hint}) {
|
||||
return false;
|
||||
// Checks HARD_SETTINGS["disable-account"] == "Y"
|
||||
return mainGetHardOption(key: "disable-account", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableInstallation({dynamic hint}) {
|
||||
@@ -1748,7 +1753,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetHardOption({required String key, dynamic hint}) {
|
||||
throw UnimplementedError("mainGetHardOption");
|
||||
return mainGetLocalOption(key: key, hint: hint);
|
||||
}
|
||||
|
||||
Future<void> mainCheckHwcodec({dynamic hint}) {
|
||||
@@ -1821,7 +1826,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetBuildinOption({required String key, dynamic hint}) {
|
||||
return '';
|
||||
return mainGetLocalOption(key: key, hint: hint);
|
||||
}
|
||||
|
||||
String installInstallOptions({dynamic hint}) {
|
||||
@@ -1979,5 +1984,41 @@ class RustdeskImpl {
|
||||
]));
|
||||
}
|
||||
|
||||
Future<int?> sessionGetEdgeScrollEdgeThickness(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
final thickness = js.context.callMethod(
|
||||
'getByName', ['option:session', 'edge-scroll-edge-thickness']);
|
||||
return Future(() => int.tryParse(thickness) ?? 100);
|
||||
}
|
||||
|
||||
Future<void> sessionSetEdgeScrollEdgeThickness(
|
||||
{required UuidValue sessionId, required int value, dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName',
|
||||
['option:session', 'edge-scroll-edge-thickness', value.toString()]));
|
||||
}
|
||||
|
||||
String sessionGetConnSessionId({required UuidValue sessionId, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['conn_session_id']);
|
||||
}
|
||||
|
||||
bool willSessionCloseCloseSession(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String sessionGetLastAuditNote({required UuidValue sessionId, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['last_audit_note']);
|
||||
}
|
||||
|
||||
Future<void> sessionSetAuditGuid(
|
||||
{required UuidValue sessionId, required String guid, dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['audit_guid', guid]));
|
||||
}
|
||||
|
||||
String sessionGetAuditGuid({required UuidValue sessionId, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['audit_guid']);
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"bump_mouse.cc"
|
||||
"bump_mouse_x11.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
|
||||
18
flutter/linux/bump_mouse.cc
Normal file
18
flutter/linux/bump_mouse.cc
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "bump_mouse.h"
|
||||
|
||||
#include "bump_mouse_x11.h"
|
||||
|
||||
#include <gdk/gdkx.h>
|
||||
|
||||
bool bump_mouse(int dx, int dy)
|
||||
{
|
||||
GdkDisplay *display = gdk_display_get_default();
|
||||
|
||||
if (GDK_IS_X11_DISPLAY(display)) {
|
||||
return bump_mouse_x11(dx, dy);
|
||||
}
|
||||
else {
|
||||
// Don't know how to support this.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
3
flutter/linux/bump_mouse.h
Normal file
3
flutter/linux/bump_mouse.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
bool bump_mouse(int dx, int dy);
|
||||
30
flutter/linux/bump_mouse_x11.cc
Normal file
30
flutter/linux/bump_mouse_x11.cc
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "bump_mouse.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <gdk/gdkx.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
bool bump_mouse_x11(int dx, int dy)
|
||||
{
|
||||
GdkDevice *mouse_device;
|
||||
|
||||
#if GTK_CHECK_VERSION(3, 20, 0)
|
||||
auto seat = gdk_display_get_default_seat(gdk_display_get_default());
|
||||
|
||||
mouse_device = gdk_seat_get_pointer(seat);
|
||||
#else
|
||||
auto devman = gdk_display_get_device_manager(gdk_display_get_default());
|
||||
|
||||
mouse_device = gdk_device_manager_get_client_pointer(devman);
|
||||
#endif
|
||||
|
||||
GdkScreen *screen;
|
||||
gint x, y;
|
||||
|
||||
gdk_device_get_position(mouse_device, &screen, &x, &y);
|
||||
gdk_device_warp(mouse_device, screen, x + dx, y + dy);
|
||||
|
||||
return true;
|
||||
}
|
||||
3
flutter/linux/bump_mouse_x11.h
Normal file
3
flutter/linux/bump_mouse_x11.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
bool bump_mouse_x11(int dx, int dy);
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include "bump_mouse.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
@@ -10,10 +12,13 @@
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
FlMethodChannel* host_channel;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data);
|
||||
|
||||
GtkWidget *find_gl_area(GtkWidget *widget);
|
||||
void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
|
||||
|
||||
@@ -24,10 +29,11 @@ GtkWidget *find_gl_area(GtkWidget *widget);
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
gtk_window_set_decorated(window, FALSE);
|
||||
// try setting icon for rustdesk, which uses the system cache
|
||||
// try setting icon for rustdesk, which uses the system cache
|
||||
GtkIconTheme* theme = gtk_icon_theme_get_default();
|
||||
gint icons[4] = {256, 128, 64, 32};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -87,6 +93,17 @@ static void my_application_activate(GApplication* application) {
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||
self->host_channel = fl_method_channel_new(
|
||||
fl_engine_get_binary_messenger(fl_view_get_engine(view)),
|
||||
"org.rustdesk.rustdesk/host",
|
||||
FL_METHOD_CODEC(codec));
|
||||
fl_method_channel_set_method_call_handler(
|
||||
self->host_channel,
|
||||
host_channel_call_handler,
|
||||
self,
|
||||
nullptr);
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
@@ -113,6 +130,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
g_clear_object(&self->host_channel);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
@@ -131,6 +149,61 @@ MyApplication* my_application_new() {
|
||||
nullptr));
|
||||
}
|
||||
|
||||
void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data)
|
||||
{
|
||||
if (strcmp(fl_method_call_get_name(method_call), "bumpMouse") == 0) {
|
||||
FlValue *args = fl_method_call_get_args(method_call);
|
||||
|
||||
FlValue *dxValue = nullptr;
|
||||
FlValue *dyValue = nullptr;
|
||||
|
||||
switch (fl_value_get_type(args))
|
||||
{
|
||||
case FL_VALUE_TYPE_MAP:
|
||||
{
|
||||
dxValue = fl_value_lookup_string(args, "dx");
|
||||
dyValue = fl_value_lookup_string(args, "dy");
|
||||
|
||||
break;
|
||||
}
|
||||
case FL_VALUE_TYPE_LIST:
|
||||
{
|
||||
int listSize = fl_value_get_length(args);
|
||||
|
||||
dxValue = (listSize >= 1) ? fl_value_get_list_value(args, 0) : nullptr;
|
||||
dyValue = (listSize >= 2) ? fl_value_get_list_value(args, 1) : nullptr;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
int dx = 0, dy = 0;
|
||||
|
||||
if (dxValue && (fl_value_get_type(dxValue) == FL_VALUE_TYPE_INT)) {
|
||||
dx = fl_value_get_int(dxValue);
|
||||
}
|
||||
|
||||
if (dyValue && (fl_value_get_type(dyValue) == FL_VALUE_TYPE_INT)) {
|
||||
dy = fl_value_get_int(dyValue);
|
||||
}
|
||||
|
||||
bool result = bump_mouse(dx, dy);
|
||||
|
||||
FlValue *result_value = fl_value_new_bool(result);
|
||||
|
||||
GError *error = nullptr;
|
||||
|
||||
if (!fl_method_call_respond_success(method_call, result_value, &error)) {
|
||||
g_warning("Failed to send Flutter Platform Channel response: %s", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
fl_value_unref(result_value);
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget *find_gl_area(GtkWidget *widget)
|
||||
{
|
||||
if (GTK_IS_GL_AREA(widget)) {
|
||||
@@ -160,7 +233,7 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view)
|
||||
GtkWidget *gl_area = NULL;
|
||||
|
||||
printf("Try setting transparent\n");
|
||||
|
||||
|
||||
gl_area = find_gl_area(GTK_WIDGET(view));
|
||||
if (gl_area != NULL) {
|
||||
gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE);
|
||||
|
||||
@@ -29,7 +29,7 @@ class MainFlutterWindow: NSWindow {
|
||||
// register self method handler
|
||||
let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin")
|
||||
setMethodHandler(registrar: registrar)
|
||||
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
||||
@@ -50,22 +50,22 @@ class MainFlutterWindow: NSWindow {
|
||||
WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin"))
|
||||
TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin"))
|
||||
}
|
||||
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
|
||||
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
|
||||
super.order(place, relativeTo: otherWin)
|
||||
hiddenWindowAtLaunch()
|
||||
}
|
||||
|
||||
|
||||
/// Override window theme.
|
||||
public func setWindowInterfaceMode(window: NSWindow, themeName: String) {
|
||||
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
||||
}
|
||||
|
||||
|
||||
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger)
|
||||
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
||||
channel.setMethodCallHandler({
|
||||
(call, result) -> Void in
|
||||
switch call.method {
|
||||
@@ -99,6 +99,58 @@ class MainFlutterWindow: NSWindow {
|
||||
result(granted)
|
||||
})
|
||||
break
|
||||
case "bumpMouse":
|
||||
var dx = 0
|
||||
var dy = 0
|
||||
|
||||
if let argMap = call.arguments as? [String: Any] {
|
||||
dx = (argMap["dx"] as? Int) ?? 0
|
||||
dy = (argMap["dy"] as? Int) ?? 0
|
||||
}
|
||||
else if let argList = call.arguments as? [Any] {
|
||||
dx = argList.count >= 1 ? (argList[0] as? Int) ?? 0 : 0
|
||||
dy = argList.count >= 2 ? (argList[1] as? Int) ?? 0 : 0
|
||||
}
|
||||
|
||||
var mouseLoc: CGPoint
|
||||
|
||||
if let dummyEvent = CGEvent(source: nil) { // can this ever fail?
|
||||
mouseLoc = dummyEvent.location
|
||||
}
|
||||
else if let screenFrame = NSScreen.screens.first?.frame {
|
||||
// NeXTStep: Origin is lower-left of primary screen, positive is up
|
||||
// Cocoa Core Graphics: Origin is upper-left of primary screen, positive is down
|
||||
let nsMouseLoc = NSEvent.mouseLocation
|
||||
|
||||
mouseLoc = CGPoint(
|
||||
x: nsMouseLoc.x,
|
||||
y: NSHeight(screenFrame) - nsMouseLoc.y)
|
||||
}
|
||||
else {
|
||||
result(false)
|
||||
break
|
||||
}
|
||||
|
||||
let newLoc = CGPoint(x: mouseLoc.x + CGFloat(dx), y: mouseLoc.y + CGFloat(dy))
|
||||
|
||||
CGDisplayMoveCursorToPoint(0, newLoc)
|
||||
|
||||
// By default, Cocoa suppresses mouse events briefly after a call to warp the
|
||||
// cursor to a new location. This is good if you want to draw the user's
|
||||
// attention to the fact that the mouse is now in a particular location, but
|
||||
// it's bad in this case; we get called as part of the handling of edge
|
||||
// scrolling, which means the mouse is typically still in motion, and we want
|
||||
// the cursor to keep moving smoothly uninterrupted.
|
||||
//
|
||||
// This function's main action is to toggle whether the mouse cursor is
|
||||
// associated with the mouse position, but setting it to true when it's
|
||||
// already true has the side-effect of cancelling this motion suppression.
|
||||
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
||||
|
||||
result(true)
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.4.2+60
|
||||
version: 1.4.4+62
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -109,6 +109,7 @@ dependencies:
|
||||
xterm: 4.0.0
|
||||
sqflite: 2.2.0
|
||||
google_fonts: ^6.2.1
|
||||
vector_math: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
|
||||
@@ -10,6 +10,7 @@ add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_desktop.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <desktop_multi_window/desktop_multi_window_plugin.h>
|
||||
#include <texture_rgba_renderer/texture_rgba_renderer_plugin_c_api.h>
|
||||
#include <flutter_gpu_texture_renderer/flutter_gpu_texture_renderer_plugin_c_api.h>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter/event_channel.h>
|
||||
#include <flutter/event_sink.h>
|
||||
#include <flutter/event_stream_handler_functions.h>
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
|
||||
#include "win32_desktop.h"
|
||||
|
||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||
: project_(project) {}
|
||||
|
||||
@@ -29,6 +40,48 @@ bool FlutterWindow::OnCreate() {
|
||||
return false;
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
|
||||
flutter::MethodChannel<> channel(
|
||||
flutter_controller_->engine()->messenger(),
|
||||
"org.rustdesk.rustdesk/host",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
channel.SetMethodCallHandler(
|
||||
[](const flutter::MethodCall<>& call, std::unique_ptr<flutter::MethodResult<>> result) {
|
||||
if (call.method_name() == "bumpMouse") {
|
||||
auto arguments = call.arguments();
|
||||
|
||||
int dx = 0, dy = 0;
|
||||
|
||||
if (std::holds_alternative<flutter::EncodableMap>(*arguments)) {
|
||||
auto argsMap = std::get<flutter::EncodableMap>(*arguments);
|
||||
|
||||
auto dxIt = argsMap.find(flutter::EncodableValue("dx"));
|
||||
auto dyIt = argsMap.find(flutter::EncodableValue("dy"));
|
||||
|
||||
if ((dxIt != argsMap.end()) && std::holds_alternative<int>(dxIt->second)) {
|
||||
dx = std::get<int>(dxIt->second);
|
||||
}
|
||||
if ((dyIt != argsMap.end()) && std::holds_alternative<int>(dyIt->second)) {
|
||||
dy = std::get<int>(dyIt->second);
|
||||
}
|
||||
} else if (std::holds_alternative<flutter::EncodableList>(*arguments)) {
|
||||
auto argsList = std::get<flutter::EncodableList>(*arguments);
|
||||
|
||||
if ((argsList.size() >= 1) && std::holds_alternative<int>(argsList[0])) {
|
||||
dx = std::get<int>(argsList[0]);
|
||||
}
|
||||
if ((argsList.size() >= 2) && std::holds_alternative<int>(argsList[1])) {
|
||||
dy = std::get<int>(argsList[1]);
|
||||
}
|
||||
}
|
||||
|
||||
bool succeeded = Win32Desktop::BumpMouse(dx, dy);
|
||||
|
||||
result->Success(succeeded);
|
||||
}
|
||||
});
|
||||
|
||||
DesktopMultiWindowSetWindowCreatedCallback([](void *controller) {
|
||||
auto *flutter_view_controller =
|
||||
reinterpret_cast<flutter::FlutterViewController *>(controller);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
#include "win32_desktop.h"
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -126,8 +127,22 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(800, 600);
|
||||
|
||||
// Get primary monitor's work area.
|
||||
Win32Window::Point workarea_origin(0, 0);
|
||||
Win32Window::Size workarea_size(0, 0);
|
||||
|
||||
Win32Desktop::GetWorkArea(workarea_origin, workarea_size);
|
||||
|
||||
// Compute window bounds for default main window position: (10, 10) x(800, 600)
|
||||
Win32Window::Point relative_origin(10, 10);
|
||||
|
||||
Win32Window::Point origin(workarea_origin.x + relative_origin.x, workarea_origin.y + relative_origin.y);
|
||||
Win32Window::Size size(800u, 600u);
|
||||
|
||||
// Fit the window to the monitor's work area.
|
||||
Win32Desktop::FitToWorkArea(origin, size);
|
||||
|
||||
std::wstring window_title;
|
||||
if (is_cm_page) {
|
||||
window_title = app_name + L" - Connection Manager";
|
||||
|
||||
82
flutter/windows/runner/win32_desktop.cpp
Normal file
82
flutter/windows/runner/win32_desktop.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "win32_desktop.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Win32Desktop
|
||||
{
|
||||
void GetWorkArea(Win32Window::Point& origin, Win32Window::Size& size)
|
||||
{
|
||||
RECT windowRect;
|
||||
|
||||
windowRect.left = origin.x;
|
||||
windowRect.top = origin.y;
|
||||
windowRect.right = origin.x + size.width;
|
||||
windowRect.bottom = origin.y + size.height;
|
||||
|
||||
HMONITOR hMonitor = MonitorFromRect(&windowRect, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
if (hMonitor == NULL)
|
||||
hMonitor = MonitorFromWindow(NULL, MONITOR_DEFAULTTOPRIMARY);
|
||||
|
||||
RECT workAreaRect;
|
||||
workAreaRect.left = 0;
|
||||
workAreaRect.top = 0;
|
||||
workAreaRect.right = 1280;
|
||||
workAreaRect.bottom = 1024 - 40; // default Windows 10 task bar height
|
||||
|
||||
if (hMonitor != NULL)
|
||||
{
|
||||
MONITORINFO monitorInfo = {0};
|
||||
|
||||
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||
|
||||
if (GetMonitorInfoW(hMonitor, &monitorInfo))
|
||||
{
|
||||
workAreaRect = monitorInfo.rcWork;
|
||||
}
|
||||
}
|
||||
|
||||
origin.x = workAreaRect.left;
|
||||
origin.y = workAreaRect.top;
|
||||
|
||||
size.width = workAreaRect.right - workAreaRect.left;
|
||||
size.height = workAreaRect.bottom - workAreaRect.top;
|
||||
}
|
||||
|
||||
void FitToWorkArea(Win32Window::Point& origin, Win32Window::Size& size)
|
||||
{
|
||||
// Retrieve the work area of the monitor that contains or
|
||||
// is closed to the supplied window bounds.
|
||||
Win32Window::Point workarea_origin = origin;
|
||||
Win32Window::Size workarea_size = size;
|
||||
|
||||
GetWorkArea(workarea_origin, workarea_size);
|
||||
|
||||
// Translate the window so that its top/left is inside the work area.
|
||||
origin.x = std::max(origin.x, workarea_origin.x);
|
||||
origin.y = std::max(origin.y, workarea_origin.y);
|
||||
|
||||
// Crop the window if it extends past the bottom/right of the work area.
|
||||
Win32Window::Point workarea_bottom_right(
|
||||
workarea_origin.x + workarea_size.width,
|
||||
workarea_origin.y + workarea_size.height);
|
||||
|
||||
size.width = std::min(size.width, workarea_bottom_right.x - origin.x);
|
||||
size.height = std::min(size.height, workarea_bottom_right.y - origin.y);
|
||||
}
|
||||
|
||||
bool BumpMouse(int dx, int dy)
|
||||
{
|
||||
POINT pos;
|
||||
|
||||
if (GetCursorPos(&pos))
|
||||
{
|
||||
SetCursorPos(pos.x + dx, pos.y + dy);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
13
flutter/windows/runner/win32_desktop.h
Normal file
13
flutter/windows/runner/win32_desktop.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef RUNNER_WIN32_DESKTOP_H_
|
||||
#define RUNNER_WIN32_DESKTOP_H_
|
||||
|
||||
#include "win32_window.h"
|
||||
|
||||
namespace Win32Desktop
|
||||
{
|
||||
void GetWorkArea(Win32Window::Point& origin, Win32Window::Size& size);
|
||||
void FitToWorkArea(Win32Window::Point& origin, Win32Window::Size& size);
|
||||
bool BumpMouse(int dx, int dy);
|
||||
}
|
||||
|
||||
#endif // RUNNER_WIN32_DESKTOP_H_
|
||||
Submodule libs/hbb_common updated: 334641686c...a86eda749e
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.2"
|
||||
version = "1.4.4"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
@@ -15,6 +15,14 @@ md5 = "0.7"
|
||||
winapi = { version = "0.3", features = ["winbase"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.61", features = [
|
||||
"Wdk",
|
||||
"Wdk_System",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32",
|
||||
"Win32_System",
|
||||
"Win32_System_SystemInformation",
|
||||
] }
|
||||
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
||||
|
||||
[package.metadata.winres]
|
||||
|
||||
@@ -92,12 +92,46 @@ fn setup(
|
||||
}
|
||||
write_meta(&dir, ts);
|
||||
#[cfg(windows)]
|
||||
windows::copy_runtime_broker(&dir);
|
||||
win::copy_runtime_broker(&dir);
|
||||
#[cfg(linux)]
|
||||
reader.configure_permission(&dir);
|
||||
Some(dir.join(&reader.exe))
|
||||
}
|
||||
|
||||
fn use_null_stdio() -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// When running in CMD on Windows 7, using Stdio::inherit() with spawn returns an "invalid handle" error.
|
||||
// Since using Stdio::null() didn’t cause any issues, and determining whether the program is launched from CMD or by double-clicking would require calling more APIs during startup, we also use Stdio::null() when launched by double-clicking on Windows 7.
|
||||
let is_windows_7 = is_windows_7();
|
||||
println!("is windows7: {}", is_windows_7);
|
||||
return is_windows_7;
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_windows_7() -> bool {
|
||||
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
||||
use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
|
||||
|
||||
unsafe {
|
||||
let mut version_info = OSVERSIONINFOW::default();
|
||||
version_info.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOW>() as u32;
|
||||
|
||||
if RtlGetVersion(&mut version_info).is_ok() {
|
||||
// Windows 7 is version 6.1
|
||||
println!(
|
||||
"Windows version: {}.{}",
|
||||
version_info.dwMajorVersion, version_info.dwMinorVersion
|
||||
);
|
||||
return version_info.dwMajorVersion == 6 && version_info.dwMinorVersion == 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
|
||||
println!("executing {}", path.display());
|
||||
// setup env
|
||||
@@ -114,12 +148,18 @@ fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
|
||||
cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1");
|
||||
}
|
||||
}
|
||||
let _child = cmd
|
||||
.env(APPNAME_RUNTIME_ENV_KEY, exe_name)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn();
|
||||
|
||||
cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name);
|
||||
if use_null_stdio() {
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
} else {
|
||||
cmd.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
}
|
||||
let _child = cmd.spawn();
|
||||
|
||||
#[cfg(windows)]
|
||||
if _ui {
|
||||
@@ -168,7 +208,7 @@ fn main() {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows {
|
||||
mod win {
|
||||
use std::{fs, os::windows::process::CommandExt, path::Path, process::Command};
|
||||
|
||||
// Used for privacy mode(magnifier impl).
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use hbb_common::{bail, ResultType};
|
||||
use std::{io, ptr::null_mut};
|
||||
use winapi::{
|
||||
|
||||
@@ -10,7 +10,7 @@ authors = ["Ram <quadrupleslap@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||
mediacodec = ["ndk"]
|
||||
linux-pkg-config = ["dep:pkg-config"]
|
||||
hwcodec = ["dep:hwcodec"]
|
||||
@@ -57,6 +57,7 @@ tracing = { version = "0.1", optional = true }
|
||||
gstreamer = { version = "0.16", optional = true }
|
||||
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
||||
gstreamer-video = { version = "0.16", optional = true }
|
||||
zbus = { version = "3.15", optional = true }
|
||||
|
||||
[dependencies.hwcodec]
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
|
||||
@@ -227,24 +227,12 @@ fn ffmpeg() {
|
||||
*/
|
||||
|
||||
fn main() {
|
||||
// in this crate, these are also valid configurations
|
||||
println!("cargo:rustc-check-cfg=cfg(dxgi,quartz,x11)");
|
||||
|
||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
|
||||
// We check if is macos, because macos uses rust 1.8.1.
|
||||
// `cargo::rustc-check-cfg` is new with Cargo 1.80.
|
||||
// No need to run `cargo version` to get the version here, because:
|
||||
// The following lines are used to suppress the lint warnings.
|
||||
// warning: unexpected `cfg` condition name: `quartz`
|
||||
if cfg!(target_os = "macos") {
|
||||
if target_os != "ios" {
|
||||
println!("cargo::rustc-check-cfg=cfg(android)");
|
||||
println!("cargo::rustc-check-cfg=cfg(dxgi)");
|
||||
println!("cargo::rustc-check-cfg=cfg(quartz)");
|
||||
println!("cargo::rustc-check-cfg=cfg(x11)");
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^ new with Cargo 1.80
|
||||
}
|
||||
}
|
||||
|
||||
// note: all link symbol names in x86 (32-bit) are prefixed wth "_".
|
||||
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
|
||||
// please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc",
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::time::{Duration, Instant};
|
||||
lazy_static! {
|
||||
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
|
||||
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
|
||||
static ref APPLICATION_CONTEXT: RwLock<Option<GlobalRef>> = RwLock::new(None);
|
||||
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
|
||||
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
|
||||
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
|
||||
@@ -462,6 +463,23 @@ fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) {
|
||||
*lock = true;
|
||||
}
|
||||
|
||||
fn try_init_rustls_platform_verifier(env: &mut JNIEnv, context_jobject: *mut c_void) {
|
||||
use hbb_common::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED as INITIALIZED;
|
||||
use std::sync::atomic::Ordering;
|
||||
let initialized = INITIALIZED.load(Ordering::Relaxed);
|
||||
if !initialized {
|
||||
let ctx_for_rustls = unsafe { JObject::from_raw(context_jobject as jni::sys::jobject) };
|
||||
if let Err(e) =
|
||||
hbb_common::rustls_platform_verifier::android::init_hosted(env, ctx_for_rustls)
|
||||
{
|
||||
log::error!("Failed to initialize rustls-platform-verifier: {:?}", e);
|
||||
} else {
|
||||
INITIALIZED.store(true, Ordering::Relaxed);
|
||||
log::info!("rustls-platform-verifier initialized successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init
|
||||
#[no_mangle]
|
||||
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
|
||||
@@ -471,3 +489,23 @@ pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) ->
|
||||
}
|
||||
jni::JNIVersion::V6.into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onAppStart(mut env: JNIEnv, _class: JClass, ctx: JObject) {
|
||||
if ctx.is_null() {
|
||||
log::error!("application context is null");
|
||||
return;
|
||||
}
|
||||
if APPLICATION_CONTEXT.read().unwrap().is_some() {
|
||||
log::info!("application context already initialized");
|
||||
return;
|
||||
}
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
if let Ok(context) = env.new_global_ref(ctx) {
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let context_jobject = context.as_obj().as_raw() as *mut c_void;
|
||||
*APPLICATION_CONTEXT.write().unwrap() = Some(context);
|
||||
try_init_rustls_platform_verifier(&mut env, context_jobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ impl EncoderApi for AomEncoder {
|
||||
}
|
||||
|
||||
impl AomEncoder {
|
||||
pub fn encode(&mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
pub fn encode<'a>(&'a mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames<'a>> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
@@ -461,7 +461,7 @@ impl AomDecoder {
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
|
||||
pub fn decode(&mut self, data: &[u8]) -> Result<DecodeFrames> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result<DecodeFrames<'a>> {
|
||||
call_aom!(aom_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
@@ -476,7 +476,7 @@ impl AomDecoder {
|
||||
}
|
||||
|
||||
/// Notify the decoder to return any pending frame
|
||||
pub fn flush(&mut self) -> Result<DecodeFrames> {
|
||||
pub fn flush<'a>(&'a mut self) -> Result<DecodeFrames<'a>> {
|
||||
call_aom!(aom_codec_decode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
|
||||
@@ -268,12 +268,12 @@ impl TraitCapturer for CameraCapturer {
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_gdi(&self) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn set_gdi(&mut self) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
|
||||
@@ -364,7 +364,7 @@ impl HwRamDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<HwRamDecoderImage>> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType<Vec<HwRamDecoderImage<'a>>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| HwRamDecoderImage { frame: f }).collect()),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
@@ -687,7 +687,7 @@ pub fn check_available_hwcodec() -> String {
|
||||
height: 720,
|
||||
pixfmt: DEFAULT_PIXFMT,
|
||||
align: HW_STRIDE_ALIGN as _,
|
||||
kbs: 0,
|
||||
kbs: 1000,
|
||||
fps: DEFAULT_FPS,
|
||||
gop: DEFAULT_GOP,
|
||||
quality: DEFAULT_HW_QUALITY,
|
||||
|
||||
@@ -88,6 +88,27 @@ impl Display {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
match self {
|
||||
Display::X11(_d) => 1.0,
|
||||
Display::WAYLAND(d) => d.scale(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.width(),
|
||||
Display::WAYLAND(d) => d.logical_width(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.height(),
|
||||
Display::WAYLAND(d) => d.logical_height(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
match self {
|
||||
Display::X11(d) => d.origin(),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
impl Default for vpx_codec_enc_cfg {
|
||||
fn default() -> Self {
|
||||
|
||||
@@ -231,7 +231,7 @@ impl EncoderApi for VpxEncoder {
|
||||
}
|
||||
|
||||
impl VpxEncoder {
|
||||
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
pub fn encode<'a>(&'a mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames<'a>> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
@@ -268,7 +268,7 @@ impl VpxEncoder {
|
||||
}
|
||||
|
||||
/// Notify the encoder to return any pending packets
|
||||
pub fn flush(&mut self) -> Result<EncodeFrames> {
|
||||
pub fn flush<'a>(&'a mut self) -> Result<EncodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_encode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
@@ -473,7 +473,7 @@ impl VpxDecoder {
|
||||
/// The `data` slice is sent to the decoder
|
||||
///
|
||||
/// It matches a call to `vpx_codec_decode`.
|
||||
pub fn decode(&mut self, data: &[u8]) -> Result<DecodeFrames> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result<DecodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
@@ -489,7 +489,7 @@ impl VpxDecoder {
|
||||
}
|
||||
|
||||
/// Notify the decoder to return any pending frame
|
||||
pub fn flush(&mut self) -> Result<DecodeFrames> {
|
||||
pub fn flush<'a>(&'a mut self) -> Result<DecodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
|
||||
@@ -367,7 +367,7 @@ impl VRamDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<VRamDecoderImage>> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType<Vec<VRamDecoderImage<'a>>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| VRamDecoderImage { frame: f }).collect()),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user