491 Commits
1.2.2 ... 1.2.3

171 changed files with 8825 additions and 3133 deletions

View File

@@ -22,7 +22,7 @@ env:
# vcpkg version: 2023.04.15 # vcpkg version: 2023.04.15
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1" VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2" VERSION: "1.2.3"
NDK_VERSION: "r25c" NDK_VERSION: "r25c"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
@@ -82,6 +82,7 @@ jobs:
- name: Install flutter rust bridge deps - name: Install flutter rust bridge deps
run: | run: |
git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
Push-Location flutter ; flutter pub get ; Pop-Location Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@@ -96,6 +97,22 @@ jobs:
VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
continue-on-error: true
shell: bash
run: |
runner_res=$(find . -name "Runner.res");
if [ "$runner_res" == "" ]; then
echo "Runner.res: not found";
else
echo "Runner.res: $runner_res";
cp $runner_res ./libs/portable/Runner.res;
echo "list ./libs/portable/Runner.res";
ls -l ./libs/portable/Runner.res;
fi
- name: Sign rustdesk files - name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7 uses: GermanBluefox/code-sign-action@v7
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'
@@ -198,6 +215,22 @@ jobs:
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
echo "output_folder=./Release" >> $GITHUB_OUTPUT echo "output_folder=./Release" >> $GITHUB_OUTPUT
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
continue-on-error: true
shell: bash
run: |
runner_res=$(find . -name "Runner.res");
if [ "$runner_res" == "" ]; then
echo "Runner.res: not found";
else
echo "Runner.res: $runner_res";
cp $runner_res ./libs/portable/Runner.res;
echo "list ./libs/portable/Runner.res";
ls -l ./libs/portable/Runner.res;
fi
- name: Sign rustdesk files - name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7 uses: GermanBluefox/code-sign-action@v7
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'
@@ -428,6 +461,13 @@ jobs:
prefix-key: rustdesk-lib-cache prefix-key: rustdesk-lib-cache
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib - name: Build rustdesk lib
env: env:
VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg
@@ -439,7 +479,9 @@ jobs:
shell: bash shell: bash
run: | run: |
pushd flutter pushd flutter
flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
# for easy debugging
flutter build ipa --release --no-codesign
# - name: Upload Artifacts # - name: Upload Artifacts
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -1572,8 +1614,8 @@ jobs:
# apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git
# # flatpak deps # # flatpak deps
# flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
# flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
# flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# # package # # package
# pushd flatpak # pushd flatpak
# git clone https://github.com/flathub/shared-modules.git --depth=1 # git clone https://github.com/flathub/shared-modules.git --depth=1
@@ -1635,8 +1677,8 @@ jobs:
apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git
# flatpak deps # flatpak deps
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# package # package
pushd flatpak pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1 git clone https://github.com/flathub/shared-modules.git --depth=1

View File

@@ -15,4 +15,4 @@ jobs:
secrets: inherit secrets: inherit
with: with:
upload-artifact: true upload-artifact: true
upload-tag: "1.2.2" upload-tag: "1.2.3"

View File

@@ -10,7 +10,7 @@ env:
# vcpkg version: 2022.05.10 # vcpkg version: 2022.05.10
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1" VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2" VERSION: "1.2.3"
jobs: jobs:
build-for-history-windows: build-for-history-windows:

View File

@@ -24,7 +24,7 @@ jobs:
path: /opt/artifacts path: /opt/artifacts
key: vcpkg-${{ matrix.job.arch }} key: vcpkg-${{ matrix.job.arch }}
- uses: Kingtous/run-on-arch-action@amd64-support - uses: rustdesk-org/run-on-arch-action@amd64-support
name: Run vcpkg install on ${{ matrix.job.arch }} name: Run vcpkg install on ${{ matrix.job.arch }}
id: vcpkg id: vcpkg
with: with:
@@ -40,12 +40,16 @@ jobs:
apt update -y apt update -y
case "${{ matrix.job.arch }}" in case "${{ matrix.job.arch }}" in
x86_64) x86_64)
# CMake 3.15+
apt install -y gpg wget ca-certificates
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
apt update -y apt update -y
apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev apt install -y curl zip unzip tar git g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev libssl-dev
wget https://github.com/Kitware/CMake/releases/download/v3.27.5/cmake-3.27.5.tar.gz
apt remove -y --purge cmake
tar -zxvf cmake-3.27.5.tar.gz
cd cmake-3.27.5
./bootstrap
make
make install
cd -
cmake --version cmake --version
gcc -v gcc -v
;; ;;

69
Cargo.lock generated
View File

@@ -4422,7 +4422,7 @@ dependencies = [
"base64", "base64",
"indexmap", "indexmap",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml 0.28.2",
"serde 1.0.163", "serde 1.0.163",
"time 0.3.21", "time 0.3.21",
] ]
@@ -4622,6 +4622,15 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "quick-xml"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.28.2" version = "0.28.2"
@@ -4872,7 +4881,7 @@ dependencies = [
[[package]] [[package]]
name = "rdev" name = "rdev"
version = "0.5.0-2" version = "0.5.0-2"
source = "git+https://github.com/fufesou/rdev#ee3057bd97c91529e8b9daf2ca133a5c49f0c0eb" source = "git+https://github.com/fufesou/rdev#2e8221d653f4995c831ad52966e79a514516b1fa"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"core-foundation", "core-foundation",
@@ -5124,7 +5133,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.2.2" version = "1.2.3"
dependencies = [ dependencies = [
"android_logger", "android_logger",
"arboard", "arboard",
@@ -5199,6 +5208,7 @@ dependencies = [
"sys-locale", "sys-locale",
"system_shutdown", "system_shutdown",
"tao", "tao",
"tauri-winrt-notification",
"tray-icon", "tray-icon",
"url", "url",
"users 0.11.0", "users 0.11.0",
@@ -5971,6 +5981,16 @@ dependencies = [
"serde_json 0.9.10", "serde_json 0.9.10",
] ]
[[package]]
name = "tauri-winrt-notification"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5bff1d532fead7c43324a0fa33643b8621a47ce2944a633be4cb6c0240898f"
dependencies = [
"quick-xml 0.23.1",
"windows 0.39.0",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.5.0" version = "3.5.0"
@@ -6824,6 +6844,19 @@ dependencies = [
"windows_x86_64_msvc 0.34.0", "windows_x86_64_msvc 0.34.0",
] ]
[[package]]
name = "windows"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
dependencies = [
"windows_aarch64_msvc 0.39.0",
"windows_i686_gnu 0.39.0",
"windows_i686_msvc 0.39.0",
"windows_x86_64_gnu 0.39.0",
"windows_x86_64_msvc 0.39.0",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.44.0" version = "0.44.0"
@@ -6973,6 +7006,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
[[package]]
name = "windows_aarch64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.2" version = "0.42.2"
@@ -6997,6 +7036,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
[[package]]
name = "windows_i686_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.2" version = "0.42.2"
@@ -7021,6 +7066,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
[[package]]
name = "windows_i686_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.2" version = "0.42.2"
@@ -7045,6 +7096,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
[[package]]
name = "windows_x86_64_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.2" version = "0.42.2"
@@ -7081,6 +7138,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
[[package]]
name = "windows_x86_64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.2" version = "0.42.2"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.2" version = "1.2.3"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -97,6 +97,7 @@ virtual_display = { path = "libs/virtual_display", optional = true }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" } impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
shutdown_hooks = "0.1" shutdown_hooks = "0.1"
tauri-winrt-notification = "0.1.2"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2" objc = "0.2"

View File

@@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br> <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>]<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>]<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> <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> </p>
@@ -13,6 +13,8 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb - bsdtar -zxvf ../rustdesk-1.2.3.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.2 version: 1.2.3
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb - bsdtar -zxvf ../rustdesk-1.2.3.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.2 version: 1.2.3
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -545,13 +545,6 @@ def main():
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# https://github.com/sindresorhus/create-dmg # https://github.com/sindresorhus/create-dmg
system2('/bin/rm -rf *.dmg') system2('/bin/rm -rf *.dmg')
plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist"
txt = open(plist).read()
with open(plist, "wt") as fh:
fh.write(txt.replace("</dict>", """
<key>LSUIElement</key>
<string>1</string>
</dict>"""))
pa = os.environ.get('P') pa = os.environ.get('P')
if pa: if pa:
system2(''' system2('''

View File

@@ -0,0 +1,89 @@
# Katkıda Bulunanların Davranış Kuralları
## Taahhüdümüz
Biz üyeler, katkıda bulunanlar ve liderler olarak, yaş, beden büyüklüğü, görünür veya görünmez engellilik, etnik köken, cinsiyet özellikleri, cinsiyet kimliği ve ifadesi, deneyim seviyesi, eğitim, sosyo-ekonomik durum, milliyet, kişisel görünüm, ırk, din veya cinsel kimlik ve yönelim ayrımı gözetmeksizin herkes için topluluğumuzdaki katılımı taciz içermeyen bir deneyim haline getirmeyi taahhüt ederiz.
ık, hoşgörülü, çeşitli, kapsayıcı ve sağlıklı bir topluluğa katkıda bulunacak şekillerde hareket etmeyi ve etkileşimde bulunmayı taahhüt ederiz.
## Standartlarımız
Topluluğumuz için olumlu bir ortam yaratmaya katkıda bulunan davranış örnekleri şunlardır:
* Diğer insanlara empati ve nezaket göstermek
* Farklı görüşlere, bakış açılarına ve deneyimlere saygılı olmak
* Yapıcı eleştiriyi vermek ve zarifçe kabul etmek
* Hatalarımızdan etkilenenlere sorumluluk kabul etmek, özür dilemek ve deneyimden öğrenmek
* Sadece bireyler olarak değil, aynı zamanda genel topluluk için en iyisi üzerine odaklanmak
Kabul edilemez davranış örnekleri şunları içerir:
* Cinselleştirilmiş dil veya imgelerin kullanımı ve cinsel ilgi veya herhangi bir türdeki yaklaşımlar
* Trollük, aşağılayıcı veya hakaret içeren yorumlar ve kişisel veya siyasi saldırılar
* Kamuoyu veya özel taciz
* Başkalarının fiziksel veya e-posta adresi gibi özel bilgilerini, açık izinleri olmadan yayınlamak
* Profesyonel bir ortamda makul bir şekilde uygunsuz kabul edilebilecek diğer davranışlar
## Uygulama Sorumlulukları
Topluluk liderleri, kabul edilebilir davranış standartlarımızııklığa kavuşturmak ve uygulamakla sorumludur ve uygunsuz, tehditkar, saldırgan veya zarar verici herhangi bir davranışa yanıt olarak uygun ve adil düzeltici önlemler alacaklardır.
Topluluk liderleri, bu Davranış Kurallarına uyumlu olmayan yorumları, taahhütlerini veya kodu, wiki düzenlemelerini, sorunları ve diğer katkıları kaldırma, düzenleme veya reddetme hakkına sahiptir. Denetim kararlarının nedenlerini uygun olduğunda ileteceklerdir.
## Kapsam
Bu Davranış Kuralları, tüm topluluk alanlarında geçerlidir ve aynı zamanda birey resmi olarak topluluğu halka açık alanlarda temsil ettiğinde de geçerlidir. Topluluğumuzu temsil etme örnekleri, resmi bir e-posta adresi kullanmak, resmi bir sosyal medya hesabı üzerinden gönderi yapmak veya çevrimiçi veya çevrimdışı bir etkinlikte atanmış bir temsilci olarak hareket etmeyi içerir.
## Uygulama
Taciz edici, rahatsız edici veya başka türlü kabul edilemez davranış örnekleri, [info@rustdesk.com](mailto:info@rustdesk.com) adresindeki uygulama sorumlularına bildirilebilir. Tüm şikayetler hızlı ve adil bir şekilde incelenecek ve araştırılacaktır.
Tüm topluluk liderleri, olayın raporlayıcısının gizliliğine ve güvenliğine saygı gösterme yükümlülüğündedir.
## Uygulama Kılavuzları
Topluluk liderleri, bu Davranış Kurallarını ihlal olarak değerlendirdikleri herhangi bir eylem için bu Topluluk Etkisi Kılavuzlarını izleyeceklerdir:
### 1. Düzeltme
**Topluluk Etkisi**: Topluluk içinde profesyonel veya hoşgörülü olmayan uygun olmayan dil veya diğer davranışların kullanımı.
**Sonuç**: Topluluk liderlerinden özel ve yazılı bir uyarı almak, ihlalin niteliği ve davranışın nedeninin açıklığa kavuşturulması. Bir kamu özrü istenebilir.
### 2. Uyarı
**Topluluk Etkisi**: Tek bir olay veya dizi aracılığıyla bir ihlal.
**Sonuç**: Devam eden davranış için sonuçları olan bir uyarı. Topluluk liderleri de dahil olmak üzere ihlalle ilgili kişilerle etkileşim, belirli bir süre boyunca önerilmez. Bu, topluluk alanlarında ve sosyal medya gibi harici kanallarda etkileşimleri içerir. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
### 3. Geçici Yasak
**Topluluk Etkisi**: Sürekli uygunsuz davranış da dahil olmak üzere topluluk standartlarının ciddi bir ihlali.
**Sonuç**: Belirli bir süre için toplulukla herhangi bir türdeki etkileşim veya halka açık iletişimden geçici bir yasak. Bu dönem boyunca, toplul
ukla veya uygulama kurallarını uygulayanlarla her türlü kamuoyu veya özel etkileşim izin verilmez. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
### 4. Kalıcı Yasak
**Topluluk Etkisi**: Topluluk standartlarının ihlalinde sürekli bir desen sergilemek, bireye sürekli olarak uygun olmayan davranışlarda bulunmak, bir bireye tacizde bulunmak veya birey sınıflarına karşı saldırganlık veya aşağılama yapmak.
**Sonuç**: Topluluk içinde her türlü halka açık etkileşimden kalıcı bir yasak.
## Atıf
Bu Davranış Kuralları, [Contributor Covenant][anasayfa], 2.0 sürümünden uyarlanmıştır ve
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] adresinde bulunmaktadır.
Topluluk Etkisi Kılavuzları,
[Mozilla'nın davranış kuralları uygulama merdiveni][Mozilla DK] tarafından ilham alınarak oluşturulmuştur.
Bu davranış kuralları hakkında yaygın soruların cevapları için, SSS'ye göz atın:
[https://www.contributor-covenant.org/faq][SSS]. Çeviriler,
[https://www.contributor-covenant.org/translations][çeviriler] adresinde bulunabilir.
[anasayfa]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla DK]: https://github.com/mozilla/diversity
[SSS]: https://www.contributor-covenant.org/faq
[çeviriler]: https://www.contributor-covenant.org/translations

31
docs/CONTRIBUTING-ID.md Normal file
View File

@@ -0,0 +1,31 @@
# Berkontribusi dalam pengembangan RustDesk
RustDesk mengajak semua orang untuk ikut berkontribusi. Berikut ini adalah panduan jika kamu sedang mempertimbangkan untuk memberikan bantuan kepada kami:
## Kontirbusi
Untuk melakukan kontribusi pada RustDesk atau dependensinya, sebaiknya dilakukan dalam bentuk pull request di GitHub. Setiap permintaan pull request akan ditinjau oleh kontributor utama atau seseorang yang memiliki wewenang untuk menggabungkan perubahan kode, baik yang sudah dimasukkan ke dalam struktur utama ataupun memberikan umpan balik untuk perubahan yang akan diperlukan. Setiap kontribusi harus sesuai dengan format ini, juga termasuk yang berasal dari kontributor utama.
Apabila kamu ingin mengatasi sebuah masalah yang sudah ada di daftar issue, harap klaim terlebih dahulu dengan memberikan komentar pada GitHub issue yang ingin kamu kerjakan. Hal ini dilakukan untuk mencegah terjadinya duplikasi dari kontributor pada daftar issue yang sama.
## Pemeriksaan Pull Request
- Branch yang menjadi acuan adalah branch master dari repositori utama dan, jika diperlukan, lakukan rebase ke branch master yang terbaru sebelum kamu mengirim pull request. Apabila terdapat masalah kita melakukan proses merge ke branch master kemungkinan kamu akan diminta untuk melakukan rebase pada perubahan yang sudah dibuat.
- Sebaiknya buatlah commit seminimal mungkin, sambil memastikan bahwa setiap commit yang dibuat sudah benar (contohnya, setiap commit harus bisa di kompilasi dan berhasil melewati tahap test).
- Setiap commit harus disertai dengan tanda tangan Sertifikat Asal Pengembang (Developer Certificate of Origin) (<http://developercertificate.org>), yang mengindikasikan bahwa kamu (and your employer if applicable) bersedia untuk patuh terhadap persyaratan dari [lisensi projek](../LICENCE). Di git bash, ini adalah opsi parameter `-s` pada `git commit`
- Jika perubahan yang kamu buat tidak mendapat tinjauan atau kamu membutuhkan orang tertentu untuk meninjaunya, kamu bisa @-reply seorang reviewer meminta peninjauan dalam permintaan pull request atau komentar, atau kamu bisa meminta tinjauan melalui [email](mailto:info@rustdesk.com).
- Sertakan test yang relevan terhadap bug atau fitur baru yang sudah dikerjakan.
Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Tindakan
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
## Komunikasi
Kontributor RustDesk sering berkunjung ke [Discord](https://discord.gg/nDceKgxnkV).

31
docs/CONTRIBUTING-TR.md Normal file
View File

@@ -0,0 +1,31 @@
# RustDesk'a Katkı Sağlamak
RustDesk, herkesten katkıyı memnuniyetle karşılar. Eğer bize yardımcı olmayı düşünüyorsanız, işte rehberlik eden kurallar:
## Katkılar
RustDesk veya bağımlılıklarına yapılan katkılar, GitHub pull istekleri şeklinde yapılmalıdır. Her bir pull isteği, çekirdek katkıcı tarafından gözden geçirilecek (yamaları kabul etme izni olan biri) ve ana ağaca kabul edilecek veya gerekli değişiklikler için geri bildirim verilecektir. Tüm katkılar bu formata uymalıdır, çekirdek katkıcılardan gelenler bile.
Eğer bir konu üzerinde çalışmak isterseniz, önce üzerinde çalışmak istediğinizi belirten bir yorum yaparak konuyu talep ediniz. Bu, katkı sağlayanların aynı konuda çift çalışmasını engellemek içindir.
## Pull İstek Kontrol Listesi
- Master dalından dallandırın ve gerekiyorsa pull isteğinizi göndermeden önce mevcut master dalına rebase yapın. Eğer master ile temiz bir şekilde birleşmezse, değişikliklerinizi rebase yapmanız istenebilir.
- Her bir commit mümkün olduğunca küçük olmalıdır, ancak her commit'in bağımsız olarak doğru olduğundan emin olun (örneğin, her commit derlenebilir ve testleri geçmelidir).
- Commit'ler, bir Geliştirici Sertifikası ile desteklenmelidir (http://developercertificate.org). Bu, [proje lisansının](../LICENCE) koşullarına uymayı kabul ettiğinizi gösteren bir onaydır. Git'te bunu `git commit` seçeneği olarak `-s` seçeneği ile yapabilirsiniz.
- Yamalarınız gözden geçirilmiyorsa veya belirli bir kişinin gözden geçirmesine ihtiyacınız varsa, çekme isteği veya yorum içinde bir gözden geçirmeyi istemek için bir inceleyiciyi @etiketleyebilir veya inceleme için [e-posta](mailto:info@rustdesk.com) ile talep edebilirsiniz.
- Düzelttiğiniz hatanın veya eklediğiniz yeni özelliğin ilgili testlerini ekleyin.
Daha spesifik git talimatları için, [GitHub iş akışı 101](https://github.com/servo/servo/wiki/GitHub-workflow)'e bakınız.
## Davranış
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-TR.md
## İletişim
RustDesk katkı sağlayıcıları, [Discord](https://discord.gg/nDceKgxnkV) kanalını sık sık ziyaret ederler.

12
docs/DEVCONTAINER-TR.md Normal file
View File

@@ -0,0 +1,12 @@
Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur.
Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır.
Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır.
Komut | Derleme Türü | Mod
-|-|-
`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama
`.devcontainer/build.sh --release linux` | Linux | sürüm
`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama
`.devcontainer/build.sh --release android` | Android-arm64 | sürüm

View File

@@ -13,15 +13,27 @@ Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Merupakan perangkat lunak Remote Desktop yang baru, dibangun dengan Rust. kamu bisa langsung menggunakannya tanpa perlu konfigurasi tambahan. Serta ,emiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo). [![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) untuk melihat panduan. Merupakan perangkat lunak Remote Desktop yang baru, dan dibangun dengan Rust. Bahkan kamu bisa langsung menggunakannya tanpa perlu melakukan konfigurasi tambahan. Serta memiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING-ID.md`](CONTRIBUTING-ID.md) untuk melihat panduan.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**UNDUH BINARY**](https://github.com/rustdesk/rustdesk/releases) [**UNDUH BINARY**](https://github.com/rustdesk/rustdesk/releases)
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Server Publik Gratis ## Server Publik Gratis
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring waktu kemungkinan akan terjadi perubahan spesifikasi pada setiap server. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote. Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
| Lokasi | Penyedia | Spesifikasi | | Lokasi | Penyedia | Spesifikasi |
| --------- | ------------- | ------------------ | | --------- | ------------- | ------------------ |
| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM | | Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
@@ -31,11 +43,11 @@ Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring waktu kemun
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Apabila kamu sudah menginstall VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya. Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
## Dependensi ## Dependensi
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter, tutorial ini hanya berlaku untuk Sciter Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter
Kamu bisa mengunduh Sciter dynamic library disini. Kamu bisa mengunduh Sciter dynamic library disini.

223
docs/README-TR.md Normal file
View File

@@ -0,0 +1,223 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Uzak masaüstü uygulamanız"><br>
<a href="#free-public-servers">Sunucular</a> •
<a href="#raw-steps-to-build">Derleme</a> •
<a href="#how-to-build-with-docker">Docker ile Derleme</a> •
<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>
</p>
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
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).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/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)
[**NİGHTLY 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)
## Ücretsiz Genel Sunucular
Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
| Konum | Sağlayıcı | Özellikler |
| --------- | ------------- | ------------------ |
| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Geliştirici Konteyneri
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
## Bağımlılıklar
Masaüstü sürümleri GUI için
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
[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)
## Temel Derleme Adımları
- 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.
- 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
- `cargo run` komutunu çalıştırın.
## [Derleme](https://rustdesk.com/docs/en/dev/build/)
## Linux Üzerinde Derleme Nasıl Yapılır
### 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
```
### 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
```
### 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
```
### 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
```
### vcpkg'yi Yükleyin
```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
```
### libvpx'i Düzeltin (Fedora için)
```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
```
### Derleme
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone 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
```
### Wayland'ı X11 (Xorg) Olarak Değiştirme
RustDesk, Wayland'ı desteklemez. Xorg'u GNOME oturumu olarak varsayılan olarak ayarlamak için [burayı](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) kontrol edin.
## Wayland Desteği
Wayland'ın diğer pencerelere tuş vuruşu göndermek için herhangi bir API sağlamadığı görünmektedir. Bu nedenle, RustDesk daha düşük bir seviyeden, yani Linux çekirdek seviyesindeki `/dev/uinput` cihazının API'sini kullanır.
Wayland tarafı kontrol edildiğinde, aşağıdaki şekilde başlatmanız gerekir:
```bash
# uinput servisini başlatın
$ sudo rustdesk --service
$ rustdesk
```
**Uyarı**: Wayland ekran kaydı farklı arayüzler kullanır. RustDesk şu anda yalnızca org.freedesktop.portal.ScreenCast'ı destekler.
```bash
$ dbus-send --session --print-reply \
--dest=org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop \
org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.portal.ScreenCast string:version
# Desteklenmez
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
# Desteklenir
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
variant uint32 4
```
## Docker ile Derleme Nasıl Yapılır
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
docker build -t "rustdesk-builder" .
```
Ardından, uygulamayı derlemek için her seferinde 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:
```sh
target/debug/rustdesk
```
Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
```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.
## 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/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
- **[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
## Ekran Görüntüleri
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
```

9
docs/SECURITY-TR.md Normal file
View File

@@ -0,0 +1,9 @@
# Güvenlik Politikası
## Bir Güvenlik Açığı Bildirme
Projemiz için güvenliği çok önemsiyoruz. Kullanıcıların keşfettikleri herhangi bir güvenlik açığını bize bildirmelerini teşvik ediyoruz.
Eğer RustDesk projesinde bir güvenlik açığı bulursanız, lütfen info@rustdesk.com adresine sorumlu bir şekilde bildirin.
Şu an için bir hata ödül programımız bulunmamaktadır. Büyük bir sorunu çözmeye çalışan küçük bir ekibiz. Herhangi bir güvenlik açığını sorumlu bir şekilde bildirmenizi rica ederiz,
böylece tüm topluluk için güvenli bir uygulama oluşturmaya devam edebiliriz.

View File

@@ -1,7 +1,7 @@
{ {
"id": "com.rustdesk.RustDesk", "id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform", "runtime": "org.freedesktop.Platform",
"runtime-version": "21.08", "runtime-version": "23.08",
"sdk": "org.freedesktop.Sdk", "sdk": "org.freedesktop.Sdk",
"command": "rustdesk", "command": "rustdesk",
"icon": "share/icons/hicolor/scalable/apps/rustdesk.svg", "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
@@ -12,7 +12,7 @@
"name": "rustdesk", "name": "rustdesk",
"buildsystem": "simple", "buildsystem": "simple",
"build-commands": [ "build-commands": [
"bsdtar -zxvf rustdesk-1.2.2.deb", "bsdtar -zxvf rustdesk-1.2.3.deb",
"tar -xvf ./data.tar.xz", "tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/", "cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
@@ -26,7 +26,7 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"path": "../rustdesk-1.2.2.deb" "path": "../rustdesk-1.2.3.deb"
}, },
{ {
"type": "file", "type": "file",

View File

@@ -46,7 +46,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb" applicationId "com.carriez.flutter_hbb"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@@ -26,6 +26,13 @@ const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331 const val WHEEL_DOWN = 523331
const val WHEEL_UP = 963 const val WHEEL_UP = 963
const val TOUCH_SCALE_START = 1
const val TOUCH_SCALE = 2
const val TOUCH_SCALE_END = 3
const val TOUCH_PAN_START = 4
const val TOUCH_PAN_UPDATE = 5
const val TOUCH_PAN_END = 6
const val WHEEL_STEP = 120 const val WHEEL_STEP = 120
const val WHEEL_DURATION = 50L const val WHEEL_DURATION = 50L
const val LONG_TAP_DELAY = 200L const val LONG_TAP_DELAY = 200L
@@ -167,6 +174,30 @@ class InputService : AccessibilityService() {
} }
} }
@RequiresApi(Build.VERSION_CODES.N)
fun onTouchInput(mask: Int, _x: Int, _y: Int) {
when (mask) {
TOUCH_PAN_UPDATE -> {
mouseX -= _x * SCREEN_INFO.scale
mouseY -= _y * SCREEN_INFO.scale
mouseX = max(0, mouseX);
mouseY = max(0, mouseY);
continueGesture(mouseX, mouseY)
}
TOUCH_PAN_START -> {
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
startGesture(mouseX, mouseY)
}
TOUCH_PAN_END -> {
endGesture(mouseX, mouseY)
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
}
else -> {}
}
}
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun consumeWheelActions() { private fun consumeWheelActions() {
if (isWheelActionsPolling) { if (isWheelActionsPolling) {

View File

@@ -71,17 +71,26 @@ class MainService : Service() {
@Keep @Keep
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun rustMouseInput(mask: Int, x: Int, y: Int) { fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off // turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && mask == LIFT_DOWN) { if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
Log.d(logTag,"Turn on Screen, WakeLock release") Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release() wakeLock.release()
} }
Log.d(logTag,"Turn on Screen") Log.d(logTag,"Turn on Screen")
wakeLock.acquire(5000) wakeLock.acquire(5000)
} else { } else {
InputService.ctx?.onMouseInput(mask,x,y) when (kind) {
"touch" -> {
InputService.ctx?.onTouchInput(mask, x, y)
}
"mouse" -> {
InputService.ctx?.onMouseInput(mask, x, y)
}
else -> {
}
}
} }
} }

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="236" preserveAspectRatio="xMidYMid"><path fill="#e24329" d="m128.075 236.075 47.104-144.97H80.97z"/><path fill="#fc6d26" d="M128.075 236.074 80.97 91.104H14.956z"/><path fill="#fca326" d="M14.956 91.104.642 135.16a9.752 9.752 0 0 0 3.542 10.903l123.891 90.012z"/><path fill="#e24329" d="M14.956 91.105H80.97L52.601 3.79c-1.46-4.493-7.816-4.492-9.275 0z"/><path fill="#fc6d26" d="m128.075 236.074 47.104-144.97h66.015z"/><path fill="#fca326" d="m241.194 91.104 14.314 44.056a9.752 9.752 0 0 1-3.543 10.903l-123.89 90.012z"/><path fill="#e24329" d="M241.194 91.105h-66.015l28.37-87.315c1.46-4.493 7.816-4.492 9.275 0z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696255389449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1922" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M435.2 704c-9 0-17.8-3.8-23.8-10.6l-115.2-128c-11.8-13.2-10.8-33.4 2.4-45.2 13.2-11.8 33.4-10.8 45.2 2.4l90.6 100.6 245.2-291.8c11.4-13.6 31.6-15.2 45-4 13.6 11.4 15.2 31.6 4 45l-268.8 320c-6 7-14.6 11.2-24 11.4-0.2 0.2-0.4 0.2-0.6 0.2z" p-id="1923"></path><path d="M800 928H224c-70.6 0-128-57.4-128-128V224c0-70.6 57.4-128 128-128h576c70.6 0 128 57.4 128 128v576c0 70.6-57.4 128-128 128zM224 160c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64H224z" p-id="1924"></path></svg>

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696245886035" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4133" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 132.717714c-9.435429 0-18.852571 3.84-29.147429 12.434286L194.011429 379.574857c-7.277714 6.418286-11.556571 15.433143-11.556572 28.288 0 22.272 16.713143 39.003429 39.424 39.003429 8.996571 0 18.432-3.437714 28.288-11.154286L512 222.281143l261.851429 213.430857c9.874286 7.716571 19.291429 11.154286 28.708571 11.154286 22.308571 0 39.003429-16.731429 39.003429-39.003429 0-12.854857-4.278857-21.869714-11.556572-28.288L541.147429 144.713143c-10.294857-8.137143-19.291429-11.995429-29.147429-11.995429z m0 758.564572c9.856 0 18.852571-3.84 29.147429-11.995429L829.988571 644.425143c7.277714-6.418286 11.556571-15.433143 11.556572-28.288 0-22.272-16.713143-39.424-38.985143-39.424-9.435429 0-18.870857 3.858286-28.708571 11.574857L512 801.718857 250.148571 588.288c-9.874286-7.716571-19.291429-11.574857-28.288-11.574857-22.710857 0-39.424 17.152-39.424 39.424 0 12.854857 4.278857 21.869714 11.556572 28.288l288.859428 234.422857c10.294857 8.594286 19.712 12.434286 29.147429 12.434286z" p-id="4134"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694049173782" class="icon" viewBox="0 0 1024 1024" width="24" height="24" fill="#fff" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="992" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M891.64 184.73H620.41c-27.41 0-54.41-7.77-77.32-22.5L428.13 87.36C402.77 71 372.91 62 342.64 62H131.95C93.5 62 62 93.5 62 132.36v759.68C62 930.91 93.5 962 131.95 962h759.68c38.86 0 70.36-31.09 70.36-69.96V255.09c0.01-38.86-31.49-70.36-70.35-70.36zM480.5 753.77c0 16.77-13.5 30.68-30.68 30.68-16.77 0-30.68-13.91-30.68-30.68V523.04l-31.91 55.64c-8.59 14.32-27.41 19.64-42.14 11.04-14.32-8.59-19.64-27.41-11.05-41.73l89.18-154.64c6.96-12.27 21.27-18 34.77-14.32 13.09 3.27 22.5 15.55 22.5 29.45v345.29z m209.04-139.5l-89.18 154.64c-5.32 9.82-15.55 15.55-26.59 15.55-2.46 0-5.32-0.41-7.77-1.23-13.5-3.68-22.91-15.55-22.91-29.46V408.5c0-16.77 13.91-30.68 30.68-30.68 17.18 0 30.68 13.91 30.68 30.68v230.73l31.91-55.64c8.59-14.73 27.41-19.64 42.14-11.05 14.73 8.6 19.64 27.01 11.04 41.73z" p-id="993"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
flutter/assets/scam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

View File

@@ -1,2 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info # https://docs.flutter.dev/deployment/ios
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# no obfuscate, because no easy to check errors
flutter build ipa --release

View File

@@ -75,7 +75,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`)
@@ -106,7 +106,7 @@ EXTERNAL SOURCES:
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios" :path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner: qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios" :path: ".symlinks/plugins/qr_code_scanner/ios"
sqflite: sqflite:
@@ -141,6 +141,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1
COCOAPODS: 1.12.0 COCOAPODS: 1.12.1

View File

@@ -208,6 +208,7 @@
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Thin Binary";
outputPaths = ( outputPaths = (
@@ -437,6 +438,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -634,6 +636,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -723,6 +726,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View File

@@ -13,9 +13,7 @@ import Flutter
} }
public func dummyMethodToEnforceBundling() { public func dummyMethodToEnforceBundling() {
get_rgba(); dummy_method_to_enforce_bundling();
// free_rgba(nil); session_get_rgba(nil);
// get_by_name("", "");
// set_by_name("", "");
} }
} }

View File

@@ -1,122 +1,122 @@
{ {
"images": [ "images" : [
{ {
"filename": "Icon-App-20x20@2x.png", "filename" : "Icon-App-20x20@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-20x20@3x.png", "filename" : "Icon-App-20x20@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29@1x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "1x", "scale" : "1x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-29x29@3x.png", "filename" : "Icon-App-29x29@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-40x40@2x.png", "filename" : "Icon-App-40x40@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-40x40@3x.png", "filename" : "Icon-App-40x40@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-60x60@2x.png", "filename" : "Icon-App-60x60@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "60x60" "size" : "60x60"
}, },
{ {
"filename": "Icon-App-60x60@3x.png", "filename" : "Icon-App-60x60@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "60x60" "size" : "60x60"
}, },
{ {
"filename": "Icon-App-20x20@1x.png", "filename" : "Icon-App-20x20@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-20x20@2x.png", "filename" : "Icon-App-20x20@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-40x40@1x.png", "filename" : "Icon-App-40x40@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-40x40@2x.png", "filename" : "Icon-App-40x40@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-76x76@1x.png", "filename" : "Icon-App-76x76@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "76x76" "size" : "76x76"
}, },
{ {
"filename": "Icon-App-76x76@2x.png", "filename" : "Icon-App-76x76@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "76x76" "size" : "76x76"
}, },
{ {
"filename": "Icon-App-83.5x83.5@2x.png", "filename" : "Icon-App-83.5x83.5@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "83.5x83.5" "size" : "83.5x83.5"
}, },
{ {
"filename": "Icon-App-1024x1024@1x.png", "filename" : "Icon-App-1024x1024@1x.png",
"idiom": "ios-marketing", "idiom" : "ios-marketing",
"scale": "1x", "scale" : "1x",
"size": "1024x1024" "size" : "1024x1024"
} }
], ],
"info": { "info" : {
"author": "icons_launcher", "author" : "xcode",
"version": 1 "version" : 1
} }
} }

View File

@@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="48" y="-2"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

View File

@@ -1,3 +1,3 @@
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import "ffi.h" #import "bridge_generated.h"

View File

@@ -1,4 +0,0 @@
void* get_rgba();
void free_rgba(void*);
void set_by_name(const char*, const char*);
const char* get_by_name(const char*, const char*);

View File

@@ -91,7 +91,6 @@ class IconFont {
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook = static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook"); IconData(0xe602, fontFamily: "AddressBook");
static const IconData checkbox = IconData(0xe7d6, fontFamily: "CheckBox");
} }
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
@@ -101,6 +100,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
required this.highlight, required this.highlight,
required this.drag_indicator, required this.drag_indicator,
required this.shadow, required this.shadow,
required this.errorBannerBg,
required this.me,
}); });
final Color? border; final Color? border;
@@ -108,6 +109,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
final Color? highlight; final Color? highlight;
final Color? drag_indicator; final Color? drag_indicator;
final Color? shadow; final Color? shadow;
final Color? errorBannerBg;
final Color? me;
static final light = ColorThemeExtension( static final light = ColorThemeExtension(
border: Color(0xFFCCCCCC), border: Color(0xFFCCCCCC),
@@ -115,6 +118,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: Color(0xFFE5E5E5), highlight: Color(0xFFE5E5E5),
drag_indicator: Colors.grey[800], drag_indicator: Colors.grey[800],
shadow: Colors.black, shadow: Colors.black,
errorBannerBg: Color(0xFFFDEEEB),
me: Colors.green,
); );
static final dark = ColorThemeExtension( static final dark = ColorThemeExtension(
@@ -123,6 +128,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: Color(0xFF3F3F3F), highlight: Color(0xFF3F3F3F),
drag_indicator: Colors.grey, drag_indicator: Colors.grey,
shadow: Colors.grey, shadow: Colors.grey,
errorBannerBg: Color(0xFF470F2D),
me: Colors.greenAccent,
); );
@override @override
@@ -132,6 +139,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
Color? highlight, Color? highlight,
Color? drag_indicator, Color? drag_indicator,
Color? shadow, Color? shadow,
Color? errorBannerBg,
Color? me,
}) { }) {
return ColorThemeExtension( return ColorThemeExtension(
border: border ?? this.border, border: border ?? this.border,
@@ -139,6 +148,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: highlight ?? this.highlight, highlight: highlight ?? this.highlight,
drag_indicator: drag_indicator ?? this.drag_indicator, drag_indicator: drag_indicator ?? this.drag_indicator,
shadow: shadow ?? this.shadow, shadow: shadow ?? this.shadow,
errorBannerBg: errorBannerBg ?? this.errorBannerBg,
me: me ?? this.me,
); );
} }
@@ -154,6 +165,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: Color.lerp(highlight, other.highlight, t), highlight: Color.lerp(highlight, other.highlight, t),
drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t), drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
shadow: Color.lerp(shadow, other.shadow, t), shadow: Color.lerp(shadow, other.shadow, t),
errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t),
me: Color.lerp(shadow, other.me, t),
); );
} }
} }
@@ -258,6 +271,32 @@ class MyTheme {
? EdgeInsets.only(left: dialogPadding) ? EdgeInsets.only(left: dialogPadding)
: EdgeInsets.only(left: dialogPadding / 3); : EdgeInsets.only(left: dialogPadding / 3);
static ScrollbarThemeData scrollbarTheme = ScrollbarThemeData(
thickness: MaterialStateProperty.all(6),
thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.dragged)) {
return Colors.grey[900];
} else if (states.contains(MaterialState.hovered)) {
return Colors.grey[700];
} else {
return Colors.grey[500];
}
}),
crossAxisMargin: 4,
);
static ScrollbarThemeData scrollbarThemeDark = scrollbarTheme.copyWith(
thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.dragged)) {
return Colors.grey[100];
} else if (states.contains(MaterialState.hovered)) {
return Colors.grey[300];
} else {
return Colors.grey[500];
}
}),
);
static ThemeData lightTheme = ThemeData( static ThemeData lightTheme = ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
hoverColor: Color.fromARGB(255, 224, 224, 224), hoverColor: Color.fromARGB(255, 224, 224, 224),
@@ -273,6 +312,7 @@ class MyTheme {
), ),
), ),
), ),
scrollbarTheme: scrollbarTheme,
inputDecorationTheme: isDesktop inputDecorationTheme: isDesktop
? InputDecorationTheme( ? InputDecorationTheme(
fillColor: grayBg, fillColor: grayBg,
@@ -357,6 +397,7 @@ class MyTheme {
), ),
), ),
), ),
scrollbarTheme: scrollbarThemeDark,
inputDecorationTheme: isDesktop inputDecorationTheme: isDesktop
? InputDecorationTheme( ? InputDecorationTheme(
fillColor: Color(0xFF24252B), fillColor: Color(0xFF24252B),
@@ -383,9 +424,6 @@ class MyTheme {
tabBarTheme: const TabBarTheme( tabBarTheme: const TabBarTheme(
labelColor: Colors.white70, labelColor: Colors.white70,
), ),
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
),
tooltipTheme: tooltipTheme(), tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null, splashColor: isDesktop ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null, highlightColor: isDesktop ? Colors.transparent : null,
@@ -555,7 +593,7 @@ closeConnection({String? id}) {
} }
} }
void windowOnTop(int? id) async { Future<void> windowOnTop(int? id) async {
if (!isDesktop) { if (!isDesktop) {
return; return;
} }
@@ -614,6 +652,7 @@ class OverlayDialogManager {
int _tagCount = 0; int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry; OverlayEntry? _mobileActionsOverlayEntry;
RxBool mobileActionsOverlayVisible = false.obs;
void setOverlayState(OverlayKeyState overlayKeyState) { void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState; _overlayKeyState = overlayKeyState;
@@ -780,12 +819,14 @@ class OverlayDialogManager {
}); });
overlayState.insert(overlay); overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay; _mobileActionsOverlayEntry = overlay;
mobileActionsOverlayVisible.value = true;
} }
void hideMobileActionsOverlay() { void hideMobileActionsOverlay() {
if (_mobileActionsOverlayEntry != null) { if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove(); _mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null; _mobileActionsOverlayEntry = null;
mobileActionsOverlayVisible.value = false;
return; return;
} }
} }
@@ -954,11 +995,22 @@ void msgBox(SessionID sessionId, String type, String title, String text,
})); }));
} }
if (reconnect != null && title == "Connection Error") { if (reconnect != null && title == "Connection Error") {
buttons.insert( // `enabled` is used to disable the dialog button once the button is clicked.
0, final enabled = true.obs;
dialogButton('Reconnect', isOutline: true, onPressed: () { final button = Obx(
reconnect(dialogManager, sessionId, false); () => dialogButton(
})); 'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button);
} }
if (link.isNotEmpty) { if (link.isNotEmpty) {
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
@@ -1077,7 +1129,7 @@ Color str2color(String str, [alpha = 0xFF]) {
return Color((hash & 0xFF7FFF) | (alpha << 24)); return Color((hash & 0xFF7FFF) | (alpha << 24));
} }
Color str2color2(String str, [alpha = 0xFF]) { Color str2color2(String str, {List<int> existing = const []}) {
Map<String, Color> colorMap = { Map<String, Color> colorMap = {
"red": Colors.red, "red": Colors.red,
"green": Colors.green, "green": Colors.green,
@@ -1094,10 +1146,10 @@ Color str2color2(String str, [alpha = 0xFF]) {
}; };
final color = colorMap[str.toLowerCase()]; final color = colorMap[str.toLowerCase()];
if (color != null) { if (color != null) {
return color.withAlpha(alpha); return color.withAlpha(0xFF);
} }
if (str.toLowerCase() == 'yellow') { if (str.toLowerCase() == 'yellow') {
return Colors.yellow.withAlpha(alpha); return Colors.yellow.withAlpha(0xFF);
} }
var hash = 0; var hash = 0;
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
@@ -1105,7 +1157,15 @@ Color str2color2(String str, [alpha = 0xFF]) {
} }
List<Color> colorList = colorMap.values.toList(); List<Color> colorList = colorMap.values.toList();
hash = hash % colorList.length; hash = hash % colorList.length;
return colorList[hash].withAlpha(alpha); var result = colorList[hash].withAlpha(0xFF);
if (existing.contains(result.value)) {
Color? notUsed =
colorList.firstWhereOrNull((e) => !existing.contains(e.value));
if (notUsed != null) {
result = notUsed;
}
}
return result;
} }
const K = 1024; const K = 1024;
@@ -1381,9 +1441,10 @@ class LastWindowPosition {
double? offsetWidth; double? offsetWidth;
double? offsetHeight; double? offsetHeight;
bool? isMaximized; bool? isMaximized;
bool? isFullscreen;
LastWindowPosition(this.width, this.height, this.offsetWidth, LastWindowPosition(this.width, this.height, this.offsetWidth,
this.offsetHeight, this.isMaximized); this.offsetHeight, this.isMaximized, this.isFullscreen);
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
@@ -1392,6 +1453,7 @@ class LastWindowPosition {
"offsetWidth": offsetWidth, "offsetWidth": offsetWidth,
"offsetHeight": offsetHeight, "offsetHeight": offsetHeight,
"isMaximized": isMaximized, "isMaximized": isMaximized,
"isFullscreen": isFullscreen,
}; };
} }
@@ -1407,7 +1469,7 @@ class LastWindowPosition {
try { try {
final m = jsonDecode(content); final m = jsonDecode(content);
return LastWindowPosition(m["width"], m["height"], m["offsetWidth"], return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
m["offsetHeight"], m["isMaximized"]); m["offsetHeight"], m["isMaximized"], m["isFullscreen"]);
} catch (e) { } catch (e) {
debugPrintStack( debugPrintStack(
label: label:
@@ -1428,6 +1490,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
late Offset position; late Offset position;
late Size sz; late Size sz;
late bool isMaximized; late bool isMaximized;
bool isFullscreen = stateGlobal.fullscreen ||
(Platform.isMacOS && stateGlobal.closeOnFullscreen);
setFrameIfMaximized() { setFrameIfMaximized() {
if (isMaximized) { if (isMaximized) {
final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name); final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
@@ -1473,20 +1537,21 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
} }
final pos = LastWindowPosition( final pos = LastWindowPosition(
sz.width, sz.height, position.dx, position.dy, isMaximized); sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
debugPrint( debugPrint(
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}"); "Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
await bind.setLocalFlutterOption( await bind.setLocalFlutterOption(
k: kWindowPrefix + type.name, v: pos.toString()); k: kWindowPrefix + type.name, v: pos.toString());
if (type == WindowType.RemoteDesktop && windowId != null) { if (type == WindowType.RemoteDesktop && windowId != null) {
await _saveSessionWindowPosition(type, windowId, isMaximized, pos); await _saveSessionWindowPosition(
type, windowId, isMaximized, isFullscreen, pos);
} }
} }
Future _saveSessionWindowPosition(WindowType windowType, int windowId, Future _saveSessionWindowPosition(WindowType windowType, int windowId,
bool isMaximized, LastWindowPosition pos) async { bool isMaximized, bool isFullscreen, LastWindowPosition pos) async {
final remoteList = await DesktopMultiWindow.invokeMethod( final remoteList = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventGetRemoteList, null); windowId, kWindowEventGetRemoteList, null);
getPeerPos(String peerId) { getPeerPos(String peerId) {
@@ -1499,7 +1564,8 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
lpos?.height ?? pos.offsetHeight, lpos?.height ?? pos.offsetHeight,
lpos?.offsetWidth ?? pos.offsetWidth, lpos?.offsetWidth ?? pos.offsetWidth,
lpos?.offsetHeight ?? pos.offsetHeight, lpos?.offsetHeight ?? pos.offsetHeight,
isMaximized) isMaximized,
isFullscreen)
.toString(); .toString();
} else { } else {
return pos.toString(); return pos.toString();
@@ -1689,9 +1755,18 @@ Future<bool> restoreWindowPosition(WindowType type,
await wc.setFrame(frame); await wc.setFrame(frame);
} }
} }
if (lpos.isMaximized == true) { if (lpos.isFullscreen == true) {
await restoreFrame(); await restoreFrame();
await wc.maximize(); // An duration is needed to avoid the window being restored after fullscreen.
Future.delayed(Duration(milliseconds: 300), () async {
stateGlobal.setFullscreen(true);
});
} else if (lpos.isMaximized == true) {
await restoreFrame();
// An duration is needed to avoid the window being restored after maximized.
Future.delayed(Duration(milliseconds: 300), () async {
await wc.maximize();
});
} else { } else {
await restoreFrame(); await restoreFrame();
} }
@@ -1759,10 +1834,10 @@ enum UriLinkType {
// uri link handler // uri link handler
bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) { bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
List<String>? args; List<String>? args;
if (cmdArgs != null) { if (cmdArgs != null && cmdArgs.isNotEmpty) {
args = cmdArgs; args = cmdArgs;
// rustdesk <uri link> // rustdesk <uri link>
if (args.isNotEmpty && args[0].startsWith(kUniLinksPrefix)) { if (args[0].startsWith(kUniLinksPrefix)) {
final uri = Uri.tryParse(args[0]); final uri = Uri.tryParse(args[0]);
if (uri != null) { if (uri != null) {
args = urlLinkToCmdArgs(uri); args = urlLinkToCmdArgs(uri);
@@ -2263,7 +2338,7 @@ String getWindowName({WindowType? overrideType}) {
} }
String getWindowNameWithId(String id, {WindowType? overrideType}) { String getWindowNameWithId(String id, {WindowType? overrideType}) {
return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; return "${DesktopTab.tablabelGetter(id).value} - ${getWindowName(overrideType: overrideType)}";
} }
Future<void> updateSystemWindowTheme() async { Future<void> updateSystemWindowTheme() async {
@@ -2440,3 +2515,77 @@ String toCapitalized(String s) {
} }
return s.substring(0, 1).toUpperCase() + s.substring(1); return s.substring(0, 1).toUpperCase() + s.substring(1);
} }
Widget buildErrorBanner(BuildContext context,
{required RxBool loading,
required RxString err,
required Function? retry,
required Function close}) {
const double height = 25;
return Obx(() => Offstage(
offstage: !(!loading.value && err.value.isNotEmpty),
child: Center(
child: Container(
height: height,
color: MyTheme.color(context).errorBannerBg,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FittedBox(
child: Icon(
Icons.info,
color: Color.fromARGB(255, 249, 81, 81),
),
).marginAll(4),
Flexible(
child: Align(
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
if (retry != null)
InkWell(
onTap: () {
retry.call();
},
child: Text(
translate("Retry"),
style: TextStyle(color: MyTheme.accent),
)).marginSymmetric(horizontal: 5),
FittedBox(
child: InkWell(
onTap: () {
close.call();
},
child: Icon(Icons.close).marginSymmetric(horizontal: 5),
),
).marginAll(4)
],
),
)).marginOnly(bottom: 14),
));
}
String getDesktopTabLabel(String peerId, String alias) {
String label = alias.isEmpty ? peerId : alias;
try {
String peer = bind.mainGetPeerSync(id: peerId);
Map<String, dynamic> config = jsonDecode(peer);
if (config['info']['hostname'] is String) {
String hostname = config['info']['hostname'];
if (hostname.isNotEmpty &&
!label.toLowerCase().contains(hostname.toLowerCase())) {
label += "@$hostname";
}
}
} catch (e) {
debugPrint("Failed to get hostname:$e");
}
return label;
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
@@ -48,11 +49,18 @@ class UserPayload {
}; };
return map; return map;
} }
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
};
return map;
}
} }
class PeerPayload { class PeerPayload {
String id = ''; String id = '';
String info = ''; Map<String, dynamic> info = {};
int? status; int? status;
String user = ''; String user = '';
String user_name = ''; String user_name = '';
@@ -60,14 +68,45 @@ class PeerPayload {
PeerPayload.fromJson(Map<String, dynamic> json) PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '', : id = json['id'] ?? '',
info = json['info'] ?? '', info = (json['info'] is Map<String, dynamic>) ? json['info'] : {},
status = json['status'], status = json['status'],
user = json['user'] ?? '', user = json['user'] ?? '',
user_name = json['user_name'] ?? '', user_name = json['user_name'] ?? '',
note = json['note'] ?? ''; note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) { static Peer toPeer(PeerPayload p) {
return Peer.fromJson({"id": p.id, "username": p.user_name}); return Peer.fromJson({
"id": p.id,
'loginName': p.user_name,
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
});
}
static String? _platform(dynamic field) {
if (field == null) {
return null;
}
final fieldStr = field.toString();
List<String> list = fieldStr.split(' / ');
if (list.isEmpty) return null;
final os = list[0];
switch (os.toLowerCase()) {
case 'windows':
return kPeerPlatformWindows;
case 'linux':
return kPeerPlatformLinux;
case 'macos':
return kPeerPlatformMacOS;
case 'android':
return kPeerPlatformAndroid;
default:
if (fieldStr.toLowerCase().contains('linux')) {
return kPeerPlatformLinux;
}
return null;
}
} }
} }

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
@@ -7,6 +10,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import '../../common.dart'; import '../../common.dart';
import 'dialog.dart'; import 'dialog.dart';
@@ -34,7 +38,7 @@ class _AddressBookState extends State<AddressBook> {
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
if (gFFI.userModel.userName.value.isEmpty) { if (!gFFI.userModel.isLogin) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
@@ -48,11 +52,13 @@ class _AddressBookState extends State<AddressBook> {
children: [ children: [
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
if (gFFI.abModel.retrying.value) LinearProgressIndicator(), if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
_buildErrorBanner( buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pullError, err: gFFI.abModel.pullError,
retry: null, retry: null,
close: () => gFFI.abModel.pullError.value = ''), close: () => gFFI.abModel.pullError.value = ''),
_buildErrorBanner( buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pushError, err: gFFI.abModel.pushError,
retry: () => gFFI.abModel.pushAb(isRetry: true), retry: () => gFFI.abModel.pushAb(isRetry: true),
close: () => gFFI.abModel.pushError.value = ''), close: () => gFFI.abModel.pushError.value = ''),
@@ -65,61 +71,6 @@ class _AddressBookState extends State<AddressBook> {
} }
}); });
Widget _buildErrorBanner(
{required RxString err,
required Function? retry,
required Function close}) {
const double height = 25;
return Obx(() => Offstage(
offstage: !(!gFFI.abModel.abLoading.value && err.value.isNotEmpty),
child: Center(
child: Container(
height: height,
color: Color.fromARGB(255, 253, 238, 235),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FittedBox(
child: Icon(
Icons.info,
color: Color.fromARGB(255, 249, 81, 81),
),
).marginAll(4),
Flexible(
child: Align(
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
if (retry != null)
InkWell(
onTap: () {
retry.call();
},
child: Text(
translate("Retry"),
style: TextStyle(color: MyTheme.accent),
)).marginSymmetric(horizontal: 5),
FittedBox(
child: InkWell(
onTap: () {
close.call();
},
child: Icon(Icons.close).marginSymmetric(horizontal: 5),
),
).marginAll(4)
],
),
)).marginOnly(bottom: 14),
));
}
Widget _buildAddressBookDesktop() { Widget _buildAddressBookDesktop() {
return Row( return Row(
children: [ children: [
@@ -208,20 +159,31 @@ class _AddressBookState extends State<AddressBook> {
} else { } else {
tags = gFFI.abModel.tags; tags = gFFI.abModel.tags;
} }
return Wrap( tagBuilder(String e) {
children: tags return AddressBookTag(
.map((e) => AddressBookTag( name: e,
name: e, tags: gFFI.abModel.selectedTags,
tags: gFFI.abModel.selectedTags, onTap: () {
onTap: () { if (gFFI.abModel.selectedTags.contains(e)) {
if (gFFI.abModel.selectedTags.contains(e)) { gFFI.abModel.selectedTags.remove(e);
gFFI.abModel.selectedTags.remove(e); } else {
} else { gFFI.abModel.selectedTags.add(e);
gFFI.abModel.selectedTags.add(e); }
} });
})) }
.toList(),
); final gridView = DynamicGridView.builder(
shrinkWrap: isMobile,
gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length,
itemBuilder: (BuildContext context, int index) {
final e = tags[index];
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView);
}); });
} }
@@ -229,11 +191,10 @@ class _AddressBookState extends State<AddressBook> {
return Expanded( return Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.abModel.peers,
initPeers: gFFI.abModel.peers.value, )),
))),
); );
} }
@@ -268,6 +229,22 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
@protected
MenuEntryBase<String> filterMenuItem() {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate('Filter by intersection'),
getter: () async {
return filterAbTagByIntersection();
},
setter: (bool v) async {
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
gFFI.abModel.filterByIntersection.value = v;
},
dismissOnClicked: true,
);
}
void _showMenu(RelativeRect pos) { void _showMenu(RelativeRect pos) {
final items = [ final items = [
getEntry(translate("Add ID"), abAddId), getEntry(translate("Add ID"), abAddId),
@@ -275,6 +252,7 @@ class _AddressBookState extends State<AddressBook> {
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(), sortMenuItem(),
syncMenuItem(), syncMenuItem(),
filterMenuItem(),
]; ];
mod_menu.showMenu( mod_menu.showMenu(
@@ -513,7 +491,7 @@ class AddressBookTag extends StatelessWidget {
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: tags.contains(name) color: tags.contains(name)
? str2color2(name, 0xFF) ? gFFI.abModel.getTagColor(name)
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(4)), borderRadius: BorderRadius.circular(4)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
@@ -528,7 +506,7 @@ class AddressBookTag extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: tags.contains(name) color: tags.contains(name)
? Colors.white ? Colors.white
: str2color2(name)), : gFFI.abModel.getTagColor(name)),
).marginOnly(right: radius / 2), ).marginOnly(right: radius / 2),
Expanded( Expanded(
child: Text(name, child: Text(name,
@@ -568,6 +546,30 @@ class AddressBookTag extends StatelessWidget {
Future.delayed(Duration.zero, () => Get.back()); Future.delayed(Duration.zero, () => Get.back());
}); });
}), }),
getEntry(translate(translate('Change Color')), () async {
final model = gFFI.abModel;
Color oldColor = model.getTagColor(name);
Color newColor = await showColorPickerDialog(
context,
oldColor,
pickersEnabled: {
ColorPickerType.accent: false,
ColorPickerType.wheel: true,
},
pickerTypeLabels: {
ColorPickerType.primary: translate("Primary Color"),
ColorPickerType.wheel: translate("HSV Color"),
},
actionButtons: ColorPickerActionButtons(
dialogOkButtonLabel: translate("OK"),
dialogCancelButtonLabel: translate("Cancel")),
showColorCode: true,
);
if (oldColor != newColor) {
model.setTagColor(name, newColor);
model.pushAb();
}
}),
getEntry(translate("Delete"), () { getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name); gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb(); gFFI.abModel.pushAb();

View File

@@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@@ -302,6 +302,53 @@ Future<String> changeDirectAccessPort(
return controller.text; return controller.text;
} }
Future<String> changeAutoDisconnectTimeout(String old) async {
final controller = TextEditingController(text: old);
await gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("Timeout in minutes")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8.0),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '10',
isCollapsed: true,
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
autofocus: true),
),
],
),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async {
await bind.mainSetOption(
key: 'auto-disconnect-timeout', value: controller.text);
close();
}),
],
onCancel: close,
);
});
return controller.text;
}
class DialogTextField extends StatelessWidget { class DialogTextField extends StatelessWidget {
final String title; final String title;
final String? hintText; final String? hintText;
@@ -664,6 +711,13 @@ void showWaitUacDialog(
(setState, close, context) => CustomAlertDialog( (setState, close, context) => CustomAlertDialog(
title: null, title: null,
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
actions: [
dialogButton(
'OK',
icon: Icon(Icons.done_rounded),
onPressed: close,
),
],
)); ));
} }
@@ -812,6 +866,8 @@ void showRequestElevationDialog(
} else { } else {
bind.sessionElevateDirect(sessionId: sessionId); bind.sessionElevateDirect(sessionId: sessionId);
} }
close();
showWaitUacDialog(sessionId, dialogManager, "wait-uac");
} }
return CustomAlertDialog( return CustomAlertDialog(
@@ -882,7 +938,7 @@ void showElevationError(SessionID sessionId, String type, String title,
dialogButton('Cancel', onPressed: () { dialogButton('Cancel', onPressed: () {
close(); close();
}, isOutline: true), }, isOutline: true),
dialogButton('Retry', onPressed: submit), if (text != 'No permission') dialogButton('Retry', onPressed: submit),
], ],
onSubmit: submit, onSubmit: submit,
onCancel: close, onCancel: close,
@@ -1223,76 +1279,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId); final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
qualityInitValue = qualityInitValue =
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
const qualityMinValue = 10.0; if (qualityInitValue < 10 || qualityInitValue > 2000) {
const qualityMoreThresholdValue = 100.0; qualityInitValue = 50;
const qualityMaxValue = 2000.0;
if (qualityInitValue < qualityMinValue) {
qualityInitValue = qualityMinValue;
} }
if (qualityInitValue > qualityMaxValue) {
qualityInitValue = qualityMaxValue;
}
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
final moreQualityInitValue = qualityInitValue > qualityMoreThresholdValue;
final RxBool moreQualityChecked = RxBool(moreQualityInitValue);
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(quality: v);
},
initialValue: qualityInitValue,
);
final qualitySlider = Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: qualitySliderValue.value,
min: qualityMinValue,
max: moreQualityChecked.value
? qualityMaxValue
: qualityMoreThresholdValue,
divisions: 18,
onChanged: (double value) {
qualitySliderValue.value = value;
debouncerQuality.value = value;
},
)),
Expanded(
flex: 1,
child: Text(
'${qualitySliderValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 1,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 1,
child: Row(
children: [
Checkbox(
value: moreQualityChecked.value,
onChanged: (bool? value) {
moreQualityChecked.value = value!;
if (!value &&
qualitySliderValue.value >
qualityMoreThresholdValue) {
qualitySliderValue.value = qualityMoreThresholdValue;
debouncerQuality.value = qualityMoreThresholdValue;
}
},
).marginOnly(right: 5),
Expanded(
child: Text(translate('More')),
)
],
)),
],
));
// fps // fps
final fpsOption = final fpsOption =
await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps'); await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
@@ -1300,55 +1289,20 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
if (fpsInitValue < 5 || fpsInitValue > 120) { if (fpsInitValue < 5 || fpsInitValue > 120) {
fpsInitValue = 30; fpsInitValue = 30;
} }
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(fps: v);
},
initialValue: qualityInitValue,
);
bool? direct; bool? direct;
try { try {
direct = direct =
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
} catch (_) {} } catch (_) {}
final fpsSlider = Offstage( bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
offstage: (await bind.mainIsUsingPublicServer() && direct != true) || version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
child: Row(
children: [
Expanded(
flex: 3,
child: Obx((() => Slider(
value: fpsSliderValue.value,
min: 5,
max: 120,
divisions: 23,
onChanged: (double value) {
fpsSliderValue.value = value;
debouncerFps.value = value;
},
)))),
Expanded(
flex: 1,
child: Obx(() => Text(
'${fpsSliderValue.value.round()}',
style: const TextStyle(fontSize: 15),
))),
Expanded(
flex: 2,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
),
);
final content = Column( final content = customImageQualityWidget(
children: [qualitySlider, fpsSlider], initQuality: qualityInitValue,
); initFps: fpsInitValue,
setQuality: (v) => setCustomValues(quality: v),
setFps: (v) => setCustomValues(fps: v),
showFps: !notShowFps);
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
} }

View File

@@ -14,6 +14,7 @@ import './dialog.dart';
const kOpSvgList = [ const kOpSvgList = [
'github', 'github',
'gitlab',
'google', 'google',
'apple', 'apple',
'okta', 'okta',
@@ -72,6 +73,11 @@ class ButtonOP extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final opLabel = {
'github': 'GitHub',
'gitlab': 'GitLab'
}[op.toLowerCase()] ??
toCapitalized(op);
return Row(children: [ return Row(children: [
Container( Container(
height: height, height: height,
@@ -97,8 +103,7 @@ class ButtonOP extends StatelessWidget {
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Center( child: Center(
child: Text( child: Text('${translate("Continue with")} $opLabel')),
'${translate("Continue with")} ${op.toLowerCase() == "github" ? "GitHub" : toCapitalized(op)}')),
), ),
), ),
], ],

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
@@ -29,49 +31,28 @@ class _MyGroupState extends State<MyGroup> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
// use username to be same with ab if (!gFFI.userModel.isLogin) {
if (gFFI.userModel.userName.value.isEmpty) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return buildBody(context);
});
}
Widget buildBody(BuildContext context) {
return Obx(() {
if (gFFI.groupModel.groupLoading.value) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
if (gFFI.groupModel.groupLoadError.isNotEmpty) { return Column(
return _buildShowError(gFFI.groupModel.groupLoadError.value); children: [
} buildErrorBanner(context,
if (isDesktop) { loading: gFFI.groupModel.groupLoading,
return _buildDesktop(); err: gFFI.groupModel.groupLoadError,
} else { retry: null,
return _buildMobile(); close: () => gFFI.groupModel.groupLoadError.value = ''),
} Expanded(child: isDesktop ? _buildDesktop() : _buildMobile())
],
);
}); });
} }
Widget _buildShowError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(error)),
TextButton(
onPressed: () {
gFFI.groupModel.pull();
},
child: Text(translate("Retry")))
],
));
}
Widget _buildDesktop() { Widget _buildDesktop() {
return Row( return Row(
children: [ children: [
@@ -100,10 +81,9 @@ class _MyGroupState extends State<MyGroup> {
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.groupModel.peers)),
initPeers: gFFI.groupModel.peersShow.value))),
) )
], ],
); );
@@ -133,16 +113,16 @@ class _MyGroupState extends State<MyGroup> {
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.groupModel.peers)),
initPeers: gFFI.groupModel.peersShow.value))),
) )
], ],
); );
} }
Widget _buildLeftHeader() { Widget _buildLeftHeader() {
final fontSize = 14.0;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
@@ -151,16 +131,16 @@ class _MyGroupState extends State<MyGroup> {
onChanged: (value) { onChanged: (value) {
searchUserText.value = value; searchUserText.value = value;
}, },
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration( decoration: InputDecoration(
filled: false, filled: false,
prefixIcon: Icon( prefixIcon: Icon(
Icons.search_rounded, Icons.search_rounded,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ).paddingOnly(top: 2),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
hintText: translate("Search"), hintText: translate("Search"),
hintStyle: hintStyle: TextStyle(fontSize: fontSize),
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
), ),
@@ -171,16 +151,22 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserContacts() { Widget _buildUserContacts() {
return Obx(() { return Obx(() {
return Column( final items = gFFI.groupModel.users.where((p0) {
children: gFFI.groupModel.users if (searchUserText.isNotEmpty) {
.where((p0) { return p0.name
if (searchUserText.isNotEmpty) { .toLowerCase()
return p0.name.contains(searchUserText.value); .contains(searchUserText.value.toLowerCase());
} }
return true; return true;
}) }).toList();
.map((e) => _buildUserItem(e)) final listView = ListView.builder(
.toList()); shrinkWrap: isMobile,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? listView
: LimitedBox(maxHeight: maxHeight, child: listView);
}); });
} }
@@ -195,6 +181,8 @@ class _MyGroupState extends State<MyGroup> {
}, child: Obx( }, child: Obx(
() { () {
bool selected = selectedUser.value == username; bool selected = selectedUser.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null, color: selected ? MyTheme.color(context).highlight : null,
@@ -206,9 +194,42 @@ class _MyGroupState extends State<MyGroup> {
child: Container( child: Container(
child: Row( child: Row(
children: [ children: [
Icon(Icons.person_rounded, color: Colors.grey, size: 16) Container(
.marginOnly(right: 4), width: 20,
Expanded(child: Text(username)), height: 20,
decoration: BoxDecoration(
color: str2color(username, 0xAF),
shape: BoxShape.circle,
),
child: Align(
alignment: Alignment.center,
child: Center(
child: Text(
username.characters.first.toUpperCase(),
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
).marginOnly(right: 4),
if (isMe) Flexible(child: Text(username)),
if (isMe)
Flexible(
child: Container(
margin: EdgeInsets.only(left: 5),
padding: EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: colorMe.withAlpha(20),
borderRadius: BorderRadius.all(Radius.circular(2)),
border: Border.all(color: colorMe.withAlpha(100))),
child: Text(
translate('Me'),
style: TextStyle(
color: colorMe.withAlpha(200), fontSize: 12),
),
),
),
if (!isMe) Expanded(child: Text(username)),
], ],
).paddingSymmetric(vertical: 4), ).paddingSymmetric(vertical: 4),
), ),

View File

@@ -26,15 +26,32 @@ class DraggableChatWindow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Draggable( return isIOS
? IOSDraggable (
position: position,
chatModel: chatModel,
width: width,
height: height,
builder: (context) {
return Column(
children: [
_buildMobileAppBar(context),
Expanded(
child: ChatPage(chatModel: chatModel),
),
],
);
},
)
: Draggable(
checkKeyboard: true, checkKeyboard: true,
position: position, position: position,
width: width, width: width,
height: height, height: height,
chatModel: chatModel,
builder: (context, onPanUpdate) { builder: (context, onPanUpdate) {
final child = isIOS final child =
? ChatPage(chatModel: chatModel) Scaffold(
: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: CustomAppBar( appBar: CustomAppBar(
onPanUpdate: onPanUpdate, onPanUpdate: onPanUpdate,
@@ -226,6 +243,7 @@ class Draggable extends StatefulWidget {
this.position = Offset.zero, this.position = Offset.zero,
required this.width, required this.width,
required this.height, required this.height,
this.chatModel,
required this.builder}) required this.builder})
: super(key: key); : super(key: key);
@@ -234,6 +252,7 @@ class Draggable extends StatefulWidget {
final Offset position; final Offset position;
final double width; final double width;
final double height; final double height;
final ChatModel? chatModel;
final Widget Function(BuildContext, GestureDragUpdateCallback) builder; final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override @override
@@ -242,6 +261,7 @@ class Draggable extends StatefulWidget {
class _DraggableState extends State<Draggable> { class _DraggableState extends State<Draggable> {
late Offset _position; late Offset _position;
late ChatModel? _chatModel;
bool _keyboardVisible = false; bool _keyboardVisible = false;
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@@ -250,6 +270,7 @@ class _DraggableState extends State<Draggable> {
void initState() { void initState() {
super.initState(); super.initState();
_position = widget.position; _position = widget.position;
_chatModel = widget.chatModel;
} }
void onPanUpdate(DragUpdateDetails d) { void onPanUpdate(DragUpdateDetails d) {
@@ -276,6 +297,7 @@ class _DraggableState extends State<Draggable> {
setState(() { setState(() {
_position = Offset(x, y); _position = Offset(x, y);
}); });
_chatModel?.setChatWindowPosition(_position);
} }
checkScreenSize() {} checkScreenSize() {}
@@ -331,6 +353,107 @@ class _DraggableState extends State<Draggable> {
} }
} }
class IOSDraggable extends StatefulWidget {
const IOSDraggable({
Key? key,
this.position = Offset.zero,
this.chatModel,
required this.width,
required this.height,
required this.builder})
: super(key: key);
final Offset position;
final ChatModel? chatModel;
final double width;
final double height;
final Widget Function(BuildContext) builder;
@override
_IOSDraggableState createState() => _IOSDraggableState();
}
class _IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel;
late double _width;
late double _height;
bool _keyboardVisible = false;
double _saveHeight = 0;
double _lastBottomHeight = 0;
@override
void initState() {
super.initState();
_position = widget.position;
_chatModel = widget.chatModel;
_width = widget.width;
_height = widget.height;
}
checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0;
// save
if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy;
}
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
_position = Offset(_position.dx, _saveHeight);
});
}
// onKeyboardVisible
if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
_position = Offset(_position.dx, y);
});
}
}
_keyboardVisible = currentVisible;
_lastBottomHeight = bottomHeight;
}
@override
Widget build(BuildContext context) {
checkKeyboard();
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
});
_chatModel?.setChatWindowPosition(_position);
},
child: Material(
child:
Container(
width: _width,
height: _height,
decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
child: widget.builder(context),
),
),
),
),
],
);
}
}
class QualityMonitor extends StatelessWidget { class QualityMonitor extends StatelessWidget {
final QualityMonitorModel qualityMonitorModel; final QualityMonitorModel qualityMonitorModel;
QualityMonitor(this.qualityMonitorModel); QualityMonitor(this.qualityMonitorModel);

View File

@@ -1,11 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -63,75 +61,29 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildMobile() { Widget _buildMobile() {
final peer = super.widget.peer; final peer = super.widget.peer;
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final child = Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer); peerTabModel.select(peer);
}, } else {
child: Container( if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: Row( child: _buildPeerTile(context, peer, null)),
children: [ ));
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: BorderRadius.circular(4),
),
padding: const EdgeInsets.all(6),
child: getPlatformImage(peer.platform)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
getOnline(4, peer.online),
Text(peer.alias.isEmpty
? formatID(peer.id)
: peer.alias)
]),
Text(name)
],
).paddingOnly(left: 8.0),
),
checkBoxOrActionMoreMobile(peer),
],
),
)));
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList();
return Tooltip(
message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
if (colors.isNotEmpty)
Positioned(
top: 2,
right: 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
)
]),
);
} }
Widget _buildDesktop() { Widget _buildDesktop() {
@@ -178,87 +130,96 @@ class _PeerCardState extends State<_PeerCard>
} }
Widget _buildPeerTile( Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
final name = final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Obx( final child = Row(
() => Container( mainAxisSize: MainAxisSize.max,
foregroundDecoration: deco.value, children: [
child: Row( Container(
mainAxisSize: MainAxisSize.max, decoration: BoxDecoration(
children: [ color: str2color('${peer.id}${peer.platform}', 0x7f),
Container( borderRadius: isMobile
decoration: BoxDecoration( ? BorderRadius.circular(_tileRadius)
color: str2color('${peer.id}${peer.platform}', 0x7f), : BorderRadius.only(
borderRadius: BorderRadius.only( topLeft: Radius.circular(_tileRadius),
topLeft: Radius.circular(_tileRadius), bottomLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: 42,
child: getPlatformImage(peer.platform, size: 30).paddingAll(6),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
), ),
), ),
child: Row( alignment: Alignment.center,
children: [ width: isMobile ? 50 : 42,
Expanded( height: isMobile ? 50 : null,
child: Column( child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
children: [ .paddingAll(6),
Row(children: [
getOnline(8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty
? formatID(peer.id)
: peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(bottom: 0, top: 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
), ),
), Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(children: [
getOnline(isMobile ? 4 : 8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(top: isMobile ? 0 : 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isMobile ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
isMobile
? checkBoxOrActionMoreMobile(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
); );
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList(); final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: isMobile
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? ''
: '', : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [ child: Stack(children: [
child, deco == null
? child
: Obx(
() => Container(
foregroundDecoration: deco.value,
child: child,
),
),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Positioned(
top: 2, top: 2,
right: 10, right: isMobile ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
@@ -349,7 +310,8 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList(); final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
@@ -765,17 +727,18 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _unrememberPasswordAction(String id) { MenuEntryBase<String> _unrememberPasswordAction(String id) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text( childBuilder: (TextStyle? style) => Text(
translate('Unremember Password'), translate('Forget Password'),
style: style, style: style,
), ),
proc: () async { proc: () async {
bool result = gFFI.abModel.changePassword(id, ''); bool result = gFFI.abModel.changePassword(id, '');
await bind.mainForgetPassword(id: id); await bind.mainForgetPassword(id: id);
bool toast = false;
if (result) { if (result) {
bool toast = tab == PeerTabIndex.ab; toast = tab == PeerTabIndex.ab;
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast); gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
} }
showToast(translate('Successful')); if (!toast) showToast(translate('Successful'));
}, },
padding: menuPadding, padding: menuPadding,
dismissOnClicked: true, dismissOnClicked: true,
@@ -900,12 +863,12 @@ class RecentPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -954,12 +917,12 @@ class FavoritePeerCard extends BasePeerCard {
_connectAction(context, peer), _connectAction(context, peer),
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
]; ];
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -1008,12 +971,12 @@ class DiscoveredPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
menuItems.add(_wolAction(peer.id)); menuItems.add(_wolAction(peer.id));
@@ -1058,12 +1021,12 @@ class AddressBookPeerCard extends BasePeerCard {
_connectAction(context, peer), _connectAction(context, peer),
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
]; ];
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -1126,21 +1089,26 @@ class MyGroupPeerCard extends BasePeerCard {
_connectAction(context, peer), _connectAction(context, peer),
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
]; ];
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); // menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); // menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id)); // menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) { // if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id)); // menuItems.add(_unrememberPasswordAction(peer.id));
// }
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
} }
return menuItems; return menuItems;
} }

View File

@@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart';
@@ -5,14 +7,18 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart'; import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pull_down_button/pull_down_button.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
@@ -63,6 +69,7 @@ class _PeerTabPageState extends State<PeerTabPage>
({dynamic hint}) => gFFI.groupModel.pull(force: hint == null), ({dynamic hint}) => gFFI.groupModel.pull(force: hint == null),
), ),
]; ];
RelativeRect? mobileTabContextMenuPos;
@override @override
void initState() { void initState() {
@@ -102,40 +109,17 @@ class _PeerTabPageState extends State<PeerTabPage>
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded(child: _createSwitchBar(context)), Expanded(
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), child:
_createRefresh(), visibleContextMenuListener(_createSwitchBar(context))),
_createMultiSelection(), if (isMobile)
Offstage( ..._mobileRightActions(context)
offstage: !isDesktop, else
child: _createPeerViewTypeSwitch(context)), ..._desktopRightActions(context)
Offstage(
offstage: gFFI.peerTabModel.currentTab == 0,
child: PeerSortDropdown(),
),
Offstage(
offstage: gFFI.peerTabModel.currentTab != 3,
child: _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel",
value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
},
),
),
], ],
)), )),
), ),
), ).paddingOnly(right: isDesktop ? 12 : 0),
_createPeersView(), _createPeersView(),
], ],
); );
@@ -147,7 +131,7 @@ class _PeerTabPageState extends State<PeerTabPage>
return ListView( return ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: model.indexs.map((t) { children: model.visibleIndexs.map((t) {
final selected = model.currentTab == t; final selected = model.currentTab == t;
final color = selected final color = selected
? MyTheme.tabbar(context).selectedTextColor ? MyTheme.tabbar(context).selectedTextColor
@@ -163,11 +147,13 @@ class _PeerTabPageState extends State<PeerTabPage>
)); ));
return Obx(() => InkWell( return Obx(() => InkWell(
child: Container( child: Container(
decoration: decoration: (hover.value
selected ? decoBorder : (hover.value ? deco : null), ? (selected ? decoBorder : deco)
: (selected ? decoBorder : null)),
child: Tooltip( child: Tooltip(
message: preferBelow: false,
model.tabTooltip(t, gFFI.groupModel.groupName.value), message: model.tabTooltip(t),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
child: Icon(model.tabIcon(t), color: color), child: Icon(model.tabIcon(t), color: color),
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4),
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4),
@@ -184,14 +170,15 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createPeersView() { Widget _createPeersView() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget child; Widget child;
if (model.indexs.isEmpty) { if (model.visibleIndexs.isEmpty) {
child = Center( child = visibleContextMenuListener(Row(
child: Text(translate('Right click to select tabs')), children: [Expanded(child: InkWell())],
); ));
} else { } else {
if (model.indexs.contains(model.currentTab)) { if (model.visibleIndexs.contains(model.currentTab)) {
child = entries[model.currentTab].widget; child = entries[model.currentTab].widget;
} else { } else {
debugPrint("should not happen! currentTab not in visibleIndexs");
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
model.setCurrentTab(model.indexs[0]); model.setCurrentTab(model.indexs[0]);
}); });
@@ -202,17 +189,19 @@ class _PeerTabPageState extends State<PeerTabPage>
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
} }
Widget _createRefresh() { Widget _createRefresh(
{required PeerTabIndex index, required RxBool loading}) {
final model = Provider.of<PeerTabModel>(context);
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage( return Offstage(
offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index, offstage: model.currentTab != index.index,
child: RefreshWidget( child: RefreshWidget(
onPressed: () { onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) { if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load(); entries[gFFI.peerTabModel.currentTab].load();
} }
}, },
spinning: gFFI.abModel.abLoading, spinning: loading,
child: RotatedBox( child: RotatedBox(
quarterTurns: 2, quarterTurns: 2,
child: Tooltip( child: Tooltip(
@@ -254,22 +243,113 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createMultiSelection() { Widget _createMultiSelection() {
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
if (model.currentTabCachedPeers.isEmpty) return Offstage();
return _hoverAction( return _hoverAction(
context: context, context: context,
onTap: () { onTap: () {
model.setMultiSelectionMode(true); model.setMultiSelectionMode(true);
if (isMobile && Navigator.canPop(context)) {
Navigator.pop(context);
}
}, },
child: Tooltip( child: Tooltip(
message: translate('Select'), message: translate('Select'),
child: Icon( child: SvgPicture.asset(
IconFont.checkbox, "assets/checkbox-outline.svg",
size: 18, width: 18,
height: 18,
color: textColor, color: textColor,
)), )),
); );
} }
void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel;
final items = List<PopupMenuItem>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisible[i]),
child: Row(
children: [
Checkbox(
value: model.isVisible[i],
onChanged: (_) {
model.setTabVisible(i, !model.isVisible[i]);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}),
Expanded(child: Text(model.tabTooltip(i))),
],
),
));
}
if (mobileTabContextMenuPos != null) {
showMenu(
context: context, position: mobileTabContextMenuPos!, items: items);
}
}
Widget visibleContextMenuListener(Widget child) {
if (isMobile) {
return GestureDetector(
onLongPressDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
mobileTabContextMenuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onLongPressUp: () {
mobileShowTabVisibilityMenu();
},
child: child,
);
} else {
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
if (e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return visibleContextMenu(cancelFunc);
},
target: e.position,
);
}
},
child: child);
}
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: model.tabTooltip(i),
getter: () async {
return model.isVisible[i];
},
setter: (show) async {
model.setTabVisible(i, show);
cancelFunc();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
}
Widget createMultiSelectionBar() { Widget createMultiSelectionBar() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
@@ -288,6 +368,9 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget deleteSelection() { Widget deleteSelection() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
if (model.currentTab == PeerTabIndex.group.index) {
return Offstage();
}
return _hoverAction( return _hoverAction(
context: context, context: context,
onTap: () { onTap: () {
@@ -457,6 +540,130 @@ class _PeerTabPageState extends State<PeerTabPage>
Tooltip(message: translate('Close'), child: Icon(Icons.clear))) Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
.marginOnly(left: 6); .marginOnly(left: 6);
} }
Widget _toggleTags() {
return _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
});
}
List<Widget> _desktopRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
Offstage(
offstage: model.currentTabCachedPeers.isEmpty,
child: _createMultiSelection(),
),
_createPeerViewTypeSwitch(context),
Offstage(
offstage: model.currentTab == PeerTabIndex.recent.index,
child: PeerSortDropdown(),
),
Offstage(
offstage: model.currentTab != PeerTabIndex.ab.index,
child: _toggleTags(),
),
];
}
List<Widget> _mobileRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize =
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length;
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120;
final otherActionWidth = 18 + 10;
dropDown(List<Widget> menus) {
final padding = 6.0;
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return PullDownButton(
buttonBuilder:
(BuildContext context, Future<void> Function() showMenu) {
return _hoverAction(
context: context,
child: Tooltip(
message: translate('More'),
child: SvgPicture.asset(
"assets/chevron_up_chevron_down.svg",
width: 18,
height: 18,
color: textColor,
)),
onTap: showMenu,
);
},
routeTheme: PullDownMenuRouteTheme(
width: menus.length * (otherActionWidth + padding * 2) * 1.0),
itemBuilder: (context) => [
PullDownMenuEntryImpl(
child: Row(
mainAxisSize: MainAxisSize.min,
children: menus
.map((e) =>
Material(child: e.paddingSymmetric(horizontal: padding)))
.toList(),
),
)
],
);
}
// Always show search, refresh
List<Widget> actions = [
const PeerSearchBar(),
if (model.currentTab == PeerTabIndex.ab.index)
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
if (model.currentTab == PeerTabIndex.group.index)
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
];
final List<Widget> dynamicActions = [
if (model.currentTabCachedPeers.isNotEmpty) _createMultiSelection(),
if (model.currentTab != PeerTabIndex.recent.index) PeerSortDropdown(),
if (model.currentTab == PeerTabIndex.ab.index) _toggleTags()
];
final rightWidth = availableWidth -
searchWidth -
(actions.length == 2 ? otherActionWidth : 0);
final availablePositions = rightWidth ~/ otherActionWidth;
debugPrint(
"dynamic action count:${dynamicActions.length}, available positions: $availablePositions");
if (availablePositions < dynamicActions.length &&
dynamicActions.length > 1) {
if (availablePositions < 2) {
actions.addAll([
dropDown(dynamicActions),
]);
} else {
actions.addAll([
...dynamicActions.sublist(0, availablePositions - 1),
dropDown(dynamicActions.sublist(availablePositions - 1)),
]);
}
} else {
actions.addAll(dynamicActions);
}
return actions;
}
} }
class PeerSearchBar extends StatefulWidget { class PeerSearchBar extends StatefulWidget {
@@ -732,3 +939,14 @@ Widget _hoverAction(
child: Container(padding: padding, child: child))), child: Container(padding: padding, child: child))),
); );
} }
class PullDownMenuEntryImpl extends StatelessWidget
implements PullDownMenuEntry {
final Widget child;
const PullDownMenuEntryImpl({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
}

View File

@@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
@@ -35,6 +37,7 @@ class LoadEvent {
static const String favorite = 'load_fav_peers'; static const String favorite = 'load_fav_peers';
static const String lan = 'load_lan_peers'; static const String lan = 'load_lan_peers';
static const String addressBook = 'load_address_book_peers'; static const String addressBook = 'load_address_book_peers';
static const String group = 'load_group_peers';
} }
/// for peer search text, global obs value /// for peer search text, global obs value
@@ -93,6 +96,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return width; return width;
}(); }();
final _scrollController = ScrollController();
_PeersViewState() { _PeersViewState() {
_startCheckOnlines(); _startCheckOnlines();
} }
@@ -174,16 +179,16 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return FutureBuilder<List<Peer>>( return FutureBuilder<List<Peer>>(
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
final cards = <Widget>[]; buildOnePeer(Peer peer) {
for (final peer in peers) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer), child: widget.peerCardBuilder(peer),
); );
cards.add(isDesktop return isDesktop
? Obx( ? Obx(
() => SizedBox( () => SizedBox(
width: 220, width: 220,
@@ -192,10 +197,34 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
child: visibilityChild, child: visibilityChild,
), ),
) )
: SizedBox(width: mobileWidth, child: visibilityChild)); : SizedBox(width: mobileWidth, child: visibilityChild);
} }
final child =
Wrap(spacing: space, runSpacing: space, children: cards); final Widget child;
if (isMobile) {
child = DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
},
);
} else {
child = DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
);
}
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();
_curPeers.addAll(peers.map((e) => e.id)); _curPeers.addAll(peers.map((e) => e.id));
@@ -312,7 +341,7 @@ abstract class BasePeersView extends StatelessWidget {
final String loadEvent; final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final List<Peer> initPeers; final RxList<Peer>? initPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
@@ -326,7 +355,7 @@ abstract class BasePeersView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _PeersView( return _PeersView(
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers),
peerFilter: peerFilter, peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder); peerCardBuilder: peerCardBuilder);
} }
@@ -343,7 +372,7 @@ class RecentPeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@@ -365,7 +394,7 @@ class FavoritePeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@@ -387,7 +416,7 @@ class DiscoveredPeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@@ -403,7 +432,7 @@ class AddressBookPeersView extends BasePeersView {
{Key? key, {Key? key,
EdgeInsets? menuPadding, EdgeInsets? menuPadding,
ScrollController? scrollController, ScrollController? scrollController,
required List<Peer> initPeers}) required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', name: 'address book peer',
@@ -421,12 +450,21 @@ class AddressBookPeersView extends BasePeersView {
if (selectedTags.isEmpty) { if (selectedTags.isEmpty) {
return true; return true;
} }
for (final tag in selectedTags) { if (gFFI.abModel.filterByIntersection.value) {
if (idents.contains(tag)) { for (final tag in selectedTags) {
return true; if (!idents.contains(tag)) {
return false;
}
} }
return true;
} else {
for (final tag in selectedTags) {
if (idents.contains(tag)) {
return true;
}
}
return false;
} }
return false;
} }
} }
@@ -435,11 +473,11 @@ class MyGroupPeerView extends BasePeersView {
{Key? key, {Key? key,
EdgeInsets? menuPadding, EdgeInsets? menuPadding,
ScrollController? scrollController, ScrollController? scrollController,
required List<Peer> initPeers}) required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'my group peer', name: 'group peer',
loadEvent: 'load_my_group_peers', loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
@@ -450,12 +488,12 @@ class MyGroupPeerView extends BasePeersView {
static bool filter(Peer peer) { static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) { if (gFFI.groupModel.searchUserText.isNotEmpty) {
if (!peer.username.contains(gFFI.groupModel.searchUserText)) { if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
return false; return false;
} }
} }
if (gFFI.groupModel.selectedUser.isNotEmpty) { if (gFFI.groupModel.selectedUser.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.username) { if (gFFI.groupModel.selectedUser.value != peer.loginName) {
return false; return false;
} }
} }

View File

@@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/input_model.dart';
@@ -92,6 +93,7 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
// Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
inputModel.tapDown(MouseButtons.left); inputModel.tapDown(MouseButtons.left);
} }
@@ -111,7 +113,10 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
inputModel.tap(MouseButtons.left); if (!handleTouch) {
// Mobile, "Mouse mode"
inputModel.tap(MouseButtons.left);
}
} }
onDoubleTapDown(TapDownDetails d) { onDoubleTapDown(TapDownDetails d) {
@@ -263,9 +268,9 @@ class _RawTouchGestureDetectorRegionState
if (scale != 0) { if (scale != 0) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': scale} PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
})); .toJson()));
} }
} else { } else {
// mobile // mobile
@@ -283,9 +288,8 @@ class _RawTouchGestureDetectorRegionState
if (isDesktop) { if (isDesktop) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': 0} PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
}));
} else { } else {
// mobile // mobile
_scale = 1; _scale = 1;

View File

@@ -0,0 +1,277 @@
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
customImageQualityWidget(
{required double initQuality,
required double initFps,
required Function(double) setQuality,
required Function(double) setFps,
required bool showFps}) {
final qualityValue = initQuality.obs;
final fpsValue = initFps.obs;
final RxBool moreQualityChecked = RxBool(qualityValue.value > 100);
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setQuality(v);
},
initialValue: qualityValue.value,
);
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setFps(v);
},
initialValue: fpsValue.value,
);
onMoreChanged(bool? value) {
if (value == null) return;
moreQualityChecked.value = value;
if (!value && qualityValue.value > 100) {
qualityValue.value = 100;
}
debouncerQuality.value = qualityValue.value;
}
return Column(
children: [
Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: qualityValue.value,
min: 10.0,
max: moreQualityChecked.value ? 2000 : 100,
divisions: moreQualityChecked.value ? 199 : 18,
onChanged: (double value) async {
qualityValue.value = value;
debouncerQuality.value = value;
},
),
),
Expanded(
flex: 1,
child: Text(
'${qualityValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: isMobile ? 2 : 1,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
)),
// mobile doesn't have enough space
if (!isMobile)
Expanded(
flex: 1,
child: Row(
children: [
Checkbox(
value: moreQualityChecked.value,
onChanged: onMoreChanged,
),
Expanded(
child: Text(translate('More')),
)
],
))
],
)),
if (isMobile)
Obx(() => Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Checkbox(
value: moreQualityChecked.value,
onChanged: onMoreChanged,
),
),
),
Expanded(
child: Text(translate('More')),
)
],
)),
if (showFps)
Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: fpsValue.value,
min: 5.0,
max: 120.0,
divisions: 23,
onChanged: (double value) async {
fpsValue.value = value;
debouncerFps.value = value;
},
),
),
Expanded(
flex: 1,
child: Text(
'${fpsValue.value.round()}',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 2,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
)),
],
);
}
customImageQualitySetting() {
final qualityKey = 'custom_image_quality';
final fpsKey = 'custom-fps';
var initQuality =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0);
if (initQuality < 10 || initQuality > 2000) {
initQuality = 50;
}
var initFps =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0);
if (initFps < 5 || initFps > 120) {
initFps = 30;
}
return customImageQualityWidget(
initQuality: initQuality,
initFps: initFps,
setQuality: (v) {
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
},
setFps: (v) {
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
},
showFps: true);
}
Future<bool> setServerConfig(
List<TextEditingController> controllers,
List<RxString> errMsgs,
ServerConfig config,
) async {
config.idServer = config.idServer.trim();
config.relayServer = config.relayServer.trim();
config.apiServer = config.apiServer.trim();
config.key = config.key.trim();
// id
if (config.idServer.isNotEmpty) {
errMsgs[0].value =
translate(await bind.mainTestIfValidServer(server: config.idServer));
if (errMsgs[0].isNotEmpty) {
return false;
}
}
// relay
if (config.relayServer.isNotEmpty) {
errMsgs[1].value =
translate(await bind.mainTestIfValidServer(server: config.relayServer));
if (errMsgs[1].isNotEmpty) {
return false;
}
}
// api
if (config.apiServer.isNotEmpty) {
if (!config.apiServer.startsWith('http://') &&
!config.apiServer.startsWith('https://')) {
errMsgs[2].value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final oldApiServer = await bind.mainGetApiServer();
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: config.idServer);
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
await bind.mainSetOption(key: 'key', value: config.key);
final newApiServer = await bind.mainGetApiServer();
if (oldApiServer.isNotEmpty &&
oldApiServer != newApiServer &&
gFFI.userModel.isLogin) {
gFFI.userModel.logOut(apiServer: oldApiServer);
}
return true;
}
List<Widget> ServerConfigImportExportWidgets(
List<TextEditingController> controllers,
List<RxString> errMsgs,
) {
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
controllers[0].text = sc.idServer;
controllers[1].text = sc.relayServer;
controllers[2].text = sc.apiServer;
controllers[3].text = sc.key;
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
success.then((value) {
if (value) {
showToast(
translate('Import server configuration successfully'));
} else {
showToast(translate('Invalid server configuration'));
}
});
} else {
showToast(translate('Invalid server configuration'));
}
} catch (e) {
showToast(translate('Invalid server configuration'));
}
} else {
showToast(translate('Clipboard is empty'));
}
});
}
export() {
final text = ServerConfig(
idServer: controllers[0].text.trim(),
relayServer: controllers[1].text.trim(),
apiServer: controllers[2].text.trim(),
key: controllers[3].text.trim())
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
return [
Tooltip(
message: translate('Import Server Config'),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton(
icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
];
}

View File

@@ -49,7 +49,8 @@ class TToggleMenu {
handleOsPasswordEditIcon( handleOsPasswordEditIcon(
SessionID sessionId, OverlayDialogManager dialogManager) { SessionID sessionId, OverlayDialogManager dialogManager) {
isEditOsPassword = true; isEditOsPassword = true;
showSetOSPassword(sessionId, false, dialogManager, null, () => isEditOsPassword = false); showSetOSPassword(
sessionId, false, dialogManager, null, () => isEditOsPassword = false);
} }
handleOsPasswordAction( handleOsPasswordAction(
@@ -62,7 +63,8 @@ handleOsPasswordAction(
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ?? await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
''; '';
if (password.isEmpty) { if (password.isEmpty) {
showSetOSPassword(sessionId, true, dialogManager, password, () => isEditOsPassword = false); showSetOSPassword(sessionId, true, dialogManager, password,
() => isEditOsPassword = false);
} else { } else {
bind.sessionInputOsPassword(sessionId: sessionId, value: password); bind.sessionInputOsPassword(sessionId: sessionId, value: password);
} }
@@ -76,7 +78,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
List<TTextMenu> v = []; List<TTextMenu> v = [];
// elevation // elevation
if (ffi.elevationModel.showRequestMenu) { if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Request Elevation')), child: Text(translate('Request Elevation')),

View File

@@ -5,6 +5,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0; const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1;
const int kMainWindowId = 0; const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
@@ -12,6 +13,8 @@ const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android"; const String kPeerPlatformAndroid = "Android";
const double kScrollbarThickness = 12.0;
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main"; const String kAppTypeMain = "main";
@@ -38,7 +41,7 @@ const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventCloseForSeparateWindow = "close_for_separate_window"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
@@ -54,6 +57,9 @@ const String kTabLabelSettingPage = "Settings";
const String kWindowPrefix = "wm_"; const String kWindowPrefix = "wm_";
const int kWindowMainId = 0; const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
// the executable name of the portable version // the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
@@ -68,10 +74,6 @@ const int kDesktopDefaultDisplayHeight = 720;
const int kMobileMaxDisplaySize = 1280; const int kMobileMaxDisplaySize = 1280;
const int kDesktopMaxDisplaySize = 3840; const int kDesktopMaxDisplaySize = 3840;
const double kDesktopFileTransferNameColWidth = 200;
const double kDesktopFileTransferModifiedColWidth = 120;
const double kDesktopFileTransferMinimumWidth = 100;
const double kDesktopFileTransferMaximumWidth = 300;
const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferRowHeight = 30.0;
const double kDesktopFileTransferHeaderHeight = 25.0; const double kDesktopFileTransferHeaderHeight = 25.0;
@@ -134,6 +136,12 @@ const kRemoteScrollStyleAuto = 'scrollauto';
/// [kRemoteScrollStyleBar] Scroll image with scroll bar. /// [kRemoteScrollStyleBar] Scroll image with scroll bar.
const kRemoteScrollStyleBar = 'scrollbar'; const kRemoteScrollStyleBar = 'scrollbar';
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
const kScrollModeDefault = 'default';
/// [kScrollModeReverse] Mouse or touchpad, the reverse scroll mode.
const kScrollModeReverse = 'reverse';
/// [kRemoteImageQualityBest] Best image quality. /// [kRemoteImageQualityBest] Best image quality.
const kRemoteImageQualityBest = 'best'; const kRemoteImageQualityBest = 'best';

View File

@@ -7,7 +7,6 @@ import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -33,9 +32,6 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Controller for the id input bar. /// Controller for the id input bar.
final _idController = IDTextEditingController(); final _idController = IDTextEditingController();
/// Nested scroll controller
final _scrollController = ScrollController();
Timer? _updateTimer; Timer? _updateTimer;
final RxBool _idInputFocused = false.obs; final RxBool _idInputFocused = false.obs;
@@ -106,7 +102,8 @@ class _ConnectionPageState extends State<ConnectionPage>
@override @override
void onWindowLeaveFullScreen() { void onWindowLeaveFullScreen() {
// Restore edge border to default edge size. // Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value = kWindowEdgeSize; stateGlobal.resizeEdgeSize.value =
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
} }
@override @override
@@ -120,30 +117,18 @@ class _ConnectionPageState extends State<ConnectionPage>
return Column( return Column(
children: [ children: [
Expanded( Expanded(
child: DesktopScrollWrapper( child: Column(
scrollController: _scrollController, children: [
child: CustomScrollView( Row(
controller: _scrollController, children: [
physics: DraggableNeverScrollableScrollPhysics(), Flexible(child: _buildRemoteIDTextField(context)),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
Row(
children: [
Flexible(child: _buildRemoteIDTextField(context)),
],
).marginOnly(top: 22),
SizedBox(height: 12),
Divider().paddingOnly(right: 12),
])),
SliverFillRemaining(
hasScrollBody: false,
child: PeerTabPage().paddingOnly(right: 12.0),
)
], ],
).paddingOnly(left: 12.0), ).marginOnly(top: 22),
), SizedBox(height: 12),
), Divider().paddingOnly(right: 12),
Expanded(child: PeerTabPage()),
],
).paddingOnly(left: 12.0)),
const Divider(height: 1), const Divider(height: 1),
buildStatus() buildStatus()
], ],

View File

@@ -48,6 +48,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var watchIsInputMonitoring = false; var watchIsInputMonitoring = false;
var watchIsCanRecordAudio = false; var watchIsCanRecordAudio = false;
Timer? _updateTimer; Timer? _updateTimer;
bool isCardClosed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -321,14 +322,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
Future<Widget> buildHelpCards() async { Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty) { if (updateUrl.isNotEmpty && !isCardClosed) {
return buildInstallCard( return buildInstallCard(
"Status", "Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.", "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
"Click to download", () async { "Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download'); final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url); await launchUrl(url);
}); },
closeButton: true);
} }
if (systemError.isNotEmpty) { if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {}); return buildInstallCard("", systemError, "", () {});
@@ -394,11 +396,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget buildInstallCard(String title, String content, String btnText, Widget buildInstallCard(String title, String content, String btnText,
GestureTapCallback onPressed, GestureTapCallback onPressed,
{String? help, String? link}) { {String? help, String? link, bool? closeButton}) {
return Container(
margin: EdgeInsets.only(top: 20), void closeCard() {
child: Container( setState(() {
decoration: BoxDecoration( isCardClosed = true;
});
}
return Stack(
children: [
Container(
margin: EdgeInsets.only(top: 20),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
@@ -467,19 +478,33 @@ class _DesktopHomePageState extends State<DesktopHomePage>
)).marginOnly(top: 6)), )).marginOnly(top: 6)),
] ]
: <Widget>[]))), : <Widget>[]))),
),
if (closeButton != null && closeButton == true)
Positioned(
top: 18,
right: 0,
child: IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
size: 20,
),
onPressed: closeCard,
),
),
],
); );
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
});
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID(); await gFFI.serverModel.fetchID();
final url = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl != url) {
updateUrl = url;
setState(() {});
}
final error = await bind.mainGetError(); final error = await bind.mainGetError();
if (systemError != error) { if (systemError != error) {
systemError = error; systemError = error;

View File

@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.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/pages/desktop_tab_page.dart';
@@ -17,7 +18,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:window_manager/window_manager.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart'; import '../../common/widgets/login.dart';
@@ -88,6 +88,11 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag); Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
controller = PageController(initialPage: widget.initialPage); controller = PageController(initialPage: widget.initialPage);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag); Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() {
if (controller.page != null) {
selectedIndex.value = controller.page!.toInt();
}
});
} }
@override @override
@@ -154,7 +159,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
scrollController: controller, scrollController: controller,
child: PageView( child: PageView(
controller: controller, controller: controller,
physics: DraggableNeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: _children(), children: _children(),
)), )),
), ),
@@ -330,6 +335,12 @@ class _GeneralState extends State<_General> {
child: _OptionCheckBox(context, "Always use software rendering", child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'), 'allow-always-software-render'),
)); ));
children.add(_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
));
if (bind.mainShowOption(key: 'allow-linux-headless')) { if (bind.mainShowOption(key: 'allow-linux-headless')) {
children.add(_OptionCheckBox( children.add(_OptionCheckBox(
context, 'Allow linux headless', 'allow-linux-headless')); context, 'Allow linux headless', 'allow-linux-headless'));
@@ -708,8 +719,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword) if (usePassword)
_SubButton('Set permanent password', setPasswordDialog, _SubButton('Set permanent password', setPasswordDialog,
permEnabled && !locked), permEnabled && !locked),
if (usePassword) // if (usePassword)
hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), // hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
if (usePassword) radios[2], if (usePassword) radios[2],
]); ]);
}))); })));
@@ -718,16 +729,12 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget more(BuildContext context) { Widget more(BuildContext context) {
bool enabled = !locked; bool enabled = !locked;
return _Card(title: 'Security', children: [ return _Card(title: 'Security', children: [
Offstage(
offstage: !Platform.isWindows,
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
enabled: enabled),
),
shareRdp(context, enabled), shareRdp(context, enabled),
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery', _OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled), reverse: true, enabled: enabled),
...directIp(context), ...directIp(context),
whitelist(), whitelist(),
...autoDisconnect(context),
]); ]);
} }
@@ -906,6 +913,63 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
)); ));
})); }));
} }
List<Widget> autoDisconnect(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
RxBool applyEnabled = false.obs;
final optionKey = 'allow-auto-disconnect';
final timeoutKey = 'auto-disconnect-timeout';
return [
_OptionCheckBox(context, 'auto_disconnect_option_tip', optionKey,
update: update, enabled: !locked),
() {
bool enabled =
option2bool(optionKey, bind.mainGetOptionSync(key: optionKey));
if (!enabled) applyEnabled.value = false;
controller.text = bind.mainGetOptionSync(key: timeoutKey);
return Offstage(
offstage: !enabled,
child: _SubLabeledWidget(
context,
'Timeout in minutes',
Row(children: [
SizedBox(
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked,
onChanged: (_) => applyEnabled.value = true,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
decoration: const InputDecoration(
hintText: '10',
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value && enabled && !locked
? () async {
applyEnabled.value = false;
await bind.mainSetOption(
key: timeoutKey, value: controller.text);
}
: null,
child: Text(
translate('Apply'),
),
))
]),
enabled: enabled && !locked,
),
);
}(),
];
}
} }
class _Network extends StatefulWidget { class _Network extends StatefulWidget {
@@ -966,54 +1030,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
var relayController = TextEditingController(text: old('relay-server')); var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server')); var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key')); var keyController = TextEditingController(text: old('key'));
final controllers = [
set(String idServer, String relayServer, String apiServer, idController,
String key) async { relayController,
idServer = idServer.trim(); apiController,
relayServer = relayServer.trim(); keyController,
apiServer = apiServer.trim(); ];
key = key.trim(); final errMsgs = [
if (idServer.isNotEmpty) { idErrMsg,
idErrMsg.value = relayErrMsg,
translate(await bind.mainTestIfValidServer(server: idServer)); apiErrMsg,
if (idErrMsg.isNotEmpty) { ];
return false;
}
}
if (relayServer.isNotEmpty) {
relayErrMsg.value =
translate(await bind.mainTestIfValidServer(server: relayServer));
if (relayErrMsg.isNotEmpty) {
return false;
}
}
if (apiServer.isNotEmpty) {
if (!apiServer.startsWith('http://') &&
!apiServer.startsWith('https://')) {
apiErrMsg.value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final oldApiServer = await bind.mainGetApiServer();
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
await bind.mainSetOption(key: 'relay-server', value: relayServer);
await bind.mainSetOption(key: 'api-server', value: apiServer);
await bind.mainSetOption(key: 'key', value: key);
final newApiServer = await bind.mainGetApiServer();
if (oldApiServer.isNotEmpty && oldApiServer != newApiServer) {
await gFFI.userModel.logOut(apiServer: oldApiServer);
}
return true;
}
submit() async { submit() async {
bool result = await set(idController.text, relayController.text, bool result = await setServerConfig(
apiController.text, keyController.text); controllers,
errMsgs,
ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text));
if (result) { if (result) {
setState(() {}); setState(() {});
showToast(translate('Successful')); showToast(translate('Successful'));
@@ -1022,83 +1059,28 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
} }
} }
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
idController.text = sc.idServer;
relayController.text = sc.relayServer;
apiController.text = sc.apiServer;
keyController.text = sc.key;
Future<bool> success =
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
success.then((value) {
if (value) {
showToast(
translate('Import server configuration successfully'));
} else {
showToast(translate('Invalid server configuration'));
}
});
} else {
showToast(translate('Invalid server configuration'));
}
} catch (e) {
showToast(translate('Invalid server configuration'));
}
} else {
showToast(translate('Clipboard is empty'));
}
});
}
export() {
final text = ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text)
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
bool secure = !enabled; bool secure = !enabled;
return _Card(title: 'ID/Relay Server', title_suffix: [ return _Card(
Tooltip( title: 'ID/Relay Server',
message: translate('Import Server Config'), title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey),
onPressed: enabled ? import : null),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton(
icon: Icon(Icons.copy, color: Colors.grey),
onPressed: enabled ? export : null)),
], children: [
Column(
children: [ children: [
Obx(() => _LabeledTextField(context, 'ID Server', idController, Column(
idErrMsg.value, enabled, secure)), children: [
Obx(() => _LabeledTextField(context, 'Relay Server', Obx(() => _LabeledTextField(context, 'ID Server', idController,
relayController, relayErrMsg.value, enabled, secure)), idErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'API Server', apiController, Obx(() => _LabeledTextField(context, 'Relay Server',
apiErrMsg.value, enabled, secure)), relayController, relayErrMsg.value, enabled, secure)),
_LabeledTextField( Obx(() => _LabeledTextField(context, 'API Server',
context, 'Key', keyController, '', enabled, secure), apiController, apiErrMsg.value, enabled, secure)),
Row( _LabeledTextField(
mainAxisAlignment: MainAxisAlignment.end, context, 'Key', keyController, '', enabled, secure),
children: [_Button('Apply', submit, enabled: enabled)], Row(
).marginOnly(top: 10), mainAxisAlignment: MainAxisAlignment.end,
], children: [_Button('Apply', submit, enabled: enabled)],
) ).marginOnly(top: 10),
]); ],
)
]);
} }
return tmpWrapper(); return tmpWrapper();
@@ -1182,15 +1164,6 @@ class _DisplayState extends State<_Display> {
} }
final groupValue = bind.mainGetUserDefaultOption(key: key); final groupValue = bind.mainGetUserDefaultOption(key: key);
final qualityKey = 'custom_image_quality';
final qualityValue =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
50.0)
.obs;
final fpsKey = 'custom-fps';
final fpsValue =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
.obs;
return _Card(title: 'Default Image Quality', children: [ return _Card(title: 'Default Image Quality', children: [
_Radio(context, _Radio(context,
value: kRemoteImageQualityBest, value: kRemoteImageQualityBest,
@@ -1214,64 +1187,7 @@ class _DisplayState extends State<_Display> {
onChanged: onChanged), onChanged: onChanged),
Offstage( Offstage(
offstage: groupValue != kRemoteImageQualityCustom, offstage: groupValue != kRemoteImageQualityCustom,
child: Column( child: customImageQualitySetting(),
children: [
Obx(() => Row(
children: [
Slider(
value: qualityValue.value,
min: 10.0,
max: 100.0,
divisions: 18,
onChanged: (double value) async {
qualityValue.value = value;
await bind.mainSetUserDefaultOption(
key: qualityKey, value: value.toString());
},
),
SizedBox(
width: 40,
child: Text(
'${qualityValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
SizedBox(
width: 50,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
))
],
)),
Obx(() => Row(
children: [
Slider(
value: fpsValue.value,
min: 5.0,
max: 120.0,
divisions: 23,
onChanged: (double value) async {
fpsValue.value = value;
await bind.mainSetUserDefaultOption(
key: fpsKey, value: value.toString());
},
),
SizedBox(
width: 40,
child: Text(
'${fpsValue.value.round()}',
style: const TextStyle(fontSize: 15),
)),
SizedBox(
width: 50,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
)),
],
),
) )
]); ]);
} }
@@ -1364,6 +1280,7 @@ class _DisplayState extends State<_Display> {
otherRow('Disable clipboard', 'disable_clipboard'), otherRow('Disable clipboard', 'disable_clipboard'),
otherRow('Lock after session end', 'lock_after_session_end'), otherRow('Lock after session end', 'lock_after_session_end'),
otherRow('Privacy mode', 'privacy_mode'), otherRow('Privacy mode', 'privacy_mode'),
otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'),
]); ]);
} }
} }
@@ -1684,9 +1601,14 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
isServer isServer
? await mainSetBoolOption(key, option) ? await mainSetBoolOption(key, option)
: await mainSetLocalBoolOption(key, option); : await mainSetLocalBoolOption(key, option);
ref.value = isServer final readOption = isServer
? mainGetBoolOptionSync(key) ? mainGetBoolOptionSync(key)
: mainGetLocalBoolOptionSync(key); : mainGetLocalBoolOptionSync(key);
if (reverse) {
ref.value = !readOption;
} else {
ref.value = readOption;
}
update?.call(); update?.call();
} }
} }

View File

@@ -364,15 +364,20 @@ class _FileManagerViewState extends State<FileManagerView> {
final _breadCrumbScroller = ScrollController(); final _breadCrumbScroller = ScrollController();
final _keyboardNode = FocusNode(); final _keyboardNode = FocusNode();
final _listSearchBuffer = TimeoutStringBuffer(); final _listSearchBuffer = TimeoutStringBuffer();
final _nameColWidth = kDesktopFileTransferNameColWidth.obs; final _nameColWidth = 0.0.obs;
final _modifiedColWidth = kDesktopFileTransferModifiedColWidth.obs; final _modifiedColWidth = 0.0.obs;
final _sizeColWidth = 0.0.obs;
final _fileListScrollController = ScrollController(); final _fileListScrollController = ScrollController();
final _globalHeaderKey = GlobalKey();
/// [_lastClickTime], [_lastClickEntry] help to handle double click /// [_lastClickTime], [_lastClickEntry] help to handle double click
var _lastClickTime = var _lastClickTime =
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000; DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
Entry? _lastClickEntry; Entry? _lastClickEntry;
double? _windowWidthPrev;
double _fileTransferMinimumWidth = 0.0;
FileController get controller => widget.controller; FileController get controller => widget.controller;
bool get isLocal => widget.controller.isLocal; bool get isLocal => widget.controller.isLocal;
FFI get _ffi => widget._ffi; FFI get _ffi => widget._ffi;
@@ -398,6 +403,7 @@ class _FileManagerViewState extends State<FileManagerView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_handleColumnPorportions();
return Container( return Container(
margin: const EdgeInsets.all(16.0), margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@@ -429,6 +435,27 @@ class _FileManagerViewState extends State<FileManagerView> {
); );
} }
void _handleColumnPorportions() {
final windowWidthNow = MediaQuery.of(context).size.width;
if (_windowWidthPrev == null) {
_windowWidthPrev = windowWidthNow;
final defaultColumnWidth = windowWidthNow * 0.115;
_fileTransferMinimumWidth = defaultColumnWidth / 3;
_nameColWidth.value = defaultColumnWidth;
_modifiedColWidth.value = defaultColumnWidth;
_sizeColWidth.value = defaultColumnWidth;
}
if (_windowWidthPrev != windowWidthNow) {
final difference = windowWidthNow / _windowWidthPrev!;
_windowWidthPrev = windowWidthNow;
_fileTransferMinimumWidth *= difference;
_nameColWidth.value *= difference;
_modifiedColWidth.value *= difference;
_sizeColWidth.value *= difference;
}
}
void onLocationFocusChanged() { void onLocationFocusChanged() {
debugPrint("focus changed on local"); debugPrint("focus changed on local");
if (_locationNode.hasFocus) { if (_locationNode.hasFocus) {
@@ -1143,9 +1170,21 @@ class _FileManagerViewState extends State<FileManagerView> {
return false; return false;
} }
void _onDrag(double dx, RxDouble column1, RxDouble column2) {
if (column1.value + dx <= _fileTransferMinimumWidth ||
column2.value - dx <= _fileTransferMinimumWidth) {
return;
}
column1.value += dx;
column2.value -= dx;
column1.value = max(_fileTransferMinimumWidth, column1.value);
column2.value = max(_fileTransferMinimumWidth, column2.value);
}
Widget _buildFileBrowserHeader(BuildContext context) { Widget _buildFileBrowserHeader(BuildContext context) {
final padding = EdgeInsets.all(1.0); final padding = EdgeInsets.all(1.0);
return SizedBox( return SizedBox(
key: _globalHeaderKey,
height: kDesktopFileTransferHeaderHeight, height: kDesktopFileTransferHeaderHeight,
child: Row( child: Row(
children: [ children: [
@@ -1155,11 +1194,8 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
DraggableDivider( DraggableDivider(
axis: Axis.vertical, axis: Axis.vertical,
onPointerMove: (dx) { onPointerMove: (dx) =>
_nameColWidth.value += dx; _onDrag(dx, _nameColWidth, _modifiedColWidth),
_nameColWidth.value = min(kDesktopFileTransferMaximumWidth,
max(kDesktopFileTransferMinimumWidth, _nameColWidth.value));
},
padding: padding, padding: padding,
), ),
Obx( Obx(
@@ -1168,15 +1204,12 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
DraggableDivider( DraggableDivider(
axis: Axis.vertical, axis: Axis.vertical,
onPointerMove: (dx) { onPointerMove: (dx) =>
_modifiedColWidth.value += dx; _onDrag(dx, _modifiedColWidth, _sizeColWidth),
_modifiedColWidth.value = min(
kDesktopFileTransferMaximumWidth,
max(kDesktopFileTransferMinimumWidth,
_modifiedColWidth.value));
},
padding: padding), padding: padding),
Expanded(child: headerItemFunc(null, SortBy.size, translate("Size"))) Expanded(
child: headerItemFunc(
_sizeColWidth.value, SortBy.size, translate("Size")))
], ],
), ),
); );
@@ -1201,23 +1234,20 @@ class _FileManagerViewState extends State<FileManagerView> {
height: kDesktopFileTransferHeaderHeight, height: kDesktopFileTransferHeaderHeight,
child: Row( child: Row(
children: [ children: [
Flexible( Expanded(
flex: 2,
child: Text( child: Text(
name, name,
style: headerTextStyle, style: headerTextStyle,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).marginSymmetric(horizontal: 4), ).marginOnly(left: 4),
), ),
Flexible( ascending.value != null
flex: 1, ? Icon(
child: ascending.value != null ascending.value!
? Icon( ? Icons.keyboard_arrow_up_rounded
ascending.value! : Icons.keyboard_arrow_down_rounded,
? Icons.keyboard_arrow_up_rounded )
: Icons.keyboard_arrow_down_rounded, : SizedBox()
)
: const Offstage())
], ],
), ),
), ),

View File

@@ -99,7 +99,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
controller: tabController, controller: tabController,
onWindowCloseButton: handleWindowCloseButton, onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10), tail: const AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.labelGetterAlias, labelGetter: DesktopTab.tablabelGetter,
)), )),
); );
return Platform.isMacOS || kUseCompatibleUiMode return Platform.isMacOS || kUseCompatibleUiMode

View File

@@ -266,7 +266,7 @@ class _PortForwardPageState extends State<PortForwardPage>
} }
void refreshTunnelConfig() async { void refreshTunnelConfig() async {
String peer = await bind.mainGetPeer(id: widget.id); String peer = bind.mainGetPeerSync(id: widget.id);
Map<String, dynamic> config = jsonDecode(peer); Map<String, dynamic> config = jsonDecode(peer);
List<dynamic> infos = config['port_forwards'] as List; List<dynamic> infos = config['port_forwards'] as List;
List<_PortForward> result = List.empty(growable: true); List<_PortForward> result = List.empty(growable: true);

View File

@@ -108,7 +108,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
return true; return true;
}, },
tail: AddButton().paddingOnly(left: 10), tail: AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.labelGetterAlias, labelGetter: DesktopTab.tablabelGetter,
)), )),
); );
return Platform.isMacOS || kUseCompatibleUiMode return Platform.isMacOS || kUseCompatibleUiMode

View File

@@ -35,6 +35,7 @@ class RemotePage extends StatefulWidget {
Key? key, Key? key,
required this.id, required this.id,
required this.sessionId, required this.sessionId,
required this.tabWindowId,
required this.password, required this.password,
required this.toolbarState, required this.toolbarState,
required this.tabController, required this.tabController,
@@ -44,6 +45,7 @@ class RemotePage extends StatefulWidget {
final String id; final String id;
final SessionID? sessionId; final SessionID? sessionId;
final int? tabWindowId;
final String? password; final String? password;
final ToolbarState toolbarState; final ToolbarState toolbarState;
final String? switchUuid; final String? switchUuid;
@@ -106,6 +108,7 @@ class _RemotePageState extends State<RemotePage>
password: widget.password, password: widget.password,
switchUuid: widget.switchUuid, switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -206,7 +209,7 @@ class _RemotePageState extends State<RemotePage>
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
await _renderTexture.destroy(closeSession); await _renderTexture.destroy(closeSession);
// ensure we leave this session, this is a double check // ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false); _ffi.inputModel.enterOrLeave(false);
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay(); _ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose(); _ffi.recordingModel.onClose();
@@ -225,49 +228,70 @@ class _RemotePageState extends State<RemotePage>
removeSharedStates(widget.id); removeSharedStates(widget.id);
} }
Widget buildBody(BuildContext context) { Widget emptyOverlay() => BlockableOverlay(
return Scaffold( /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
backgroundColor: Theme.of(context).colorScheme.background, /// see override build() in [BlockableOverlay]
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
body: BlockableOverlay(
state: _blockableOverlayState, state: _blockableOverlayState,
underlying: Container( underlying: Container(
color: Colors.black, color: Colors.transparent,
child: RawKeyFocusScope( ),
focusNode: _rawKeyFocusNode, );
onFocusChange: (bool imageFocused) {
debugPrint( Widget buildBody(BuildContext context) {
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); remoteToolbar(BuildContext context) => RemoteToolbar(
// See [onWindowBlur]. id: widget.id,
if (Platform.isWindows) { ffi: _ffi,
if (_isWindowBlur) { state: widget.toolbarState,
imageFocused = false; onEnterOrLeaveImageSetter: (func) =>
Future.delayed(Duration.zero, () { _onEnterOrLeaveImage4Toolbar = func,
_rawKeyFocusNode.unfocus(); onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
}); );
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Stack(
children: [
Container(
color: Colors.black,
child: RawKeyFocusScope(
focusNode: _rawKeyFocusNode,
onFocusChange: (bool imageFocused) {
debugPrint(
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
// See [onWindowBlur].
if (Platform.isWindows) {
if (_isWindowBlur) {
imageFocused = false;
Future.delayed(Duration.zero, () {
_rawKeyFocusNode.unfocus();
});
}
if (imageFocused) {
_ffi.inputModel.enterOrLeave(true);
} else {
_ffi.inputModel.enterOrLeave(false);
}
} }
if (imageFocused) { },
_ffi.inputModel.enterOrLeave(true); inputModel: _ffi.inputModel,
} else { child: getBodyForDesktop(context))),
_ffi.inputModel.enterOrLeave(false); Obx(() => Stack(
} children: [
} _ffi.ffiModel.pi.isSet.isTrue &&
}, _ffi.ffiModel.waitForFirstImage.isTrue
inputModel: _ffi.inputModel, ? emptyOverlay()
child: getBodyForDesktop(context))), : () {
upperLayer: [ _ffi.ffiModel.tryShowAndroidActionsOverlay();
OverlayEntry( return Offstage();
builder: (context) => RemoteToolbar( }(),
id: widget.id, // Use Overlay to enable rebuild every time on menu button click.
ffi: _ffi, _ffi.ffiModel.pi.isSet.isTrue
state: widget.toolbarState, ? Overlay(initialEntries: [
onEnterOrLeaveImageSetter: (func) => OverlayEntry(builder: remoteToolbar)
_onEnterOrLeaveImage4Toolbar = func, ])
onEnterOrLeaveImageCleaner: () => : remoteToolbar(context),
_onEnterOrLeaveImage4Toolbar = null, _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
)) ],
)),
], ],
), ),
); );
@@ -305,7 +329,7 @@ class _RemotePageState extends State<RemotePage>
if (!_rawKeyFocusNode.hasFocus) { if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus(); _rawKeyFocusNode.requestFocus();
} }
bind.sessionEnterOrLeave(sessionId: sessionId, enter: true); _ffi.inputModel.enterOrLeave(true);
} }
} }
@@ -325,7 +349,7 @@ class _RemotePageState extends State<RemotePage>
} }
// See [onWindowBlur]. // See [onWindowBlur].
if (!Platform.isWindows) { if (!Platform.isWindows) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false); _ffi.inputModel.enterOrLeave(false);
} }
} }
@@ -385,7 +409,7 @@ class _RemotePageState extends State<RemotePage>
keyboardEnabled: _keyboardEnabled, keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved, remoteCursorMoved: _remoteCursorMoved,
textureId: _renderTexture.textureId, textureId: _renderTexture.textureId,
useTextureRender: _renderTexture.useTextureRender, useTextureRender: RenderTexture.useTextureRender,
listenerBuilder: (child) => listenerBuilder: (child) =>
_buildRawTouchAndPointerRegion(child, enterView, leaveView), _buildRawTouchAndPointerRegion(child, enterView, leaveView),
); );
@@ -461,21 +485,20 @@ class _ImagePaintState extends State<ImagePaint> {
var c = Provider.of<CanvasModel>(context); var c = Provider.of<CanvasModel>(context);
final s = c.scale; final s = c.scale;
bool isViewAdaptive() => c.viewStyle.style == kRemoteViewStyleAdaptive;
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
mouseRegion({child}) => Obx(() { mouseRegion({child}) => Obx(() {
double getCursorScale() { double getCursorScale() {
var c = Provider.of<CanvasModel>(context); var c = Provider.of<CanvasModel>(context);
var cursorScale = 1.0; var cursorScale = 1.0;
if (Platform.isWindows) { if (Platform.isWindows) {
// debug win10 // debug win10
final isViewAdaptive = if (zoomCursor.value && isViewAdaptive()) {
c.viewStyle.style == kRemoteViewStyleAdaptive;
if (zoomCursor.value && isViewAdaptive) {
cursorScale = s * c.devicePixelRatio; cursorScale = s * c.devicePixelRatio;
} }
} else { } else {
final isViewOriginal = if (zoomCursor.value || isViewOriginal()) {
c.viewStyle.style == kRemoteViewStyleOriginal;
if (zoomCursor.value || isViewOriginal) {
cursorScale = s; cursorScale = s;
} }
} }
@@ -515,7 +538,11 @@ class _ImagePaintState extends State<ImagePaint> {
imageWidget = SizedBox( imageWidget = SizedBox(
width: imageWidth, width: imageWidth,
height: imageHeight, height: imageHeight,
child: Obx(() => Texture(textureId: widget.textureId.value)), child: Obx(() => Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
)),
); );
} else { } else {
imageWidget = CustomPaint( imageWidget = CustomPaint(
@@ -549,14 +576,20 @@ class _ImagePaintState extends State<ImagePaint> {
late final Widget imageWidget; late final Widget imageWidget;
if (c.size.width > 0 && c.size.height > 0) { if (c.size.width > 0 && c.size.height > 0) {
if (widget.useTextureRender) { if (widget.useTextureRender) {
final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x;
final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y;
imageWidget = Stack( imageWidget = Stack(
children: [ children: [
Positioned( Positioned(
left: c.x.toInt().toDouble(), left: x,
top: c.y.toInt().toDouble(), top: y,
width: c.getDisplayWidth() * s, width: c.getDisplayWidth() * s,
height: c.getDisplayHeight() * s, height: c.getDisplayHeight() * s,
child: Texture(textureId: widget.textureId.value), child: Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
),
) )
], ],
); );
@@ -581,7 +614,7 @@ class _ImagePaintState extends State<ImagePaint> {
} else { } else {
final key = cache.updateGetKey(scale); final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) { if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key"); debugPrint("Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety] // [Safety]
// It's ok to call async registerCursor in current synchronous context, // It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always // because activating the cursor is also an async call and will always
@@ -670,6 +703,7 @@ class _ImagePaintState extends State<ImagePaint> {
enableCustomMouseWheelScrolling: cursorOverImage.isFalse, enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
customMouseWheelScrollConfig: scrollConfig, customMouseWheelScrollConfig: scrollConfig,
child: RawScrollbar( child: RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey, thumbColor: Colors.grey,
controller: _horizontal, controller: _horizontal,
thumbVisibility: false, thumbVisibility: false,
@@ -687,6 +721,7 @@ class _ImagePaintState extends State<ImagePaint> {
enableCustomMouseWheelScrolling: cursorOverImage.isFalse, enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
customMouseWheelScrollConfig: scrollConfig, customMouseWheelScrollConfig: scrollConfig,
child: RawScrollbar( child: RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey, thumbColor: Colors.grey,
controller: _vertical, controller: _vertical,
thumbVisibility: false, thumbVisibility: false,

View File

@@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -55,6 +56,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
RemoteCountState.init(); RemoteCountState.init();
peerId = params['id']; peerId = params['id'];
final sessionId = params['session_id']; final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id'];
if (peerId != null) { if (peerId != null) {
ConnectionTypeState.init(peerId!); ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) { tabController.onSelected = (id) {
@@ -77,6 +79,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId!, id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId), sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
password: params['password'], password: params['password'],
toolbarState: _toolbarState, toolbarState: _toolbarState,
tabController: tabController, tabController: tabController,
@@ -98,13 +101,20 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
print( print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId // for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) { if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
final switchUuid = args['switch_uuid']; final switchUuid = args['switch_uuid'];
final sessionId = args['session_id']; final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
windowOnTop(windowId()); windowOnTop(windowId());
if (tabController.length == 0) {
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
stateGlobal.setFullscreen(true);
}
}
ConnectionTypeState.init(id); ConnectionTypeState.init(id);
_toolbarState.setShow( _toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y'); bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
@@ -118,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
sessionId: sessionId == null ? null : SessionID(sessionId), sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
password: args['password'], password: args['password'],
toolbarState: _toolbarState, toolbarState: _toolbarState,
tabController: tabController, tabController: tabController,
@@ -147,12 +158,24 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}') .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList() .toList()
.join(';'); .join(';');
} else if (call.method == kWindowEventCloseForSeparateWindow) { } else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final peerId = call.arguments; final peerId = call.arguments;
closeSessionOnDispose[peerId] = false; try {
tabController.closeBy(peerId); final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == peerId)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (returnValue != null) {
closeSessionOnDispose[peerId] = false;
tabController.closeBy(peerId);
}
} }
_update_remote_count(); _update_remote_count();
return returnValue;
}); });
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
restoreWindowPosition( restoreWindowPosition(
@@ -187,7 +210,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
onWindowCloseButton: handleWindowCloseButton, onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10), tail: const AddButton().paddingOnly(left: 10),
pageViewBuilder: (pageView) => pageView, pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.labelGetterAlias, labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() { tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key); final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) { if (!connectionType.isValid()) {
@@ -249,7 +272,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
return; return;
} }
if (e.buttons == 2) { final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as RemotePage;
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
e.buttons == 2) {
showRightMenu( showRightMenu(
(CancelFunc cancelFunc) { (CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc); return _tabMenuBuilder(key, cancelFunc);
@@ -337,7 +364,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
)); ));
} }
if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {} if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {
menu.add(RemoteMenuEntry.insertLock(sessionId, padding,
dismissFunc: cancelFunc));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(RemoteMenuEntry.insertCtrlAltDel(sessionId, padding,
dismissFunc: cancelFunc));
}
}
menu.addAll([ menu.addAll([
MenuEntryDivider<String>(), MenuEntryDivider<String>(),
@@ -380,7 +415,24 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
void onRemoveId(String id) async { void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) { if (tabController.state.value.tabs.isEmpty) {
await WindowController.fromWindowId(windowId()).close(); stateGlobal.setFullscreen(false, procWnd: false);
// Keep calling until the window status is hidden.
//
// Workaround for Windows:
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
// `await WindowController.fromWindowId(windowId()).close();`.
Future<void> loopCloseWindow() async {
int c = 0;
final windowController = WindowController.fromWindowId(windowId());
while (c < 20 &&
tabController.state.value.tabs.isEmpty &&
(!await windowController.isHidden())) {
await windowController.close();
await Future.delayed(Duration(milliseconds: 100));
c++;
}
}
loopCloseWindow();
} }
ConnectionTypeState.delete(id); ConnectionTypeState.delete(id);
_update_remote_count(); _update_remote_count();

View File

@@ -2,6 +2,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@@ -9,12 +10,14 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/chat_page.dart'; import '../../common/widgets/chat_page.dart';
import '../../models/file_model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../models/server_model.dart'; import '../../models/server_model.dart';
@@ -32,6 +35,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
void initState() { void initState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, ""); gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this); windowManager.addListener(this);
Get.put(tabController);
tabController.onRemoved = (_, id) { tabController.onRemoved = (_, id) {
onRemoveId(id); onRemoveId(id);
}; };
@@ -111,6 +115,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
}); });
} }
windowManager.setTitle(getWindowNameWithId(client.peerId)); windowManager.setTitle(getWindowNameWithId(client.peerId));
gFFI.cmFileModel.updateCurrentClientId(client.id);
} }
} }
}; };
@@ -170,40 +175,65 @@ class ConnectionManagerState extends State<ConnectionManager> {
], ],
); );
}, },
pageViewBuilder: (pageView) => Row( pageViewBuilder: (pageView) => LayoutBuilder(
children: [ builder: (context, constrains) {
Consumer<ChatModel>( var borderWidth = 0.0;
builder: (_, model, child) => model.isShowCMChatPage if (constrains.maxWidth >
? Expanded( kConnectionManagerWindowSizeClosedChat.width) {
child: buildRemoteBlock( borderWidth = kConnectionManagerWindowSizeOpenChat.width -
child: Container( constrains.maxWidth;
decoration: BoxDecoration( } else {
border: Border( borderWidth = kConnectionManagerWindowSizeClosedChat.width -
right: BorderSide( constrains.maxWidth;
color: Theme.of(context) }
.dividerColor))), if (borderWidth < 0 || borderWidth > 50) {
child: borderWidth = 0;
ChatPage(type: ChatPageType.desktopCM)), }
), final realClosedWidth =
flex: (kConnectionManagerWindowSizeOpenChat.width - kConnectionManagerWindowSizeClosedChat.width -
kConnectionManagerWindowSizeClosedChat borderWidth;
.width) final realChatPageWidth =
.toInt(), constrains.maxWidth - realClosedWidth;
) return Row(children: [
: Offstage(), if (constrains.maxWidth >
), kConnectionManagerWindowSizeClosedChat.width)
Expanded( Consumer<ChatModel>(
child: pageView, builder: (_, model, child) => SizedBox(
flex: kConnectionManagerWindowSizeClosedChat.width width: realChatPageWidth,
.toInt() - child: buildRemoteBlock(
4 // prevent stretch of the page view when chat is open, child: Container(
), decoration: BoxDecoration(
], border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child: buildSidePage()),
),
)),
SizedBox(
width: realClosedWidth,
child:
SizedBox(width: realClosedWidth, child: pageView)),
]);
},
), ),
), ),
); );
} }
Widget buildSidePage() {
final selected = gFFI.serverModel.tabController.state.value.selected;
if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
return Offstage();
}
final clientType = gFFI.serverModel.clients[selected].type_();
if (clientType == ClientType.file) {
return _FileTransferLogPage();
} else {
return ChatPage(type: ChatPageType.desktopCM);
}
}
Widget buildTitleBar() { Widget buildTitleBar() {
return SizedBox( return SizedBox(
height: kDesktopRemoteTabBarHeight, height: kDesktopRemoteTabBarHeight,
@@ -447,14 +477,21 @@ class _CmHeaderState extends State<_CmHeader>
), ),
), ),
Offstage( Offstage(
offstage: !client.authorized || client.type_() != ClientType.remote, offstage: !client.authorized ||
(client.type_() != ClientType.remote &&
client.type_() != ClientType.file),
child: IconButton( child: IconButton(
onPressed: () => checkClickTime( onPressed: () => checkClickTime(client.id, () {
client.id, if (client.type_() != ClientType.file) {
() => gFFI.chatModel gFFI.chatModel.toggleCMSidePage();
.toggleCMChatPage(MessageKey(client.peerId, client.id)), } else {
), gFFI.chatModel
icon: SvgPicture.asset('assets/chat2.svg'), .toggleCMChatPage(MessageKey(client.peerId, client.id));
}
}),
icon: SvgPicture.asset(client.type_() == ClientType.file
? 'assets/file_transfer.svg'
: 'assets/chat2.svg'),
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
), ),
) )
@@ -912,3 +949,181 @@ void checkClickTime(int id, Function() callback) async {
if (d > 120) callback(); if (d > 120) callback();
}); });
} }
class _FileTransferLogPage extends StatefulWidget {
_FileTransferLogPage({Key? key}) : super(key: key);
@override
State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
}
class __FileTransferLogPageState extends State<_FileTransferLogPage> {
@override
Widget build(BuildContext context) {
return statusList();
}
Widget generateCard(Widget child) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(
Radius.circular(15.0),
),
),
child: child,
);
}
Widget statusList() {
return PreferredSize(
preferredSize: const Size(200, double.infinity),
child: Container(
padding: const EdgeInsets.all(12.0),
child: Obx(
() {
final jobTable = gFFI.cmFileModel.currentJobTable;
statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(),
itemBuilder: (BuildContext context, int index) {
final item = jobs[index];
return Padding(
padding: const EdgeInsets.only(bottom: 5),
child: generateCard(
Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 50,
child: Column(
children: [
Transform.rotate(
angle: item.isRemoteToLocal ? 0 : pi,
child: SvgPicture.asset(
"assets/arrow.svg",
color: Theme.of(context)
.tabBarTheme
.labelColor,
),
),
Text(item.isRemoteToLocal
? translate('Send')
: translate('Receive'))
],
),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item.fileName,
).paddingSymmetric(vertical: 10),
if (item.totalSize > 0)
Text(
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
if (item.totalSize > 0)
Offstage(
offstage: item.state !=
JobState.inProgress,
child: Text(
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage:
item.state == JobState.inProgress,
child: Text(
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
if (item.totalSize > 0)
Offstage(
offstage: item.state !=
JobState.inProgress,
child: LinearPercentIndicator(
padding:
EdgeInsets.only(right: 15),
animateFromLastPercent: true,
center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
),
barRadius: Radius.circular(15),
percent: item.finishedSize /
item.totalSize,
progressColor: MyTheme.accent,
backgroundColor:
Theme.of(context).hoverColor,
lineHeight:
kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [],
),
],
),
],
).paddingSymmetric(vertical: 10),
),
);
},
itemCount: jobTable.length,
);
return jobTable.isEmpty
? generateCard(
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/transfer.svg",
color: Theme.of(context).tabBarTheme.labelColor,
height: 40,
).paddingOnly(bottom: 10),
Text(
translate("No transfers in progress"),
textAlign: TextAlign.center,
textScaleFactor: 1.20,
style: TextStyle(
color:
Theme.of(context).tabBarTheme.labelColor),
),
],
),
),
)
: statusListView(jobTable);
},
)),
);
}
}

View File

@@ -101,6 +101,9 @@ class ToolbarState {
class _ToolbarTheme { class _ToolbarTheme {
static const Color blueColor = MyTheme.button; static const Color blueColor = MyTheme.button;
static const Color hoverBlueColor = MyTheme.accent; static const Color hoverBlueColor = MyTheme.accent;
static Color inactiveColor = Colors.grey[800]!;
static Color hoverInactiveColor = Colors.grey[850]!;
static const Color redColor = Colors.redAccent; static const Color redColor = Colors.redAccent;
static const Color hoverRedColor = Colors.red; static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension // kMinInteractiveDimension
@@ -543,9 +546,11 @@ class _PinMenu extends StatelessWidget {
assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg", assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar', tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar',
onPressed: state.switchPin, onPressed: state.switchPin,
color: state.pin ? _ToolbarTheme.blueColor : Colors.grey[800]!, color:
hoverColor: state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
state.pin ? _ToolbarTheme.hoverBlueColor : Colors.grey[850]!, hoverColor: state.pin
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
), ),
); );
} }
@@ -558,13 +563,18 @@ class _MobileActionMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!ffi.ffiModel.isPeerAndroid) return Offstage(); if (!ffi.ffiModel.isPeerAndroid) return Offstage();
return _IconMenuButton( return Obx(() => _IconMenuButton(
assetName: 'assets/actions_mobile.svg', assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions', tooltip: 'Mobile Actions',
onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), onPressed: () =>
color: _ToolbarTheme.blueColor, ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
hoverColor: _ToolbarTheme.hoverBlueColor, color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
); ? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor,
hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
));
} }
} }
@@ -765,13 +775,14 @@ class ScreenAdjustor {
} }
await WindowController.fromWindowId(windowId) await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height)); .setFrame(Rect.fromLTWH(left, top, width, height));
stateGlobal.setMaximized(false);
} }
} }
updateScreen() async { updateScreen() async {
final v = await rustDeskWinManager.call( final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, ''); WindowType.Main, kWindowGetWindowInfo, '');
final String valueStr = v; final String valueStr = v.result;
if (valueStr.isEmpty) { if (valueStr.isEmpty) {
_screen = null; _screen = null;
} else { } else {
@@ -1042,10 +1053,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FfiModel get ffiModel => widget.ffi.ffiModel; FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display; Display get display => ffiModel.display;
List<Resolution> get resolutions => pi.resolutions; List<Resolution> get resolutions => pi.resolutions;
bool get isWayland => bind.mainCurrentIsWayland();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_getLocalResolutionWayland();
} }
@override @override
@@ -1054,7 +1067,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
final visible = final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1); ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage(); if (!visible) return Offstage();
_getLocalResolution();
final showOriginalBtn = final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution; display.isOriginalResolutionSet && !display.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal(); final showFitLocalBtn = !_isRemoteResolutionFitLocal();
@@ -1090,6 +1102,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
); );
} }
Future<void> _getLocalResolutionWayland() async {
if (!isWayland) return _getLocalResolution();
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
}
}
_getLocalResolution() { _getLocalResolution() {
_localResolution = null; _localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay(); final String currentDisplay = bind.mainGetCurrentDisplay();
@@ -1299,23 +1325,25 @@ class _KeyboardMenu extends StatelessWidget {
color: _ToolbarTheme.blueColor, color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor, hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [ menuChildren: [
mode(modeOnly), keyboardMode(modeOnly),
localKeyboardType(), localKeyboardType(),
Divider(), Divider(),
view_mode(), viewMode(),
Divider(),
reverseMouseWheel(),
]); ]);
} }
mode(String? modeOnly) { keyboardMode(String? modeOnly) {
return futureBuilder(future: () async { return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ?? return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
_kKeyLegacyMode; _kKeyLegacyMode;
}(), hasData: (data) { }(), hasData: (data) {
final groupValue = data as String; final groupValue = data as String;
List<KeyboardModeMenu> modes = [ List<InputModeMenu> modes = [
KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), InputModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), InputModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), InputModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
]; ];
List<RdoMenuButton> list = []; List<RdoMenuButton> list = [];
final enabled = !ffi.ffiModel.viewOnly; final enabled = !ffi.ffiModel.viewOnly;
@@ -1325,7 +1353,7 @@ class _KeyboardMenu extends StatelessWidget {
sessionId: ffi.sessionId, value: value); sessionId: ffi.sessionId, value: value);
} }
for (KeyboardModeMenu mode in modes) { for (InputModeMenu mode in modes) {
if (modeOnly != null && mode.key != modeOnly) { if (modeOnly != null && mode.key != modeOnly) {
continue; continue;
} else if (!bind.sessionIsKeyboardModeSupported( } else if (!bind.sessionIsKeyboardModeSupported(
@@ -1374,7 +1402,7 @@ class _KeyboardMenu extends StatelessWidget {
); );
} }
view_mode() { viewMode() {
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
return CkbMenuButton( return CkbMenuButton(
@@ -1390,6 +1418,30 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi, ffi: ffi,
child: Text(translate('View Mode'))); child: Text(translate('View Mode')));
} }
reverseMouseWheel() {
return futureBuilder(future: () async {
final v =
await bind.sessionGetReverseMouseWheel(sessionId: ffi.sessionId);
if (v != null && v != '') {
return v;
}
return bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
}(), hasData: (data) {
final enabled = !ffi.ffiModel.viewOnly;
onChanged(bool? value) async {
if (value == null) return;
await bind.sessionSetReverseMouseWheel(
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
}
return CkbMenuButton(
value: data == 'Y',
onChanged: enabled ? onChanged : null,
child: Text(translate('Reverse mouse wheel')),
ffi: ffi);
});
}
} }
class _ChatMenu extends StatefulWidget { class _ChatMenu extends StatefulWidget {
@@ -1587,26 +1639,26 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
width: _ToolbarTheme.buttonSize, width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize, height: _ToolbarTheme.buttonSize,
child: MenuItemButton( child: MenuItemButton(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.transparent), backgroundColor: MaterialStatePropertyAll(Colors.transparent),
padding: MaterialStatePropertyAll(EdgeInsets.zero), padding: MaterialStatePropertyAll(EdgeInsets.zero),
overlayColor: MaterialStatePropertyAll(Colors.transparent)), overlayColor: MaterialStatePropertyAll(Colors.transparent)),
onHover: (value) => setState(() { onHover: (value) => setState(() {
hover = value; hover = value;
}), }),
onPressed: widget.onPressed, onPressed: widget.onPressed,
child: Tooltip( child: Tooltip(
message: translate(widget.tooltip), message: translate(widget.tooltip),
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_ToolbarTheme.iconRadius), borderRadius:
color: hover ? widget.hoverColor : widget.color, BorderRadius.circular(_ToolbarTheme.iconRadius),
), color: hover ? widget.hoverColor : widget.color,
child: icon)), ),
) child: icon)),
), )),
).marginSymmetric( ).marginSymmetric(
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin, horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin); vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
@@ -1670,18 +1722,17 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
onHover: (value) => setState(() { onHover: (value) => setState(() {
hover = value; hover = value;
}), }),
child: Tooltip( child: Tooltip(
message: translate(widget.tooltip), message: translate(widget.tooltip),
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
BorderRadius.circular(_ToolbarTheme.iconRadius), BorderRadius.circular(_ToolbarTheme.iconRadius),
color: hover ? widget.hoverColor : widget.color, color: hover ? widget.hoverColor : widget.color,
), ),
child: icon)) child: icon))),
),
menuChildren: widget.menuChildren menuChildren: widget.menuChildren
.map((e) => _buildPointerTrackWidget(e, widget.ffi)) .map((e) => _buildPointerTrackWidget(e, widget.ffi))
.toList())); .toList()));
@@ -1968,11 +2019,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
} }
} }
class KeyboardModeMenu { class InputModeMenu {
final String key; final String key;
final String menu; final String menu;
KeyboardModeMenu({required this.key, required this.menu}); InputModeMenu({required this.key, required this.menu});
} }
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); _menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();

View File

@@ -8,7 +8,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
@@ -77,7 +76,7 @@ CancelFunc showRightMenu(ToastBuilder builder,
targetContext: context, targetContext: context,
verticalOffset: 0, verticalOffset: 0,
horizontalOffset: 0, horizontalOffset: 0,
duration: Duration(seconds: 4), duration: Duration(seconds: 300),
animationDuration: Duration(milliseconds: 0), animationDuration: Duration(milliseconds: 0),
animationReverseDuration: Duration(milliseconds: 0), animationReverseDuration: Duration(milliseconds: 0),
preferDirection: PreferDirection.rightTop, preferDirection: PreferDirection.rightTop,
@@ -267,13 +266,9 @@ class DesktopTab extends StatelessWidget {
tabType == DesktopTabType.install; tabType == DesktopTabType.install;
} }
static RxString labelGetterAlias(String peerId) { static RxString tablabelGetter(String peerId) {
final opt = 'alias'; final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
PeerStringOption.init(peerId, opt, () { return RxString(getDesktopTabLabel(peerId, alias));
final alias = bind.mainGetPeerOptionSync(id: peerId, key: opt);
return alias.isEmpty ? peerId : alias;
});
return PeerStringOption.find(peerId, opt);
} }
@override @override
@@ -440,7 +435,6 @@ class DesktopTab extends StatelessWidget {
tabType: tabType, tabType: tabType,
state: state, state: state,
tail: tail, tail: tail,
isMaximized: stateGlobal.isMaximized,
showMinimize: showMinimize, showMinimize: showMinimize,
showMaximize: showMaximize, showMaximize: showMaximize,
showClose: showClose, showClose: showClose,
@@ -455,7 +449,6 @@ class WindowActionPanel extends StatefulWidget {
final bool isMainWindow; final bool isMainWindow;
final DesktopTabType tabType; final DesktopTabType tabType;
final Rx<DesktopTabState> state; final Rx<DesktopTabState> state;
final RxBool isMaximized;
final bool showMinimize; final bool showMinimize;
final bool showMaximize; final bool showMaximize;
@@ -468,7 +461,6 @@ class WindowActionPanel extends StatefulWidget {
required this.isMainWindow, required this.isMainWindow,
required this.tabType, required this.tabType,
required this.state, required this.state,
required this.isMaximized,
this.tail, this.tail,
this.showMinimize = true, this.showMinimize = true,
this.showMaximize = true, this.showMaximize = true,
@@ -485,6 +477,8 @@ class WindowActionPanel extends StatefulWidget {
class WindowActionPanelState extends State<WindowActionPanel> class WindowActionPanelState extends State<WindowActionPanel>
with MultiWindowListener, WindowListener { with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1)); final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
@override @override
void initState() { void initState() {
@@ -495,18 +489,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
Future.delayed(Duration(milliseconds: 500), () { Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) { if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) { windowManager.isMaximized().then((maximized) {
if (widget.isMaximized.value != maximized) { if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized)); (_) => setState(() => stateGlobal.setMaximized(maximized)));
} }
}); });
} else { } else {
final wc = WindowController.fromWindowId(kWindowId!); final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) { wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized"); debugPrint("isMaximized $maximized");
if (widget.isMaximized.value != maximized) { if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized)); (_) => setState(() => stateGlobal.setMaximized(maximized)));
} }
}); });
} }
@@ -517,6 +511,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
void dispose() { void dispose() {
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this); windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -535,10 +530,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override @override
void onWindowMaximize() { void onWindowMaximize() {
// catch maximize from system
if (!widget.isMaximized.value) {
widget.isMaximized.value = true;
}
stateGlobal.setMinimized(false); stateGlobal.setMinimized(false);
_setMaximized(true); _setMaximized(true);
super.onWindowMaximize(); super.onWindowMaximize();
@@ -546,10 +537,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override @override
void onWindowUnmaximize() { void onWindowUnmaximize() {
// catch unmaximize from system
if (widget.isMaximized.value) {
widget.isMaximized.value = false;
}
stateGlobal.setMinimized(false); stateGlobal.setMinimized(false);
_setMaximized(false); _setMaximized(false);
super.onWindowUnmaximize(); super.onWindowUnmaximize();
@@ -577,6 +564,33 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override @override
void onWindowClose() async { void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
await controller.hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
}
macOSWindowClose(
Future<void> Function() restoreFunc,
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc) async {
await restoreFunc();
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
_macOSCheckRestoreCounter++;
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
_macOSCheckRestoreTimer?.cancel();
_macOSCheckRestoreTimer = null;
Timer(Duration(milliseconds: 700), () async => await closeFunc());
}
});
}
// hide window on close // hide window on close
if (widget.isMainWindow) { if (widget.isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
@@ -584,23 +598,28 @@ class WindowActionPanelState extends State<WindowActionPanel>
} }
// macOS specific workaround, the window is not hiding when in fullscreen. // macOS specific workaround, the window is not hiding when in fullscreen.
if (Platform.isMacOS && await windowManager.isFullScreen()) { if (Platform.isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false); stateGlobal.closeOnFullscreen = true;
await Future.delayed(Duration(seconds: 1)); await macOSWindowClose(
() async => await windowManager.setFullScreen(false),
() async => await windowManager.isFullScreen(),
mainWindowClose);
} else {
stateGlobal.closeOnFullscreen = false;
await mainWindowClose();
} }
await windowManager.hide();
} else { } else {
// it's safe to hide the subwindow // it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!); final controller = WindowController.fromWindowId(kWindowId!);
if (Platform.isMacOS && await controller.isFullScreen()) { if (Platform.isMacOS && await controller.isFullScreen()) {
await controller.setFullscreen(false); stateGlobal.closeOnFullscreen = true;
await Future.delayed(Duration(seconds: 1)); await macOSWindowClose(
() async => await controller.setFullscreen(false),
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller));
} else {
stateGlobal.closeOnFullscreen = false;
await notMainWindowClose(controller);
} }
await controller.hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
} }
super.onWindowClose(); super.onWindowClose();
} }
@@ -632,9 +651,10 @@ class WindowActionPanelState extends State<WindowActionPanel>
Offstage( Offstage(
offstage: !widget.showMaximize || Platform.isMacOS, offstage: !widget.showMaximize || Platform.isMacOS,
child: Obx(() => ActionIcon( child: Obx(() => ActionIcon(
message: message: stateGlobal.isMaximized.isTrue
widget.isMaximized.value ? 'Restore' : 'Maximize', ? 'Restore'
icon: widget.isMaximized.value : 'Maximize',
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore ? IconFont.restore
: IconFont.max, : IconFont.max,
onTap: _toggleMaximize, onTap: _toggleMaximize,
@@ -671,10 +691,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
void _toggleMaximize() { void _toggleMaximize() {
toggleMaximize(widget.isMainWindow).then((maximize) { toggleMaximize(widget.isMainWindow).then((maximize) {
if (widget.isMaximized.value != maximize) { // update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
// update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize stateGlobal.setMaximized(maximize);
widget.isMaximized.value = maximize;
}
}); });
} }
} }
@@ -898,14 +916,17 @@ class _TabState extends State<_Tab> with RestorationMixin {
final labelWidget = Obx(() { final labelWidget = Obx(() {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Text( child: Tooltip(
translate(widget.label.value), message: translate(widget.label.value),
textAlign: TextAlign.center, child: Text(
style: TextStyle( translate(widget.label.value),
color: isSelected textAlign: TextAlign.center,
? MyTheme.tabbar(context).selectedTextColor style: TextStyle(
: MyTheme.tabbar(context).unSelectedTextColor), color: isSelected
overflow: TextOverflow.ellipsis, ? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor),
overflow: TextOverflow.ellipsis,
),
)); ));
}); });

View File

@@ -125,7 +125,7 @@ void runMainApp(bool startService) async {
bind.pluginSyncUi(syncTo: kAppTypeMain); bind.pluginSyncUi(syncTo: kAppTypeMain);
bind.pluginListReload(); bind.pluginListReload();
} }
gFFI.abModel.loadCache(); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
// Set window option. // Set window option.
@@ -153,7 +153,7 @@ void runMobileApp() async {
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit(); if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath(); platformFFI.syncAndroidServiceAppDirConfigPath();
gFFI.abModel.loadCache(); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
} }
@@ -223,6 +223,7 @@ void runConnectionManagerScreen(bool hide) async {
const DesktopServerPage(), const DesktopServerPage(),
MyTheme.currentThemeMode(), MyTheme.currentThemeMode(),
); );
gFFI.serverModel.hideCm = hide;
if (hide) { if (hide) {
await hideCmWindow(isStartup: true); await hideCmWindow(isStartup: true);
} else { } else {
@@ -233,19 +234,24 @@ void runConnectionManagerScreen(bool hide) async {
listenUniLinks(handleByFlutter: false); listenUniLinks(handleByFlutter: false);
} }
bool _isCmReadyToShow = false;
showCmWindow({bool isStartup = false}) async { showCmWindow({bool isStartup = false}) async {
if (isStartup) { if (isStartup) {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions( WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat); size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker(); bind.mainHideDocker();
await windowManager.show(); await Future.wait([
await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]); windowManager.show(),
// ensure initial window size to be changed windowManager.focus(),
await windowManager.setSizeAlignment( windowManager.setOpacity(1)
kConnectionManagerWindowSizeClosedChat, Alignment.topRight); ]);
}); // ensure initial window size to be changed
} else { await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 1) { if (await windowManager.getOpacity() != 1) {
await windowManager.setOpacity(1); await windowManager.setOpacity(1);
await windowManager.focus(); await windowManager.focus();
@@ -262,16 +268,18 @@ hideCmWindow({bool isStartup = false}) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions( WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat); size: kConnectionManagerWindowSizeClosedChat);
windowManager.setOpacity(0); windowManager.setOpacity(0);
windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
});
} else {
await windowManager.setOpacity(0);
bind.mainHideDocker(); bind.mainHideDocker();
await windowManager.minimize(); await windowManager.minimize();
await windowManager.hide(); await windowManager.hide();
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 0) {
await windowManager.setOpacity(0);
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
}
} }
} }
@@ -395,7 +403,7 @@ class _AppState extends State<App> {
themeMode: MyTheme.currentThemeMode(), themeMode: MyTheme.currentThemeMode(),
home: isDesktop home: isDesktop
? const DesktopTabPage() ? const DesktopTabPage()
: !isAndroid : isWeb
? WebHomePage() ? WebHomePage()
: HomePage(), : HomePage(),
localizationsDelegates: const [ localizationsDelegates: const [

View File

@@ -28,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection"); final title = translate("Connection");
@override @override
final appBarActions = !isAndroid ? <Widget>[const WebMenu()] : <Widget>[]; final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
@override @override
State<ConnectionPage> createState() => _ConnectionPageState(); State<ConnectionPage> createState() => _ConnectionPageState();
@@ -57,7 +57,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}(); }();
} }
if (isAndroid) { if (isAndroid) {
Timer(const Duration(seconds: 5), () async { Timer(const Duration(seconds: 1), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl(); _updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (_updateUrl.isNotEmpty) setState(() {}); if (_updateUrl.isNotEmpty) setState(() {});
}); });
@@ -80,7 +80,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
_buildRemoteIDTextField(), _buildRemoteIDTextField(),
])), ])),
SliverFillRemaining( SliverFillRemaining(
hasScrollBody: false, hasScrollBody: true,
child: PeerTabPage(), child: PeerTabPage(),
) )
], ],
@@ -211,25 +211,6 @@ class WebMenu extends StatefulWidget {
} }
class _WebMenuState extends State<WebMenu> { class _WebMenuState extends State<WebMenu> {
String url = "";
@override
void initState() {
super.initState();
() async {
final urlRes = await bind.mainGetApiServer();
var update = false;
if (urlRes != url) {
url = urlRes;
update = true;
}
if (update) {
setState(() {});
}
}();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Provider.of<FfiModel>(context); Provider.of<FfiModel>(context);
@@ -251,16 +232,14 @@ class _WebMenuState extends State<WebMenu> {
child: Text(translate('ID/Relay Server')), child: Text(translate('ID/Relay Server')),
) )
] + ] +
(url.contains('admin.rustdesk.com') [
? <PopupMenuItem<String>>[] PopupMenuItem(
: [ value: "login",
PopupMenuItem( child: Text(gFFI.userModel.userName.value.isEmpty
value: "login", ? translate("Login")
child: Text(gFFI.userModel.userName.value.isEmpty : '${translate("Logout")} (${gFFI.userModel.userName.value})'),
? translate("Login") )
: '${translate("Logout")} (${gFFI.userModel.userName.value})'), ] +
)
]) +
[ [
PopupMenuItem( PopupMenuItem(
value: "about", value: "about",

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/overlay.dart';
import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -26,7 +25,9 @@ class _HomePageState extends State<HomePage> {
var _selectedIndex = 0; var _selectedIndex = 0;
int get selectedIndex => _selectedIndex; int get selectedIndex => _selectedIndex;
final List<PageShape> _pages = []; final List<PageShape> _pages = [];
final _blockableOverlayState = BlockableOverlayState(); bool get isChatPageCurrentTab => isAndroid
? _selectedIndex == 1
: false; // change this when ios have chat page
void refreshPages() { void refreshPages() {
setState(() { setState(() {
@@ -38,7 +39,6 @@ class _HomePageState extends State<HomePage> {
void initState() { void initState() {
super.initState(); super.initState();
initPages(); initPages();
_blockableOverlayState.applyFfi(gFFI);
} }
void initPages() { void initPages() {
@@ -82,13 +82,15 @@ class _HomePageState extends State<HomePage> {
unselectedItemColor: MyTheme.darkGray, unselectedItemColor: MyTheme.darkGray,
onTap: (index) => setState(() { onTap: (index) => setState(() {
// close chat overlay when go chat page // close chat overlay when go chat page
if (index == 1 && _selectedIndex != index) { if (_selectedIndex != index) {
gFFI.chatModel.hideChatIconOverlay(); _selectedIndex = index;
gFFI.chatModel.hideChatWindowOverlay(); if (isChatPageCurrentTab) {
gFFI.chatModel gFFI.chatModel.hideChatIconOverlay();
.mobileClearClientUnread(gFFI.chatModel.currentKey.connId); gFFI.chatModel.hideChatWindowOverlay();
gFFI.chatModel.mobileClearClientUnread(
gFFI.chatModel.currentKey.connId);
}
} }
_selectedIndex = index;
}), }),
), ),
body: _pages.elementAt(_selectedIndex), body: _pages.elementAt(_selectedIndex),
@@ -98,7 +100,7 @@ class _HomePageState extends State<HomePage> {
Widget appTitle() { Widget appTitle() {
final currentUser = gFFI.chatModel.currentUser; final currentUser = gFFI.chatModel.currentUser;
final currentKey = gFFI.chatModel.currentKey; final currentKey = gFFI.chatModel.currentKey;
if (_selectedIndex == 1 && if (isChatPageCurrentTab &&
currentUser != null && currentUser != null &&
currentKey.peerId.isNotEmpty) { currentKey.peerId.isNotEmpty) {
final connected = final connected =

View File

@@ -39,6 +39,8 @@ class _RemotePageState extends State<RemotePage> {
String _value = ''; String _value = '';
Orientation? _currentOrientation; Orientation? _currentOrientation;
final _blockableOverlayState = BlockableOverlayState();
final keyboardVisibilityController = KeyboardVisibilityController(); final keyboardVisibilityController = KeyboardVisibilityController();
late final StreamSubscription<bool> keyboardSubscription; late final StreamSubscription<bool> keyboardSubscription;
final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _mobileFocusNode = FocusNode();
@@ -67,6 +69,8 @@ class _RemotePageState extends State<RemotePage> {
initSharedStates(widget.id); initSharedStates(widget.id);
gFFI.chatModel gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
_blockableOverlayState.applyFfi(gFFI);
} }
@override @override
@@ -88,6 +92,19 @@ class _RemotePageState extends State<RemotePage> {
removeSharedStates(widget.id); removeSharedStates(widget.id);
} }
// to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
// But I don't know why and how to fix it.
Widget emptyOverlay(Color bgColor) => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: bgColor,
),
);
void onSoftKeyboardChanged(bool visible) { void onSoftKeyboardChanged(bool visible) {
if (!visible) { if (!visible) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -198,13 +215,19 @@ class _RemotePageState extends State<RemotePage> {
}); });
} }
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
Widget _bottomWidget() => _showGestureHelp
? getGestureHelp()
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar(keyboard)
: Offstage());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pi = Provider.of<FfiModel>(context).pi;
final keyboardIsVisible = final keyboardIsVisible =
keyboardVisibilityController.isVisible && _showEdit; keyboardVisibilityController.isVisible && _showEdit;
final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
@@ -241,11 +264,22 @@ class _RemotePageState extends State<RemotePage> {
} }
}); });
}), }),
bottomNavigationBar: _showGestureHelp bottomNavigationBar: Obx(() => Stack(
? getGestureHelp() alignment: Alignment.bottomCenter,
: (_showBar && pi.displays.isNotEmpty children: [
? getBottomAppBar(keyboard) gFFI.ffiModel.pi.isSet.isTrue &&
: null), gFFI.ffiModel.waitForFirstImage.isTrue
? emptyOverlay(MyTheme.canvasColor)
: () {
gFFI.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
_bottomWidget(),
gFFI.ffiModel.pi.isSet.isFalse
? emptyOverlay(MyTheme.canvasColor)
: Offstage(),
],
)),
body: Overlay( body: Overlay(
initialEntries: [ initialEntries: [
OverlayEntry(builder: (context) { OverlayEntry(builder: (context) {
@@ -284,12 +318,17 @@ class _RemotePageState extends State<RemotePage> {
Widget getRawPointerAndKeyBody(Widget child) { Widget getRawPointerAndKeyBody(Widget child) {
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
return RawPointerMouseRegion( return RawPointerMouseRegion(
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
inputModel: inputModel, inputModel: inputModel,
child: RawKeyFocusScope( // Disable RawKeyFocusScope before the connecting is established.
focusNode: _physicalFocusNode, // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
inputModel: inputModel, child: gFFI.ffiModel.pi.isSet.isTrue
child: child)); ? RawKeyFocusScope(
focusNode: _physicalFocusNode,
inputModel: inputModel,
child: child)
: child,
);
} }
Widget getBottomAppBar(bool keyboard) { Widget getBottomAppBar(bool keyboard) {
@@ -368,17 +407,23 @@ class _RemotePageState extends State<RemotePage> {
}, },
), ),
]), ]),
IconButton( Obx(() => IconButton(
color: Colors.white, color: Colors.white,
icon: Icon(Icons.expand_more), icon: Icon(Icons.expand_more),
onPressed: () { onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
setState(() => _showBar = !_showBar); ? null
}), : () {
setState(() => _showBar = !_showBar);
},
)),
], ],
), ),
); );
} }
bool get showCursorPaint =>
!gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded;
Widget getBodyForMobile() { Widget getBodyForMobile() {
final keyboardIsVisible = keyboardVisibilityController.isVisible; final keyboardIsVisible = keyboardVisibilityController.isVisible;
return Container( return Container(
@@ -411,7 +456,7 @@ class _RemotePageState extends State<RemotePage> {
), ),
), ),
]; ];
if (!gFFI.canvasModel.cursorEmbedded) { if (showCursorPaint) {
paints.add(CursorPaint()); paints.add(CursorPaint());
} }
return paints; return paints;
@@ -420,7 +465,7 @@ class _RemotePageState extends State<RemotePage> {
Widget getBodyForDesktopWithListener(bool keyboard) { Widget getBodyForDesktopWithListener(bool keyboard) {
var paints = <Widget>[ImagePaint()]; var paints = <Widget>[ImagePaint()];
if (!gFFI.canvasModel.cursorEmbedded) { if (showCursorPaint) {
final cursor = bind.sessionGetToggleOptionSync( final cursor = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor'); sessionId: sessionId, arg: 'show-remote-cursor');
if (keyboard || cursor) { if (keyboard || cursor) {
@@ -466,7 +511,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.ffiModel.toggleTouchMode(); gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : ''; final v = gFFI.ffiModel.touchMode ? 'Y' : '';
bind.sessionPeerOption( bind.sessionPeerOption(
sessionId: sessionId, name: "touch", value: v); sessionId: sessionId, name: "touch-mode", value: v);
}))); })));
} }
@@ -695,8 +740,8 @@ class CursorPaint extends StatelessWidget {
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: m.image ?? preDefaultCursor.image, image: m.image ?? preDefaultCursor.image,
x: m.x * s - hotx * s + c.x, x: m.x * s - hotx + c.x,
y: m.y * s - hoty * s + c.y - adjust, y: m.y * s - hoty + c.y - adjust,
scale: 1), scale: 1),
); );
} }

View File

@@ -210,11 +210,216 @@ class ServiceNotRunningNotification extends StatelessWidget {
.marginOnly(bottom: 8), .marginOnly(bottom: 8),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.play_arrow),
onPressed: serverModel.toggleService, onPressed: () {
if (gFFI.userModel.userName.value.isEmpty && bind.mainGetLocalOption(key: "show-scam-warning") != "N") {
_showScamWarning(context, serverModel);
} else {
serverModel.toggleService();
}
},
label: Text(translate("Start Service"))) label: Text(translate("Start Service")))
], ],
)); ));
} }
void _showScamWarning(BuildContext context, ServerModel serverModel) {
showDialog(
context: context,
builder: (BuildContext context) {
return ScamWarningDialog(serverModel: serverModel);
},
);
}
}
class ScamWarningDialog extends StatefulWidget {
final ServerModel serverModel;
ScamWarningDialog({required this.serverModel});
@override
_ScamWarningDialogState createState() => _ScamWarningDialogState();
}
class _ScamWarningDialogState extends State<ScamWarningDialog> {
int _countdown = 12;
bool show_warning = false;
late Timer _timer;
late ServerModel _serverModel;
@override
void initState() {
super.initState();
_serverModel = widget.serverModel;
startCountdown();
}
void startCountdown() {
const oneSecond = Duration(seconds: 1);
_timer = Timer.periodic(oneSecond, (timer) {
setState(() {
_countdown--;
if (_countdown <= 0) {
timer.cancel();
}
});
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isButtonLocked = _countdown > 0;
return AlertDialog(
content: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Color(0xffe242bc),
Color(0xfff4727c),
],
),
borderRadius: BorderRadius.circular(20.0),
),
padding: EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_amber_sharp,
color: Colors.white,
),
SizedBox(width: 10),
Text(
translate("Warning"),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20.0,
),
),
],
),
SizedBox(height: 20),
Center(
child: Image.asset('assets/scam.png',
width: 180,
),
),
SizedBox(height: 18),
Text(
translate("scam_title"),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 22.0,
),
),
SizedBox(height: 18),
SizedBox(
height: 220,
child: Scrollbar(
child: SingleChildScrollView(
child: Text(
translate("scam_text1")+"\n\n"
+translate("scam_text2")+"\n",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
),
),
),
Row(
children: <Widget>[
Checkbox(
value: show_warning,
onChanged: (value) {
setState((){
show_warning = value!;
});
},
),
Text(
translate("Don't show again"),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15.0,
),
),
],
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
constraints: BoxConstraints(maxWidth: 150),
child: ElevatedButton(
onPressed: isButtonLocked
? null
: () {
Navigator.of(context).pop();
_serverModel.toggleService();
if (show_warning) {
bind.mainSetLocalOption(key: "show-scam-warning", value: "N");
}
},
style: ElevatedButton.styleFrom(
primary: Colors.blueAccent,
),
child: Text(
isButtonLocked ? translate("I Agree")+" (${_countdown}s)" : translate("I Agree"),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
SizedBox(width: 15),
Container(
constraints: BoxConstraints(maxWidth: 150),
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
primary: Colors.blueAccent,
),
child: Text(
translate("Decline"),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
)])),
contentPadding: EdgeInsets.all(0.0),
);
}
} }
class ServerInfo extends StatelessWidget { class ServerInfo extends StatelessWidget {

View File

@@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:settings_ui/settings_ui.dart'; import 'package:settings_ui/settings_ui.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
@@ -44,10 +45,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _enableDirectIPAccess = false; var _enableDirectIPAccess = false;
var _enableRecordSession = false; var _enableRecordSession = false;
var _autoRecordIncomingSession = false; var _autoRecordIncomingSession = false;
var _allowAutoDisconnect = false;
var _localIP = ""; var _localIP = "";
var _directAccessPort = ""; var _directAccessPort = "";
var _fingerprint = ""; var _fingerprint = "";
var _buildDate = ""; var _buildDate = "";
var _autoDisconnectTimeout = "";
@override @override
void initState() { void initState() {
@@ -150,6 +153,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_buildDate = buildDate; _buildDate = buildDate;
} }
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
await bind.mainGetOption(key: 'allow-auto-disconnect'));
if (allowAutoDisconnect != _allowAutoDisconnect) {
update = true;
_allowAutoDisconnect = allowAutoDisconnect;
}
final autoDisconnectTimeout =
await bind.mainGetOption(key: 'auto-disconnect-timeout');
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
update = true;
_autoDisconnectTimeout = autoDisconnectTimeout;
}
if (update) { if (update) {
setState(() {}); setState(() {});
} }
@@ -305,6 +322,48 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
await bind.mainSetOption(key: 'direct-server', value: value); await bind.mainSetOption(key: 'direct-server', value: value);
setState(() {}); setState(() {});
}, },
),
SettingsTile.switchTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("auto_disconnect_option_tip")),
Offstage(
offstage: !_allowAutoDisconnect,
child: Text(
'${_autoDisconnectTimeout.isEmpty ? '10' : _autoDisconnectTimeout} min',
style: Theme.of(context).textTheme.bodySmall,
)),
])),
Offstage(
offstage: !_allowAutoDisconnect,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.edit,
size: 20,
),
onPressed: () async {
final timeout = await changeAutoDisconnectTimeout(
_autoDisconnectTimeout);
setState(() {
_autoDisconnectTimeout = timeout;
});
}))
]),
initialValue: _allowAutoDisconnect,
onToggle: (_) async {
_allowAutoDisconnect = !_allowAutoDisconnect;
String value =
bool2option('allow-auto-disconnect', _allowAutoDisconnect);
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value);
setState(() {});
},
) )
]; ];
if (_hasIgnoreBattery) { if (_hasIgnoreBattery) {
@@ -383,7 +442,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsSection( SettingsSection(
title: Text(translate('Account')), title: Text(translate('Account')),
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile(
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login') ? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.userName.value})')), : '${translate('Logout')} (${gFFI.userModel.userName.value})')),
@@ -399,19 +458,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
], ],
), ),
SettingsSection(title: Text(translate("Settings")), tiles: [ SettingsSection(title: Text(translate("Settings")), tiles: [
SettingsTile.navigation( SettingsTile(
title: Text(translate('ID/Relay Server')), title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud), leading: Icon(Icons.cloud),
onPressed: (context) { onPressed: (context) {
showServerSettings(gFFI.dialogManager); showServerSettings(gFFI.dialogManager);
}), }),
SettingsTile.navigation( SettingsTile(
title: Text(translate('Language')), title: Text(translate('Language')),
leading: Icon(Icons.translate), leading: Icon(Icons.translate),
onPressed: (context) { onPressed: (context) {
showLanguageSettings(gFFI.dialogManager); showLanguageSettings(gFFI.dialogManager);
}), }),
SettingsTile.navigation( SettingsTile(
title: Text(translate( title: Text(translate(
Theme.of(context).brightness == Brightness.light Theme.of(context).brightness == Brightness.light
? 'Dark Theme' ? 'Dark Theme'
@@ -424,45 +483,50 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
}, },
) )
]), ]),
SettingsSection( if (isAndroid)
title: Text(translate("Recording")), SettingsSection(
tiles: [ title: Text(translate("Recording")),
SettingsTile.switchTile( tiles: [
title: Text(translate('Automatically record incoming sessions')), SettingsTile.switchTile(
leading: Icon(Icons.videocam), title:
description: FutureBuilder( Text(translate('Automatically record incoming sessions')),
builder: (ctx, data) => Offstage( leading: Icon(Icons.videocam),
offstage: !data.hasData, description: FutureBuilder(
child: Text("${translate("Directory")}: ${data.data}")), builder: (ctx, data) => Offstage(
future: bind.mainDefaultVideoSaveDirectory()), offstage: !data.hasData,
initialValue: _autoRecordIncomingSession, child: Text("${translate("Directory")}: ${data.data}")),
onToggle: (v) async { future: bind.mainDefaultVideoSaveDirectory()),
await bind.mainSetOption( initialValue: _autoRecordIncomingSession,
key: "allow-auto-record-incoming", onToggle: (v) async {
value: bool2option("allow-auto-record-incoming", v)); await bind.mainSetOption(
final newValue = option2bool( key: "allow-auto-record-incoming",
'allow-auto-record-incoming', value: bool2option("allow-auto-record-incoming", v));
await bind.mainGetOption( final newValue = option2bool(
key: 'allow-auto-record-incoming')); 'allow-auto-record-incoming',
setState(() { await bind.mainGetOption(
_autoRecordIncomingSession = newValue; key: 'allow-auto-record-incoming'));
}); setState(() {
}, _autoRecordIncomingSession = newValue;
), });
], },
), ),
SettingsSection( ],
title: Text(translate("Share Screen")), ),
tiles: shareScreenTiles, if (isAndroid)
), SettingsSection(
SettingsSection( title: Text(translate("Share Screen")),
title: Text(translate("Enhancements")), tiles: shareScreenTiles,
tiles: enhancementsTiles, ),
), defaultDisplaySection(),
if (isAndroid)
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
),
SettingsSection( SettingsSection(
title: Text(translate("About")), title: Text(translate("About")),
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile(
onPressed: (context) async { onPressed: (context) async {
if (await canLaunchUrl(Uri.parse(url))) { if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url)); await launchUrl(Uri.parse(url));
@@ -477,21 +541,28 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
)), )),
), ),
leading: Icon(Icons.info)), leading: Icon(Icons.info)),
SettingsTile.navigation( SettingsTile(
title: Text(translate("Build Date")), title: Text(translate("Build Date")),
value: Padding( value: Padding(
padding: EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: 8),
child: Text(_buildDate), child: Text(_buildDate),
), ),
leading: Icon(Icons.query_builder)), leading: Icon(Icons.query_builder)),
SettingsTile.navigation( if (isAndroid)
onPressed: (context) => onCopyFingerprint(_fingerprint), SettingsTile(
title: Text(translate("Fingerprint")), onPressed: (context) => onCopyFingerprint(_fingerprint),
value: Padding( title: Text(translate("Fingerprint")),
padding: EdgeInsets.symmetric(vertical: 8), value: Padding(
child: Text(_fingerprint), padding: EdgeInsets.symmetric(vertical: 8),
), child: Text(_fingerprint),
leading: Icon(Icons.fingerprint)), ),
leading: Icon(Icons.fingerprint)),
SettingsTile(
title: Text(translate("Privacy Statement")),
onPressed: (context) =>
launchUrlString('https://rustdesk.com/privacy.html'),
leading: Icon(Icons.privacy_tip),
)
], ],
), ),
], ],
@@ -508,6 +579,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
} }
return true; return true;
} }
defaultDisplaySection() {
return SettingsSection(
title: Text(translate("Display Settings")),
tiles: [
SettingsTile(
title: Text(translate('Display Settings')),
leading: Icon(Icons.desktop_windows_outlined),
trailing: Icon(Icons.arrow_forward_ios),
onPressed: (context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return _DisplayPage();
}));
})
],
);
}
} }
void showServerSettings(OverlayDialogManager dialogManager) async { void showServerSettings(OverlayDialogManager dialogManager) async {
@@ -618,3 +706,181 @@ class ScanButton extends StatelessWidget {
); );
} }
} }
class _DisplayPage extends StatefulWidget {
const _DisplayPage({super.key});
@override
State<_DisplayPage> createState() => __DisplayPageState();
}
class __DisplayPageState extends State<_DisplayPage> {
@override
Widget build(BuildContext context) {
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
final h264 = codecsJson['h264'] ?? false;
final h265 = codecsJson['h265'] ?? false;
var codecList = [
_RadioEntry('Auto', 'auto'),
_RadioEntry('VP8', 'vp8'),
_RadioEntry('VP9', 'vp9'),
_RadioEntry('AV1', 'av1'),
if (h264) _RadioEntry('H264', 'h264'),
if (h265) _RadioEntry('H265', 'h265')
];
RxBool showCustomImageQuality = false.obs;
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.arrow_back_ios)),
title: Text(translate('Display Settings')),
centerTitle: true,
),
body: SettingsList(sections: [
SettingsSection(
tiles: [
_getPopupDialogRadioEntry(
title: 'Default View Style',
list: [
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
],
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
asyncSetter: (value) async {
await bind.mainSetUserDefaultOption(
key: 'view_style', value: value);
},
),
_getPopupDialogRadioEntry(
title: 'Default Image Quality',
list: [
_RadioEntry('Good image quality', kRemoteImageQualityBest),
_RadioEntry('Balanced', kRemoteImageQualityBalanced),
_RadioEntry('Optimize reaction time', kRemoteImageQualityLow),
_RadioEntry('Custom', kRemoteImageQualityCustom),
],
getter: () {
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
return v;
},
asyncSetter: (value) async {
await bind.mainSetUserDefaultOption(
key: 'image_quality', value: value);
showCustomImageQuality.value =
value == kRemoteImageQualityCustom;
},
tail: customImageQualitySetting(),
showTail: showCustomImageQuality,
notCloseValue: kRemoteImageQualityCustom,
),
_getPopupDialogRadioEntry(
title: 'Default Codec',
list: codecList,
getter: () =>
bind.mainGetUserDefaultOption(key: 'codec-preference'),
asyncSetter: (value) async {
await bind.mainSetUserDefaultOption(
key: 'codec-preference', value: value);
},
),
],
),
SettingsSection(
title: Text(translate('Other Default Options')),
tiles: [
otherRow('Show remote cursor', 'show_remote_cursor'),
otherRow('Show quality monitor', 'show_quality_monitor'),
otherRow('Mute', 'disable_audio'),
otherRow('Disable clipboard', 'disable_clipboard'),
otherRow('Lock after session end', 'lock_after_session_end'),
otherRow('Privacy mode', 'privacy_mode'),
otherRow('Touch mode', 'touch-mode'),
],
),
]),
);
}
otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
return SettingsTile.switchTile(
initialValue: value,
title: Text(translate(label)),
onToggle: (b) async {
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
setState(() {});
},
);
}
}
class _RadioEntry {
final String label;
final String value;
_RadioEntry(this.label, this.value);
}
typedef _RadioEntryGetter = String Function();
typedef _RadioEntrySetter = Future<void> Function(String);
_getPopupDialogRadioEntry({
required String title,
required List<_RadioEntry> list,
required _RadioEntryGetter getter,
required _RadioEntrySetter asyncSetter,
Widget? tail,
RxBool? showTail,
String? notCloseValue,
}) {
RxString groupValue = ''.obs;
RxString valueText = ''.obs;
init() {
groupValue.value = getter();
final e = list.firstWhereOrNull((e) => e.value == groupValue.value);
if (e != null) {
valueText.value = e.label;
}
}
init();
void showDialog() async {
gFFI.dialogManager.show((setState, close, context) {
onChanged(String? value) async {
if (value == null) return;
await asyncSetter(value);
init();
if (value != notCloseValue) {
close();
}
}
return CustomAlertDialog(
content: Obx(
() => Column(children: [
...list
.map((e) => getRadio(Text(translate(e.label)), e.value,
groupValue.value, (String? value) => onChanged(value)))
.toList(),
Offstage(
offstage:
!(tail != null && showTail != null && showTail.value == true),
child: tail,
),
]),
));
}, backDismiss: true, clickMaskDismiss: true);
}
return SettingsTile(
title: Text(translate(title)),
onPressed: (context) => showDialog(),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Obx(() => Text(translate(valueText.value))),
),
);
}

View File

@@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@@ -147,59 +147,72 @@ void setTemporaryPasswordLengthDialog(
void showServerSettingsWithValue( void showServerSettingsWithValue(
ServerConfig serverConfig, OverlayDialogManager dialogManager) async { ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
final oldCfg = ServerConfig.fromOptions(oldOptions);
var isInProgress = false; var isInProgress = false;
final idCtrl = TextEditingController(text: serverConfig.idServer); final idCtrl = TextEditingController(text: serverConfig.idServer);
final relayCtrl = TextEditingController(text: serverConfig.relayServer); final relayCtrl = TextEditingController(text: serverConfig.relayServer);
final apiCtrl = TextEditingController(text: serverConfig.apiServer); final apiCtrl = TextEditingController(text: serverConfig.apiServer);
final keyCtrl = TextEditingController(text: serverConfig.key); final keyCtrl = TextEditingController(text: serverConfig.key);
String? idServerMsg; RxString idServerMsg = ''.obs;
String? relayServerMsg; RxString relayServerMsg = ''.obs;
String? apiServerMsg; RxString apiServerMsg = ''.obs;
final controllers = [idCtrl, relayCtrl, apiCtrl, keyCtrl];
final errMsgs = [
idServerMsg,
relayServerMsg,
apiServerMsg,
];
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
Future<bool> validate() async { Future<bool> submit() async {
if (idCtrl.text != oldCfg.idServer) { setState(() {
final res = await validateAsync(idCtrl.text); isInProgress = true;
setState(() => idServerMsg = res); });
if (idServerMsg != null) return false; bool ret = await setServerConfig(
} controllers,
if (relayCtrl.text != oldCfg.relayServer) { errMsgs,
relayServerMsg = await validateAsync(relayCtrl.text); ServerConfig(
if (relayServerMsg != null) return false; idServer: idCtrl.text.trim(),
} relayServer: relayCtrl.text.trim(),
if (apiCtrl.text != oldCfg.apiServer) { apiServer: apiCtrl.text.trim(),
if (apiServerMsg != null) return false; key: keyCtrl.text.trim()));
} setState(() {
return true; isInProgress = false;
});
return ret;
} }
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('ID/Relay Server')), title: Row(
children: [
Expanded(child: Text(translate('ID/Relay Server'))),
...ServerConfigImportExportWidgets(controllers, errMsgs),
],
),
content: Form( content: Form(
child: Column( child: Obx(() => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
TextFormField( TextFormField(
controller: idCtrl, controller: idCtrl,
decoration: InputDecoration( decoration: InputDecoration(
labelText: translate('ID Server'), labelText: translate('ID Server'),
errorText: idServerMsg), errorText: idServerMsg.value.isEmpty
? null
: idServerMsg.value),
)
] +
[
TextFormField(
controller: relayCtrl,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg.value.isEmpty
? null
: relayServerMsg.value),
) )
] + ] +
(isAndroid
? [
TextFormField(
controller: relayCtrl,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg),
)
]
: []) +
[ [
TextFormField( TextFormField(
controller: apiCtrl, controller: apiCtrl,
@@ -214,7 +227,7 @@ void showServerSettingsWithValue(
return translate("invalid_http"); return translate("invalid_http");
} }
} }
return apiServerMsg; return null;
}, },
), ),
TextFormField( TextFormField(
@@ -225,7 +238,7 @@ void showServerSettingsWithValue(
), ),
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(), if (isInProgress) const LinearProgressIndicator(),
])), ]))),
actions: [ actions: [
dialogButton('Cancel', onPressed: () { dialogButton('Cancel', onPressed: () {
close(); close();
@@ -233,35 +246,12 @@ void showServerSettingsWithValue(
dialogButton( dialogButton(
'OK', 'OK',
onPressed: () async { onPressed: () async {
setState(() { if (await submit()) {
idServerMsg = null;
relayServerMsg = null;
apiServerMsg = null;
isInProgress = true;
});
if (await validate()) {
if (idCtrl.text != oldCfg.idServer) {
if (oldCfg.idServer.isNotEmpty) {
await gFFI.userModel.logOut();
}
bind.mainSetOption(
key: "custom-rendezvous-server", value: idCtrl.text);
}
if (relayCtrl.text != oldCfg.relayServer) {
bind.mainSetOption(key: "relay-server", value: relayCtrl.text);
}
if (keyCtrl.text != oldCfg.key) {
bind.mainSetOption(key: "key", value: keyCtrl.text);
}
if (apiCtrl.text != oldCfg.apiServer) {
bind.mainSetOption(key: "api-server", value: apiCtrl.text);
}
close(); close();
showToast(translate('Successful')); showToast(translate('Successful'));
} else {
showToast(translate('Failed'));
} }
setState(() {
isInProgress = false;
});
}, },
), ),
], ],

View File

@@ -3,9 +3,9 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
@@ -23,13 +23,20 @@ bool shouldSortTags() {
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty; return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
} }
final filterAbTagOption = 'filter-ab-by-intersection';
bool filterAbTagByIntersection() {
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
}
class AbModel { class AbModel {
final abLoading = false.obs; final abLoading = false.obs;
final pullError = "".obs; final pullError = "".obs;
final pushError = "".obs; final pushError = "".obs;
final tags = [].obs; final tags = [].obs;
final RxMap<String, int> tagColors = Map<String, int>.fromEntries([]).obs;
final peers = List<Peer>.empty(growable: true).obs; final peers = List<Peer>.empty(growable: true).obs;
final sortTags = shouldSortTags().obs; final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
final retrying = false.obs; final retrying = false.obs;
bool get emtpy => peers.isEmpty && tags.isEmpty; bool get emtpy => peers.isEmpty && tags.isEmpty;
@@ -80,10 +87,11 @@ class AbModel {
if (resp.body.toLowerCase() == "null") { if (resp.body.toLowerCase() == "null") {
// normal reply, emtpy ab return null // normal reply, emtpy ab return null
tags.clear(); tags.clear();
tagColors.clear();
peers.clear(); peers.clear();
} else if (resp.body.isNotEmpty) { } else if (resp.body.isNotEmpty) {
Map<String, dynamic> json = Map<String, dynamic> json =
_jsonDecode(utf8.decode(resp.bodyBytes), resp.statusCode); _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) { if (json.containsKey('error')) {
throw json['error']; throw json['error'];
} else if (json.containsKey('data')) { } else if (json.containsKey('data')) {
@@ -93,26 +101,7 @@ class AbModel {
} catch (e) {} } catch (e) {}
final data = jsonDecode(json['data']); final data = jsonDecode(json['data']);
if (data != null) { if (data != null) {
final oldOnlineIDs = _deserialize(data);
peers.where((e) => e.online).map((e) => e.id).toList();
tags.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
if (isFull(false)) {
peers.removeRange(licensedDevices, peers.length);
}
// restore online
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
_saveCache(); // save on success _saveCache(); // save on success
} }
} }
@@ -121,9 +110,6 @@ class AbModel {
if (!quiet) { if (!quiet) {
pullError.value = pullError.value =
'${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) {
BotToast.showText(contentColor: Colors.red, text: pullError.value);
}
} }
} finally { } finally {
abLoading.value = false; abLoading.value = false;
@@ -132,9 +118,10 @@ class AbModel {
_timerCounter = 0; _timerCounter = 0;
if (pullError.isNotEmpty) { if (pullError.isNotEmpty) {
if (statusCode == 401) { if (statusCode == 401) {
gFFI.userModel.reset(clearAbCache: true); gFFI.userModel.reset(resetOther: true);
} }
} }
platformFFI.tryHandle({'name': LoadEvent.addressBook});
} }
} }
@@ -147,6 +134,7 @@ class AbModel {
'alias': alias, 'alias': alias,
'tags': tags, 'tags': tags,
}); });
_mergePeerFromGroup(peer);
peers.add(peer); peers.add(peer);
} }
@@ -242,10 +230,7 @@ class AbModel {
final api = "${await bind.mainGetApiServer()}/api/ab"; final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = getHttpHeaders(); var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json"; authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); final body = jsonEncode({"data": jsonEncode(_serialize())});
final body = jsonEncode({
"data": jsonEncode({"tags": tags, "peers": peersJsonData})
});
http.Response resp; http.Response resp;
// support compression // support compression
if (licensedDevices > 0 && body.length > 1024) { if (licensedDevices > 0 && body.length > 1024) {
@@ -261,7 +246,8 @@ class AbModel {
ret = true; ret = true;
_saveCache(); _saveCache();
} else { } else {
Map<String, dynamic> json = _jsonDecode(resp.body, resp.statusCode); Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) { if (json.containsKey('error')) {
throw json['error']; throw json['error'];
} else if (resp.statusCode == 200) { } else if (resp.statusCode == 200) {
@@ -318,6 +304,7 @@ class AbModel {
void deleteTag(String tag) { void deleteTag(String tag) {
gFFI.abModel.selectedTags.remove(tag); gFFI.abModel.selectedTags.remove(tag);
tags.removeWhere((element) => element == tag); tags.removeWhere((element) => element == tag);
tagColors.remove(tag);
for (var peer in peers) { for (var peer in peers) {
if (peer.tags.isEmpty) { if (peer.tags.isEmpty) {
continue; continue;
@@ -353,6 +340,11 @@ class AbModel {
} }
}).toList(); }).toList();
} }
int? oldColor = tagColors[oldTag];
if (oldColor != null) {
tagColors.remove(oldTag);
tagColors.addAll({newTag: oldColor});
}
} }
void unsetSelectedTags() { void unsetSelectedTags() {
@@ -368,6 +360,20 @@ class AbModel {
} }
} }
Color getTagColor(String tag) {
int? colorValue = tagColors[tag];
if (colorValue != null) {
return Color(colorValue);
}
return str2color2(tag, existing: tagColors.values.toList());
}
setTagColor(String tag, Color color) {
if (tags.contains(tag)) {
tagColors[tag] = color.value;
}
}
void merge(Peer r, Peer p) { void merge(Peer r, Peer p) {
p.hash = r.hash.isEmpty ? p.hash : r.hash; p.hash = r.hash.isEmpty ? p.hash : r.hash;
p.username = r.username.isEmpty ? p.username : r.username; p.username = r.username.isEmpty ? p.username : r.username;
@@ -467,43 +473,33 @@ class AbModel {
_saveCache() { _saveCache() {
try { try {
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); var m = _serialize();
final m = <String, dynamic>{ m.addAll(<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'), "access_token": bind.mainGetLocalOption(key: 'access_token'),
"peers": peersJsonData, });
"tags": tags.map((e) => e.toString()).toList(),
};
bind.mainSaveAb(json: jsonEncode(m)); bind.mainSaveAb(json: jsonEncode(m));
} catch (e) { } catch (e) {
debugPrint('ab save:$e'); debugPrint('ab save:$e');
} }
} }
loadCache() async { Future<void> loadCache() async {
try { try {
if (_cacheLoadOnceFlag || abLoading.value) return; if (_cacheLoadOnceFlag || abLoading.value || initialized) return;
_cacheLoadOnceFlag = true; _cacheLoadOnceFlag = true;
final access_token = bind.mainGetLocalOption(key: 'access_token'); final access_token = bind.mainGetLocalOption(key: 'access_token');
if (access_token.isEmpty) return; if (access_token.isEmpty) return;
final cache = await bind.mainLoadAb(); final cache = await bind.mainLoadAb();
if (abLoading.value) return;
final data = jsonDecode(cache); final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return; if (data == null || data['access_token'] != access_token) return;
tags.clear(); _deserialize(data);
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
} catch (e) { } catch (e) {
debugPrint("load ab cache: $e"); debugPrint("load ab cache: $e");
} }
} }
Map<String, dynamic> _jsonDecode(String body, int statusCode) { Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
try { try {
Map<String, dynamic> json = jsonDecode(body); Map<String, dynamic> json = jsonDecode(body);
return json; return json;
@@ -516,6 +512,50 @@ class AbModel {
} }
} }
Map<String, dynamic> _serialize() {
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList();
final tagColorJsonData = jsonEncode(tagColors);
return {
"tags": tags,
"peers": peersJsonData,
"tag_colors": tagColorJsonData
};
}
_deserialize(dynamic data) {
if (data == null) return;
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
tags.clear();
tagColors.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
if (isFull(false)) {
peers.removeRange(licensedDevices, peers.length);
}
// restore online
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
if (data['tag_colors'] is String) {
Map<String, dynamic> map = jsonDecode(data['tag_colors']);
tagColors.value = Map<String, int>.from(map);
}
// add color to tag
final tagsWithoutColor =
tags.toList().where((e) => !tagColors.containsKey(e)).toList();
for (var t in tagsWithoutColor) {
tagColors[t] = str2color2(t, existing: tagColors.values.toList()).value;
}
}
reSyncToast(Future<bool> future) { reSyncToast(Future<bool> future) {
if (!shouldSyncAb()) return; if (!shouldSyncAb()) return;
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
@@ -528,4 +568,26 @@ class AbModel {
} }
}); });
} }
reset() async {
pullError.value = '';
pushError.value = '';
tags.clear();
peers.clear();
await bind.mainClearAb();
}
_mergePeerFromGroup(Peer p) {
final g = gFFI.groupModel.peers.firstWhereOrNull((e) => p.id == e.id);
if (g == null) return;
if (p.username.isEmpty) {
p.username = g.username;
}
if (p.hostname.isEmpty) {
p.hostname = g.hostname;
}
if (p.platform.isEmpty) {
p.platform = g.platform;
}
}
} }

View File

@@ -10,7 +10,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/mobile/pages/home_page.dart'; import 'package:flutter_hbb/mobile/pages/home_page.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@@ -63,7 +62,7 @@ class ChatModel with ChangeNotifier {
bool isConnManager = false; bool isConnManager = false;
RxBool isWindowFocus = true.obs; RxBool isWindowFocus = true.obs;
BlockableOverlayState? _blockableOverlayState; BlockableOverlayState _blockableOverlayState = BlockableOverlayState();
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted); final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus; Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
@@ -72,6 +71,13 @@ class ChatModel with ChangeNotifier {
RxInt mobileUnreadSum = 0.obs; RxInt mobileUnreadSum = 0.obs;
MessageKey? latestReceivedKey; MessageKey? latestReceivedKey;
Offset chatWindowPosition = Offset(20, 80);
void setChatWindowPosition(Offset position) {
chatWindowPosition = position;
notifyListeners();
}
@override @override
void dispose() { void dispose() {
textController.dispose(); textController.dispose();
@@ -86,13 +92,13 @@ class ChatModel with ChangeNotifier {
late final Map<MessageKey, MessageBody> _messages = {}; late final Map<MessageKey, MessageBody> _messages = {};
MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value
late bool _isShowCMChatPage = false; late bool _isShowCMSidePage = false;
Map<MessageKey, MessageBody> get messages => _messages; Map<MessageKey, MessageBody> get messages => _messages;
MessageKey get currentKey => _currentKey; MessageKey get currentKey => _currentKey;
bool get isShowCMChatPage => _isShowCMChatPage; bool get isShowCMSidePage => _isShowCMSidePage;
void setOverlayState(BlockableOverlayState blockableOverlayState) { void setOverlayState(BlockableOverlayState blockableOverlayState) {
_blockableOverlayState = blockableOverlayState; _blockableOverlayState = blockableOverlayState;
@@ -154,7 +160,7 @@ class ChatModel with ChangeNotifier {
} }
} }
final overlayState = _blockableOverlayState?.state; final overlayState = _blockableOverlayState.state;
if (overlayState == null) return; if (overlayState == null) return;
final overlay = OverlayEntry(builder: (context) { final overlay = OverlayEntry(builder: (context) {
@@ -210,7 +216,7 @@ class ChatModel with ChangeNotifier {
} }
}, },
child: DraggableChatWindow( child: DraggableChatWindow(
position: chatInitPos ?? Offset(20, 80), position: chatInitPos ?? chatWindowPosition,
width: 250, width: 250,
height: 350, height: 350,
chatModel: this)); chatModel: this));
@@ -255,7 +261,7 @@ class ChatModel with ChangeNotifier {
showChatPage(MessageKey key) async { showChatPage(MessageKey key) async {
if (isDesktop) { if (isDesktop) {
if (isConnManager) { if (isConnManager) {
if (!_isShowCMChatPage) { if (!_isShowCMSidePage) {
await toggleCMChatPage(key); await toggleCMChatPage(key);
} }
} else { } else {
@@ -276,8 +282,15 @@ class ChatModel with ChangeNotifier {
if (gFFI.chatModel.currentKey != key) { if (gFFI.chatModel.currentKey != key) {
gFFI.chatModel.changeCurrentKey(key); gFFI.chatModel.changeCurrentKey(key);
} }
if (_isShowCMChatPage) { await toggleCMSidePage();
_isShowCMChatPage = !_isShowCMChatPage; }
var _togglingCMSidePage = false; // protect order for await
toggleCMSidePage() async {
if (_togglingCMSidePage) return false;
_togglingCMSidePage = true;
if (_isShowCMSidePage) {
_isShowCMSidePage = !_isShowCMSidePage;
notifyListeners(); notifyListeners();
await windowManager.show(); await windowManager.show();
await windowManager.setSizeAlignment( await windowManager.setSizeAlignment(
@@ -287,9 +300,10 @@ class ChatModel with ChangeNotifier {
await windowManager.show(); await windowManager.show();
await windowManager.setSizeAlignment( await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeOpenChat, Alignment.topRight); kConnectionManagerWindowSizeOpenChat, Alignment.topRight);
_isShowCMChatPage = !_isShowCMChatPage; _isShowCMSidePage = !_isShowCMSidePage;
notifyListeners(); notifyListeners();
} }
_togglingCMSidePage = false;
} }
changeCurrentKey(MessageKey key) { changeCurrentKey(MessageKey key) {
@@ -396,7 +410,7 @@ class ChatModel with ChangeNotifier {
parent.target?.serverModel.jumpTo(id); parent.target?.serverModel.jumpTo(id);
} }
} else { } else {
if (HomePage.homeKey.currentState?.selectedIndex != 1 || if (HomePage.homeKey.currentState?.isChatPageCurrentTab != true ||
_currentKey != messagekey) { _currentKey != messagekey) {
client.unreadChatMessageCount.value += 1; client.unreadChatMessageCount.value += 1;
mobileUpdateUnreadSum(); mobileUpdateUnreadSum();

View File

@@ -0,0 +1,142 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:get/get.dart';
import 'file_model.dart';
class CmFileModel {
final WeakReference<FFI> parent;
final currentJobTable = RxList<JobProgress>();
final _jobTables = HashMap<int, RxList<JobProgress>>.fromEntries([]);
Stopwatch stopwatch = Stopwatch();
int _lastElapsed = 0;
CmFileModel(this.parent);
void updateCurrentClientId(int id) {
if (_jobTables[id] == null) {
_jobTables[id] = RxList<JobProgress>();
}
Future.delayed(Duration.zero, () {
currentJobTable.value = _jobTables[id]!;
});
}
onFileTransferLog(dynamic log) {
try {
dynamic d = jsonDecode(log);
if (!stopwatch.isRunning) stopwatch.start();
bool calcSpeed = stopwatch.elapsedMilliseconds - _lastElapsed >= 1000;
if (calcSpeed) {
_lastElapsed = stopwatch.elapsedMilliseconds;
}
if (d is List<dynamic>) {
for (var l in d) {
_dealOneJob(l, calcSpeed);
}
} else {
_dealOneJob(d, calcSpeed);
}
currentJobTable.refresh();
} catch (e) {
debugPrint("onFileTransferLog:$e");
}
}
_dealOneJob(dynamic l, bool calcSpeed) {
final data = TransferJobSerdeData.fromJson(l);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
return;
}
JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
if (job == null) {
job = JobProgress();
jobTable.add(job);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
}
job.id = data.id;
job.isRemoteToLocal = data.isRemote;
job.fileName = data.path;
job.totalSize = data.totalSize;
job.finishedSize = data.finishedSize;
if (job.finishedSize > data.totalSize) {
job.finishedSize = data.totalSize;
}
job.isRemoteToLocal = data.isRemote;
if (job.finishedSize > 0) {
if (job.finishedSize < job.totalSize) {
job.state = JobState.inProgress;
} else {
job.state = JobState.done;
}
}
if (data.done) {
job.state = JobState.done;
} else if (data.cancel || data.error == 'skipped') {
job.state = JobState.done;
job.err = 'skipped';
} else if (data.error.isNotEmpty) {
job.state = JobState.error;
job.err = data.error;
}
if (calcSpeed) {
job.speed = (data.transferred - job.lastTransferredSize) * 1.0;
job.lastTransferredSize = data.transferred;
}
jobTable.refresh();
}
}
class TransferJobSerdeData {
int connId;
int id;
String path;
bool isRemote;
int totalSize;
int finishedSize;
int transferred;
bool done;
bool cancel;
String error;
TransferJobSerdeData({
required this.connId,
required this.id,
required this.path,
required this.isRemote,
required this.totalSize,
required this.finishedSize,
required this.transferred,
required this.done,
required this.cancel,
required this.error,
});
TransferJobSerdeData.fromJson(dynamic d)
: this(
connId: d['connId'] ?? 0,
id: int.tryParse(d['id'].toString()) ?? 0,
path: d['path'] ?? '',
isRemote: d['isRemote'] ?? false,
totalSize: d['totalSize'] ?? 0,
finishedSize: d['finishedSize'] ?? 0,
transferred: d['transferred'] ?? 0,
done: d['done'] ?? false,
cancel: d['cancel'] ?? false,
error: d['error'] ?? '',
);
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'; import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
@@ -9,7 +8,7 @@ class RenderTexture {
final RxInt textureId = RxInt(-1); final RxInt textureId = RxInt(-1);
int _textureKey = -1; int _textureKey = -1;
SessionID? _sessionId; SessionID? _sessionId;
final useTextureRender = bind.mainUseTextureRender(); static final useTextureRender = bind.mainUseTextureRender();
final textureRenderer = TextureRgbaRenderer(); final textureRenderer = TextureRgbaRenderer();
@@ -21,7 +20,6 @@ class RenderTexture {
_sessionId = sessionId; _sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async { textureRenderer.createTexture(_textureKey).then((id) async {
debugPrint("id: $id, texture_key: $_textureKey");
if (id != -1) { if (id != -1) {
final ptr = await textureRenderer.getTexturePtr(_textureKey); final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerTexture(sessionId, ptr); platformFFI.registerTexture(sessionId, ptr);

View File

@@ -1029,6 +1029,7 @@ class JobProgress {
var to = ""; var to = "";
var showHidden = false; var showHidden = false;
var err = ""; var err = "";
int lastTransferredSize = 0;
clear() { clear() {
state = JobState.none; state = JobState.none;

View File

@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
@@ -11,57 +12,74 @@ import 'package:http/http.dart' as http;
class GroupModel { class GroupModel {
final RxBool groupLoading = false.obs; final RxBool groupLoading = false.obs;
final RxString groupLoadError = "".obs; final RxString groupLoadError = "".obs;
final RxString groupId = ''.obs;
RxString groupName = ''.obs;
final RxList<UserPayload> users = RxList.empty(growable: true); final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<Peer> peersShow = RxList.empty(growable: true); final RxList<Peer> peers = RxList.empty(growable: true);
final RxString selectedUser = ''.obs; final RxString selectedUser = ''.obs;
final RxString searchUserText = ''.obs; final RxString searchUserText = ''.obs;
WeakReference<FFI> parent; WeakReference<FFI> parent;
var initialized = false; var initialized = false;
var _cacheLoadOnceFlag = false;
var _statusCode = 200;
bool get emtpy => users.isEmpty && peers.isEmpty;
GroupModel(this.parent); GroupModel(this.parent);
reset() {
groupName.value = '';
groupId.value = '';
users.clear();
peersShow.clear();
initialized = false;
}
Future<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
/* if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (!force && initialized) return; if (!force && initialized) return;
if (!quiet) { if (!quiet) {
groupLoading.value = true; groupLoading.value = true;
groupLoadError.value = ""; groupLoadError.value = "";
} }
await _pull(); try {
await _pull();
} catch (_) {}
groupLoading.value = false; groupLoading.value = false;
initialized = true; initialized = true;
*/ platformFFI.tryHandle({'name': LoadEvent.group});
if (_statusCode == 401) {
gFFI.userModel.reset(resetOther: true);
} else {
_saveCache();
}
} }
Future<void> _pull() async { Future<void> _pull() async {
reset(); List<UserPayload> tmpUsers = List.empty(growable: true);
if (bind.mainGetLocalOption(key: 'access_token') == '') { if (!await _getUsers(tmpUsers)) {
return; return;
} }
try { List<Peer> tmpPeers = List.empty(growable: true);
if (!await _getGroup()) { if (!await _getPeers(tmpPeers)) {
reset();
return;
}
} catch (e) {
debugPrint('$e');
reset();
return; return;
} }
// me first
var index = tmpUsers
.indexWhere((user) => user.name == gFFI.userModel.userName.value);
if (index != -1) {
var user = tmpUsers.removeAt(index);
tmpUsers.insert(0, user);
}
users.value = tmpUsers;
if (!users.any((u) => u.name == selectedUser.value)) {
selectedUser.value = '';
}
// recover online
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
peers.value = tmpPeers;
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
groupLoadError.value = '';
}
Future<bool> _getUsers(List<UserPayload> tmpUsers) async {
final api = "${await bind.mainGetApiServer()}/api/users"; final api = "${await bind.mainGetApiServer()}/api/users";
try { try {
var uri0 = Uri.parse(api); var uri0 = Uri.parse(api);
final pageSize = 20; final pageSize = 100;
var total = 0; var total = 0;
int current = 0; int current = 0;
do { do {
@@ -74,86 +92,68 @@ class GroupModel {
queryParameters: { queryParameters: {
'current': current.toString(), 'current': current.toString(),
'pageSize': pageSize.toString(), 'pageSize': pageSize.toString(),
if (gFFI.userModel.isAdmin.isFalse) 'grp': groupId.value, 'accessible': '',
'status': '1',
}); });
final resp = await http.get(uri, headers: getHttpHeaders()); final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { _statusCode = resp.statusCode;
Map<String, dynamic> json = jsonDecode(utf8.decode(resp.bodyBytes)); Map<String, dynamic> json =
if (json.containsKey('error')) { _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
throw json['error']; if (json.containsKey('error')) {
if (json['error'] == 'Admin required!' ||
json['error']
.toString()
.contains('ambiguous column name: status')) {
throw translate('upgrade_rustdesk_server_pro_to_{1.1.10}_tip');
} else { } else {
if (json.containsKey('total')) { throw json['error'];
if (total == 0) total = json['total']; }
if (json.containsKey('data')) { }
final data = json['data']; if (resp.statusCode != 200) {
if (data is List) { throw 'HTTP ${resp.statusCode}';
for (final user in data) { }
final u = UserPayload.fromJson(user); if (json.containsKey('total')) {
if (!users.any((e) => e.name == u.name)) { if (total == 0) total = json['total'];
users.add(u); if (json.containsKey('data')) {
} final data = json['data'];
} if (data is List) {
for (final user in data) {
final u = UserPayload.fromJson(user);
int index = tmpUsers.indexWhere((e) => e.name == u.name);
if (index < 0) {
tmpUsers.add(u);
} else {
tmpUsers[index] = u;
} }
} }
} }
} }
} }
} while (current * pageSize < total); } while (current * pageSize < total);
return true;
} catch (err) { } catch (err) {
debugPrint('$err'); debugPrint('get accessible users: $err');
groupLoadError.value = err.toString(); groupLoadError.value =
} finally { '${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
_pullUserPeers();
} }
}
Future<bool> _getGroup() async {
final url = await bind.mainGetApiServer();
final body = {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid()
};
try {
final response = await http.post(Uri.parse('$url/api/currentGroup'),
headers: getHttpHeaders(), body: json.encode(body));
final status = response.statusCode;
if (status == 401 || status == 400) {
return false;
}
final data = json.decode(utf8.decode(response.bodyBytes));
final error = data['error'];
if (error != null) {
throw error;
}
groupName.value = data['name'] ?? '';
groupId.value = data['guid'] ?? '';
return groupId.value.isNotEmpty && groupName.isNotEmpty;
} catch (e) {
debugPrint('$e');
groupLoadError.value = e.toString();
} finally {}
return false; return false;
} }
Future<void> _pullUserPeers() async { Future<bool> _getPeers(List<Peer> tmpPeers) async {
peersShow.clear();
final api = "${await bind.mainGetApiServer()}/api/peers";
try { try {
final api = "${await bind.mainGetApiServer()}/api/peers";
var uri0 = Uri.parse(api); var uri0 = Uri.parse(api);
final pageSize = final pageSize = 100;
20; // ????????????????????????????????????????????????????? stupid stupis, how about >20 peers
var total = 0; var total = 0;
int current = 0; int current = 0;
var queryParameters = {
'current': current.toString(),
'pageSize': pageSize.toString(),
};
if (!gFFI.userModel.isAdmin.value) {
queryParameters.addAll({'grp': groupId.value});
}
do { do {
current += 1; current += 1;
var queryParameters = {
'current': current.toString(),
'pageSize': pageSize.toString(),
'accessible': '',
'status': '1',
};
var uri = Uri( var uri = Uri(
scheme: uri0.scheme, scheme: uri0.scheme,
host: uri0.host, host: uri0.host,
@@ -161,32 +161,102 @@ class GroupModel {
port: uri0.port, port: uri0.port,
queryParameters: queryParameters); queryParameters: queryParameters);
final resp = await http.get(uri, headers: getHttpHeaders()); final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { _statusCode = resp.statusCode;
Map<String, dynamic> json = jsonDecode(utf8.decode(resp.bodyBytes));
if (json.containsKey('error')) { Map<String, dynamic> json =
throw json['error']; _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
} else { if (json.containsKey('error')) {
if (json.containsKey('total')) { throw json['error'];
if (total == 0) total = json['total']; }
if (json.containsKey('data')) { if (resp.statusCode != 200) {
final data = json['data']; throw 'HTTP ${resp.statusCode}';
if (data is List) { }
for (final p in data) { if (json.containsKey('total')) {
final peerPayload = PeerPayload.fromJson(p); if (total == 0) total = json['total'];
final peer = PeerPayload.toPeer(peerPayload); if (json.containsKey('data')) {
if (!peersShow.any((e) => e.id == peer.id)) { final data = json['data'];
peersShow.add(peer); if (data is List) {
} for (final p in data) {
} final peerPayload = PeerPayload.fromJson(p);
final peer = PeerPayload.toPeer(peerPayload);
int index = tmpPeers.indexWhere((e) => e.id == peer.id);
if (index < 0) {
tmpPeers.add(peer);
} else {
tmpPeers[index] = peer;
} }
} }
} }
} }
} }
} while (current * pageSize < total); } while (current * pageSize < total);
return true;
} catch (err) { } catch (err) {
debugPrint('$err'); debugPrint('get accessible peers: $err');
groupLoadError.value = err.toString(); groupLoadError.value =
} finally {} '${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
return false;
}
Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
try {
Map<String, dynamic> json = jsonDecode(body);
return json;
} catch (e) {
final err = body.isNotEmpty && body.length < 128 ? body : e.toString();
if (statusCode != 200) {
throw 'HTTP $statusCode, $err';
}
throw err;
}
}
void _saveCache() {
try {
final map = (<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'),
"users": users.map((e) => e.toGroupCacheJson()).toList(),
'peers': peers.map((e) => e.toGroupCacheJson()).toList()
});
bind.mainSaveGroup(json: jsonEncode(map));
} catch (e) {
debugPrint('group save:$e');
}
}
Future<void> loadCache() async {
try {
if (_cacheLoadOnceFlag || groupLoading.value || initialized) return;
_cacheLoadOnceFlag = true;
final access_token = bind.mainGetLocalOption(key: 'access_token');
if (access_token.isEmpty) return;
final cache = await bind.mainLoadGroup();
if (groupLoading.value) return;
final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return;
users.clear();
peers.clear();
if (data['users'] is List) {
for (var u in data['users']) {
users.add(UserPayload.fromJson(u));
}
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
} catch (e) {
debugPrint("load group cache: $e");
}
}
reset() async {
groupLoadError.value = '';
users.clear();
peers.clear();
selectedUser.value = '';
await bind.mainClearGroup();
} }
} }

View File

@@ -35,6 +35,24 @@ extension ToString on MouseButtons {
} }
} }
class PointerEventToRust {
final String kind;
final String type;
final dynamic value;
PointerEventToRust(this.kind, this.type, this.value);
Map<String, dynamic> toJson() {
return {
'k': kind,
'v': {
't': type,
'v': value,
}
};
}
}
class InputModel { class InputModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
String keyboardMode = "legacy"; String keyboardMode = "legacy";
@@ -62,11 +80,11 @@ class InputModel {
int _lastButtons = 0; int _lastButtons = 0;
Offset lastMousePos = Offset.zero; Offset lastMousePos = Offset.zero;
get id => parent.target?.id ?? "";
late final SessionID sessionId; late final SessionID sessionId;
bool get keyboardPerm => parent.target!.ffiModel.keyboard; bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
InputModel(this.parent) { InputModel(this.parent) {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
@@ -223,14 +241,8 @@ class InputModel {
command: command); command: command);
} }
Map<String, dynamic> getEvent(PointerEvent evt, String type) { Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {}; final Map<String, dynamic> out = {};
out['x'] = evt.position.dx;
out['y'] = evt.position.dy;
if (alt) out['alt'] = 'true';
if (shift) out['shift'] = 'true';
if (ctrl) out['ctrl'] = 'true';
if (command) out['command'] = 'true';
// Check update event type and set buttons to be sent. // Check update event type and set buttons to be sent.
int buttons = _lastButtons; int buttons = _lastButtons;
@@ -260,7 +272,6 @@ class InputModel {
out['buttons'] = buttons; out['buttons'] = buttons;
out['type'] = type; out['type'] = type;
return out; return out;
} }
@@ -292,7 +303,7 @@ class InputModel {
} }
/// Modify the given modifier map [evt] based on current modifier key status. /// Modify the given modifier map [evt] based on current modifier key status.
Map<String, String> modify(Map<String, String> evt) { Map<String, dynamic> modify(Map<String, dynamic> evt) {
if (ctrl) evt['ctrl'] = 'true'; if (ctrl) evt['ctrl'] = 'true';
if (shift) evt['shift'] = 'true'; if (shift) evt['shift'] = 'true';
if (alt) evt['alt'] = 'true'; if (alt) evt['alt'] = 'true';
@@ -334,27 +345,33 @@ class InputModel {
isPhysicalMouse.value = true; isPhysicalMouse.value = true;
} }
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventMove)); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
} }
} }
void onPointerPanZoomStart(PointerPanZoomStartEvent e) { void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
_lastScale = 1.0; _lastScale = 1.0;
_stopFling = true; _stopFling = true;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position);
}
} }
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
final scale = ((e.scale - _lastScale) * 1000).toInt(); if (peerPlatform != kPeerPlatformAndroid) {
_lastScale = e.scale; final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale;
if (scale != 0) { if (scale != 0) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': scale} PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
})); .toJson()));
return; return;
}
} }
final delta = e.panDelta; final delta = e.panDelta;
@@ -362,7 +379,7 @@ class InputModel {
var x = delta.dx.toInt(); var x = delta.dx.toInt();
var y = delta.dy.toInt(); var y = delta.dy.toInt();
if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) { if (peerPlatform == kPeerPlatformLinux) {
_trackpadScrollUnsent += (delta * _trackpadSpeed); _trackpadScrollUnsent += (delta * _trackpadSpeed);
x = _trackpadScrollUnsent.dx.truncate(); x = _trackpadScrollUnsent.dx.truncate();
y = _trackpadScrollUnsent.dy.truncate(); y = _trackpadScrollUnsent.dy.truncate();
@@ -378,9 +395,13 @@ class InputModel {
} }
} }
if (x != 0 || y != 0) { if (x != 0 || y != 0) {
bind.sessionSendMouse( if (peerPlatform == kPeerPlatformAndroid) {
sessionId: sessionId, handlePointerEvent('touch', 'pan_update', Offset(x.toDouble(), y.toDouble()));
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); } else {
bind.sessionSendMouse(
sessionId: sessionId,
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
}
} }
} }
@@ -436,11 +457,15 @@ class InputModel {
} }
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_end', e.position);
return;
}
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': 0} PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
}));
waitLastFlingDone(); waitLastFlingDone();
_stopFling = false; _stopFling = false;
@@ -465,21 +490,21 @@ class InputModel {
} }
} }
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventDown)); handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
} }
} }
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventUp)); handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
} }
} }
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventMove)); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
} }
} }
@@ -504,19 +529,16 @@ class InputModel {
} }
void refreshMousePos() => handleMouse({ void refreshMousePos() => handleMouse({
'x': lastMousePos.dx,
'y': lastMousePos.dy,
'buttons': 0, 'buttons': 0,
'type': _kMouseEventMove, 'type': _kMouseEventMove,
}); }, lastMousePos);
void tryMoveEdgeOnExit(Offset pos) => handleMouse( void tryMoveEdgeOnExit(Offset pos) => handleMouse(
{ {
'x': pos.dx,
'y': pos.dy,
'buttons': 0, 'buttons': 0,
'type': _kMouseEventMove, 'type': _kMouseEventMove,
}, },
pos,
onExit: true, onExit: true,
); );
@@ -550,17 +572,49 @@ class InputModel {
return Offset(x, y); return Offset(x, y);
} }
void handleMouse( void handlePointerEvent(String kind, String type, Offset offset) {
Map<String, dynamic> evt, { double x = offset.dx;
bool onExit = false, double y = offset.dy;
}) { if (_checkPeerControlProtected(x, y)) {
double x = evt['x']; return;
double y = max(0.0, evt['y']); }
final cursorModel = parent.target!.cursorModel; // Only touch events are handled for now. So we can just ignore buttons.
// to-do: handle mouse events
late final dynamic evtValue;
if (type == 'pan_update') {
evtValue = {
'x': x.toInt(),
'y': y.toInt(),
};
} else {
final isMoveTypes = ['pan_start', 'pan_end'];
final pos = handlePointerDevicePos(
kPointerEventKindTouch,
x,
y,
isMoveTypes.contains(type),
type,
);
if (pos == null) {
return;
}
evtValue = {
'x': pos.x,
'y': pos.y,
};
}
final evt = PointerEventToRust(kind, type, evtValue).toJson();
bind.sessionSendPointer(
sessionId: sessionId, msg: json.encode(modify(evt)));
}
bool _checkPeerControlProtected(double x, double y) {
final cursorModel = parent.target!.cursorModel;
if (cursorModel.isPeerControlProtected) { if (cursorModel.isPeerControlProtected) {
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return; return true;
} }
if (!cursorModel.gotMouseControl) { if (!cursorModel.gotMouseControl) {
@@ -571,10 +625,23 @@ class InputModel {
cursorModel.gotMouseControl = true; cursorModel.gotMouseControl = true;
} else { } else {
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return; return true;
} }
} }
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return false;
}
void handleMouse(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
}) {
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
return;
}
var type = ''; var type = '';
var isMove = false; var isMove = false;
@@ -592,17 +659,58 @@ class InputModel {
return; return;
} }
evt['type'] = type; evt['type'] = type;
final pos = handlePointerDevicePos(
kPointerEventKindMouse,
x,
y,
isMove,
type,
onExit: onExit,
buttons: evt['buttons'],
);
if (pos == null) {
return;
}
if (type != '') {
evt['x'] = '0';
evt['y'] = '0';
} else {
evt['x'] = '${pos.x}';
evt['y'] = '${pos.y}';
}
Map<int, String> mapButtons = {
kPrimaryMouseButton: 'left',
kSecondaryMouseButton: 'right',
kMiddleMouseButton: 'wheel',
kBackMouseButton: 'back',
kForwardMouseButton: 'forward'
};
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
}
Point? handlePointerDevicePos(
String kind,
double x,
double y,
bool isMove,
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
y -= CanvasModel.topToEdge; y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge; x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel; final canvasModel = parent.target!.canvasModel;
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final ffiModel = parent.target!.ffiModel; final ffiModel = parent.target!.ffiModel;
if (isMove) { if (isMove) {
canvasModel.moveDesktopMouse(x, y); canvasModel.moveDesktopMouse(x, y);
} }
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final d = ffiModel.display; final d = ffiModel.display;
final imageWidth = d.width * canvasModel.scale; final imageWidth = d.width * canvasModel.scale;
final imageHeight = d.height * canvasModel.scale; final imageHeight = d.height * canvasModel.scale;
@@ -650,7 +758,7 @@ class InputModel {
} catch (e) { } catch (e) {
debugPrintStack( debugPrintStack(
label: 'canvasModel.scale value ${canvasModel.scale}, $e'); label: 'canvasModel.scale value ${canvasModel.scale}, $e');
return; return null;
} }
int minX = d.x.toInt(); int minX = d.x.toInt();
@@ -659,40 +767,16 @@ class InputModel {
int maxY = (d.y + d.height).toInt() - 1; int maxY = (d.y + d.height).toInt() - 1;
evtX = trySetNearestRange(evtX, minX, maxX, 5); evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5); evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { if (kind == kPointerEventKindMouse) {
// If left mouse up, no early return. if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { // If left mouse up, no early return.
return; if (!(buttons == kPrimaryMouseButton && evtType == 'up')) {
return null;
}
} }
} }
if (type != '') { return Point(evtX, evtY);
evtX = 0;
evtY = 0;
}
evt['x'] = '$evtX';
evt['y'] = '$evtY';
var buttons = '';
switch (evt['buttons']) {
case kPrimaryMouseButton:
buttons = 'left';
break;
case kSecondaryMouseButton:
buttons = 'right';
break;
case kMiddleMouseButton:
buttons = 'wheel';
break;
case kBackMouseButton:
buttons = 'back';
break;
case kForwardMouseButton:
buttons = 'forward';
break;
}
evt['buttons'] = buttons;
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(evt));
} }
/// Web only /// Web only

View File

@@ -4,12 +4,14 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart'; import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
@@ -37,11 +39,52 @@ import 'platform_model.dart';
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id); typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool); typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
final _waitForImageDialogShow = <UuidValue, bool>{};
final _waitForFirstImage = <UuidValue, bool>{};
final _constSessionId = Uuid().v4obj(); final _constSessionId = Uuid().v4obj();
class CachedPeerData {
Map<String, dynamic> updatePrivacyMode = {};
Map<String, dynamic> peerInfo = {};
List<Map<String, dynamic>> cursorDataList = [];
Map<String, dynamic> lastCursorId = {};
bool secure = false;
bool direct = false;
CachedPeerData();
@override
String toString() {
return jsonEncode({
'updatePrivacyMode': updatePrivacyMode,
'peerInfo': peerInfo,
'cursorDataList': cursorDataList,
'lastCursorId': lastCursorId,
'secure': secure,
'direct': direct,
});
}
static CachedPeerData? fromString(String s) {
try {
final map = jsonDecode(s);
final data = CachedPeerData();
data.updatePrivacyMode = map['updatePrivacyMode'];
data.peerInfo = map['peerInfo'];
for (final cursorData in map['cursorDataList']) {
data.cursorDataList.add(cursorData);
}
data.lastCursorId = map['lastCursorId'];
data.secure = map['secure'];
data.direct = map['direct'];
return data;
} catch (e) {
debugPrint('Failed to parse CachedPeerData: $e');
return null;
}
}
}
class FfiModel with ChangeNotifier { class FfiModel with ChangeNotifier {
CachedPeerData cachedPeerData = CachedPeerData();
PeerInfo _pi = PeerInfo(); PeerInfo _pi = PeerInfo();
Display _display = Display(); Display _display = Display();
@@ -56,6 +99,10 @@ class FfiModel with ChangeNotifier {
WeakReference<FFI> parent; WeakReference<FFI> parent;
late final SessionID sessionId; late final SessionID sessionId;
RxBool waitForImageDialogShow = true.obs;
Timer? waitForImageTimer;
RxBool waitForFirstImage = true.obs;
Map<String, bool> get permissions => _permissions; Map<String, bool> get permissions => _permissions;
Display get display => _display; Display get display => _display;
@@ -114,9 +161,12 @@ class FfiModel with ChangeNotifier {
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
clearPermissions(); clearPermissions();
waitForImageTimer?.cancel();
} }
setConnectionType(String peerId, bool secure, bool direct) { setConnectionType(String peerId, bool secure, bool direct) {
cachedPeerData.secure = secure;
cachedPeerData.direct = direct;
_secure = secure; _secure = secure;
_direct = direct; _direct = direct;
try { try {
@@ -143,6 +193,24 @@ class FfiModel with ChangeNotifier {
_permissions.clear(); _permissions.clear();
} }
handleCachedPeerData(CachedPeerData data, String peerId) async {
handleMsgBox({
'type': 'success',
'title': 'Successful',
'text': 'Connected, waiting for image...',
'link': '',
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
await handlePeerInfo(data.peerInfo, peerId);
for (final element in data.cursorDataList) {
updateLastCursorId(element);
await handleCursorData(element);
}
updateLastCursorId(data.lastCursorId);
handleCursorId(data.lastCursorId);
}
// todo: why called by two position // todo: why called by two position
StreamEventHandler startEventListener(SessionID sessionId, String peerId) { StreamEventHandler startEventListener(SessionID sessionId, String peerId) {
return (evt) async { return (evt) async {
@@ -159,9 +227,11 @@ class FfiModel with ChangeNotifier {
} else if (name == 'switch_display') { } else if (name == 'switch_display') {
handleSwitchDisplay(evt, sessionId, peerId); handleSwitchDisplay(evt, sessionId, peerId);
} else if (name == 'cursor_data') { } else if (name == 'cursor_data') {
await parent.target?.cursorModel.updateCursorData(evt); updateLastCursorId(evt);
await handleCursorData(evt);
} else if (name == 'cursor_id') { } else if (name == 'cursor_id') {
await parent.target?.cursorModel.updateCursorId(evt); updateLastCursorId(evt);
handleCursorId(evt);
} else if (name == 'cursor_position') { } else if (name == 'cursor_position') {
await parent.target?.cursorModel.updateCursorPosition(evt, peerId); await parent.target?.cursorModel.updateCursorPosition(evt, peerId);
} else if (name == 'clipboard') { } else if (name == 'clipboard') {
@@ -199,8 +269,6 @@ class FfiModel with ChangeNotifier {
updateBlockInputState(evt, peerId); updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') { } else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt, sessionId, peerId); updatePrivacyMode(evt, sessionId, peerId);
} else if (name == 'alias') {
handleAliasChanged(evt);
} else if (name == 'show_elevation') { } else if (name == 'show_elevation') {
final show = evt['show'].toString() == 'true'; final show = evt['show'].toString() == 'true';
parent.target?.serverModel.setShowElevation(show); parent.target?.serverModel.setShowElevation(show);
@@ -252,6 +320,10 @@ class FfiModel with ChangeNotifier {
} }
} }
} }
} else if (name == "cm_file_transfer_log") {
if (isDesktop) {
gFFI.cmFileModel.onFileTransferLog(evt['log']);
}
} else { } else {
debugPrint('Unknown event name: $name'); debugPrint('Unknown event name: $name');
} }
@@ -282,13 +354,6 @@ class FfiModel with ChangeNotifier {
platformFFI.setEventCallback(startEventListener(sessionId, peerId)); platformFFI.setEventCallback(startEventListener(sessionId, peerId));
} }
handleAliasChanged(Map<String, dynamic> evt) {
final rxAlias = PeerStringOption.find(evt['id'], 'alias');
if (rxAlias.value != evt['alias']) {
rxAlias.value = evt['alias'];
}
}
_updateCurDisplay(SessionID sessionId, Display newDisplay) { _updateCurDisplay(SessionID sessionId, Display newDisplay) {
if (newDisplay != _display) { if (newDisplay != _display) {
if (newDisplay.x != _display.x || newDisplay.y != _display.y) { if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
@@ -436,7 +501,7 @@ class FfiModel with ChangeNotifier {
closeConnection(); closeConnection();
} }
if (_waitForFirstImage[sessionId] == false) return; if (waitForFirstImage.isFalse) return;
dialogManager.show( dialogManager.show(
(setState, close, context) => CustomAlertDialog( (setState, close, context) => CustomAlertDialog(
title: null, title: null,
@@ -447,7 +512,12 @@ class FfiModel with ChangeNotifier {
onCancel: onClose), onCancel: onClose),
tag: '$sessionId-waiting-for-image', tag: '$sessionId-waiting-for-image',
); );
_waitForImageDialogShow[sessionId] = true; waitForImageDialogShow.value = true;
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
if (waitForFirstImage.isTrue) {
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
}
});
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId); bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
} }
@@ -464,6 +534,8 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt]. /// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId) async { handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
cachedPeerData.peerInfo = evt;
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers(); bind.mainLoadRecentPeers();
@@ -482,23 +554,13 @@ class FfiModel with ChangeNotifier {
} }
final connType = parent.target?.connType; final connType = parent.target?.connType;
if (isPeerAndroid) { if (isPeerAndroid) {
_touchMode = true; _touchMode = true;
if (connType == ConnType.defaultConn &&
parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer(
const Duration(milliseconds: 100),
() => parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!));
}
} else { } else {
_touchMode = await bind.sessionGetOption( _touchMode = await bind.sessionGetOption(
sessionId: sessionId, arg: 'touch-mode') != sessionId: sessionId, arg: 'touch-mode') !=
''; '';
} }
if (connType == ConnType.fileTransfer) { if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady(); parent.target?.fileModel.onReady();
} else if (connType == ConnType.defaultConn) { } else if (connType == ConnType.defaultConn) {
@@ -514,7 +576,7 @@ class FfiModel with ChangeNotifier {
} }
if (displays.isNotEmpty) { if (displays.isNotEmpty) {
_reconnects = 1; _reconnects = 1;
_waitForFirstImage[sessionId] = true; waitForFirstImage.value = true;
} }
Map<String, dynamic> features = json.decode(evt['features']); Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1; _pi.features.privacyMode = features['privacy_mode'] == 1;
@@ -538,11 +600,25 @@ class FfiModel with ChangeNotifier {
} }
} }
_pi.isSet.value = true;
stateGlobal.resetLastResolutionGroupValues(peerId); stateGlobal.resetLastResolutionGroupValues(peerId);
notifyListeners(); notifyListeners();
} }
tryShowAndroidActionsOverlay({int delayMSecs = 10}) {
if (isPeerAndroid) {
if (parent.target?.connType == ConnType.defaultConn &&
parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer(
Duration(milliseconds: delayMSecs),
() => parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!));
}
}
}
handleResolutions(String id, dynamic resolutions) { handleResolutions(String id, dynamic resolutions) {
try { try {
final List<dynamic> dynamicArray = jsonDecode(resolutions as String); final List<dynamic> dynamicArray = jsonDecode(resolutions as String);
@@ -579,9 +655,24 @@ class FfiModel with ChangeNotifier {
return d; return d;
} }
updateLastCursorId(Map<String, dynamic> evt) {
parent.target?.cursorModel.id = int.parse(evt['id']);
}
handleCursorId(Map<String, dynamic> evt) {
cachedPeerData.lastCursorId = evt;
parent.target?.cursorModel.updateCursorId(evt);
}
handleCursorData(Map<String, dynamic> evt) async {
cachedPeerData.cursorDataList.add(evt);
await parent.target?.cursorModel.updateCursorData(evt);
}
/// Handle the peer info synchronization event based on [evt]. /// Handle the peer info synchronization event based on [evt].
handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async { handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async {
if (evt['displays'] != null) { if (evt['displays'] != null) {
cachedPeerData.peerInfo['displays'] = evt['displays'];
List<dynamic> displays = json.decode(evt['displays']); List<dynamic> displays = json.decode(evt['displays']);
List<Display> newDisplays = []; List<Display> newDisplays = [];
for (int i = 0; i < displays.length; ++i) { for (int i = 0; i < displays.length; ++i) {
@@ -1196,6 +1287,7 @@ class CursorModel with ChangeNotifier {
final _cacheKeys = <String>{}; final _cacheKeys = <String>{};
double _x = -10000; double _x = -10000;
double _y = -10000; double _y = -10000;
int _id = -1;
double _hotx = 0; double _hotx = 0;
double _hoty = 0; double _hoty = 0;
double _displayOriginX = 0; double _displayOriginX = 0;
@@ -1204,7 +1296,7 @@ class CursorModel with ChangeNotifier {
bool gotMouseControl = true; bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now() DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String id = ''; String peerId = '';
WeakReference<FFI> parent; WeakReference<FFI> parent;
ui.Image? get image => _image; ui.Image? get image => _image;
@@ -1218,6 +1310,8 @@ class CursorModel with ChangeNotifier {
double get hotx => _hotx; double get hotx => _hotx;
double get hoty => _hoty; double get hoty => _hoty;
set id(int id) => _id = id;
bool get isPeerControlProtected => bool get isPeerControlProtected =>
DateTime.now().difference(_lastPeerMouse).inMilliseconds < DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec; kMouseControlTimeoutMSec;
@@ -1356,32 +1450,33 @@ class CursorModel with ChangeNotifier {
} }
updateCursorData(Map<String, dynamic> evt) async { updateCursorData(Map<String, dynamic> evt) async {
var id = int.parse(evt['id']); final id = int.parse(evt['id']);
_hotx = double.parse(evt['hotx']); final hotx = double.parse(evt['hotx']);
_hoty = double.parse(evt['hoty']); final hoty = double.parse(evt['hoty']);
var width = int.parse(evt['width']); final width = int.parse(evt['width']);
var height = int.parse(evt['height']); final height = int.parse(evt['height']);
List<dynamic> colors = json.decode(evt['colors']); List<dynamic> colors = json.decode(evt['colors']);
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
final image = await img.decodeImageFromPixels( final image = await img.decodeImageFromPixels(
rgba, width, height, ui.PixelFormat.rgba8888); rgba, width, height, ui.PixelFormat.rgba8888);
_image = image; if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
if (await _updateCache(rgba, image, id, width, height)) { _images[id] = Tuple3(image, hotx, hoty);
_images[id] = Tuple3(image, _hotx, _hoty);
} else {
_hotx = 0;
_hoty = 0;
}
try {
// my throw exception, because the listener maybe already dispose
notifyListeners();
} catch (e) {
debugPrint('WARNING: updateCursorId $id, without notifyListeners(). $e');
} }
// Update last cursor data.
// Do not use the previous `image` and `id`, because `_id` may be changed.
_updateCurData();
} }
Future<bool> _updateCache( Future<bool> _updateCache(
Uint8List rgba, ui.Image image, int id, int w, int h) async { Uint8List rgba,
ui.Image image,
int id,
double hotx,
double hoty,
int w,
int h,
) async {
Uint8List? data; Uint8List? data;
img2.Image imgOrigin = img2.Image.fromBytes( img2.Image imgOrigin = img2.Image.fromBytes(
width: w, height: h, bytes: rgba.buffer, order: img2.ChannelOrder.rgba); width: w, height: h, bytes: rgba.buffer, order: img2.ChannelOrder.rgba);
@@ -1395,33 +1490,45 @@ class CursorModel with ChangeNotifier {
} }
data = imgBytes.buffer.asUint8List(); data = imgBytes.buffer.asUint8List();
} }
_cache = CursorData( final cache = CursorData(
peerId: this.id, peerId: peerId,
id: id, id: id,
image: imgOrigin, image: imgOrigin,
scale: 1.0, scale: 1.0,
data: data, data: data,
hotxOrigin: _hotx, hotxOrigin: hotx,
hotyOrigin: _hoty, hotyOrigin: hoty,
width: w, width: w,
height: h, height: h,
); );
_cacheMap[id] = _cache!; _cacheMap[id] = cache;
return true; return true;
} }
updateCursorId(Map<String, dynamic> evt) async { bool _updateCurData() {
final id = int.parse(evt['id']); _cache = _cacheMap[_id];
_cache = _cacheMap[id]; final tmp = _images[_id];
final tmp = _images[id];
if (tmp != null) { if (tmp != null) {
_image = tmp.item1; _image = tmp.item1;
_hotx = tmp.item2; _hotx = tmp.item2;
_hoty = tmp.item3; _hoty = tmp.item3;
notifyListeners(); try {
// may throw exception, because the listener maybe already dispose
notifyListeners();
} catch (e) {
debugPrint(
'WARNING: updateCursorId $_id, without notifyListeners(). $e');
}
return true;
} else { } else {
return false;
}
}
updateCursorId(Map<String, dynamic> evt) {
if (!_updateCurData()) {
debugPrint( debugPrint(
'WARNING: updateCursorId $id, cache is ${_cache == null ? "null" : "not null"}. without notifyListeners()'); 'WARNING: updateCursorId $_id, cache is ${_cache == null ? "null" : "not null"}. without notifyListeners()');
} }
} }
@@ -1576,6 +1683,7 @@ class ElevationModel with ChangeNotifier {
bool get showRequestMenu => _canElevate && !_running; bool get showRequestMenu => _canElevate && !_running;
onPeerInfo(PeerInfo pi) { onPeerInfo(PeerInfo pi) {
_canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false; _canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false;
_running = false;
} }
onPortableServiceRunning(Map<String, dynamic> evt) { onPortableServiceRunning(Map<String, dynamic> evt) {
@@ -1596,7 +1704,6 @@ class FFI {
/// dialogManager use late to ensure init after main page binding [globalKey] /// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager(); late final dialogManager = OverlayDialogManager();
late final bool isSessionAdded;
late final SessionID sessionId; late final SessionID sessionId;
late final ImageModel imageModel; // session late final ImageModel imageModel; // session
late final FfiModel ffiModel; // session late final FfiModel ffiModel; // session
@@ -1613,9 +1720,9 @@ class FFI {
late final RecordingModel recordingModel; // session late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session late final InputModel inputModel; // session
late final ElevationModel elevationModel; // session late final ElevationModel elevationModel; // session
late final CmFileModel cmFileModel; // cm
FFI(SessionID? sId) { FFI(SessionID? sId) {
isSessionAdded = sId != null;
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
imageModel = ImageModel(WeakReference(this)); imageModel = ImageModel(WeakReference(this));
ffiModel = FfiModel(WeakReference(this)); ffiModel = FfiModel(WeakReference(this));
@@ -1632,6 +1739,15 @@ class FFI {
recordingModel = RecordingModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this)); inputModel = InputModel(WeakReference(this));
elevationModel = ElevationModel(WeakReference(this)); elevationModel = ElevationModel(WeakReference(this));
cmFileModel = CmFileModel(WeakReference(this));
}
/// Mobile reuse FFI
void mobileReset() {
ffiModel.waitForFirstImage.value = true;
ffiModel.waitForImageDialogShow.value = true;
ffiModel.waitForImageTimer?.cancel();
ffiModel.waitForImageTimer = null;
} }
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
@@ -1641,9 +1757,11 @@ class FFI {
bool isRdp = false, bool isRdp = false,
String? switchUuid, String? switchUuid,
String? password, String? password,
bool? forceRelay}) { bool? forceRelay,
int? tabWindowId}) {
closed = false; closed = false;
auditNote = ''; auditNote = '';
if (isMobile) mobileReset();
assert(!(isFileTransfer && isPortForward), 'more than one connect type'); assert(!(isFileTransfer && isPortForward), 'more than one connect type');
if (isFileTransfer) { if (isFileTransfer) {
connType = ConnType.fileTransfer; connType = ConnType.fileTransfer;
@@ -1654,9 +1772,11 @@ class FFI {
connType = ConnType.defaultConn; connType = ConnType.defaultConn;
canvasModel.id = id; canvasModel.id = id;
imageModel.id = id; imageModel.id = id;
cursorModel.id = id; cursorModel.peerId = id;
} }
if (!isSessionAdded) { // If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one.
if (tabWindowId == null) {
// ignore: unused_local_variable // ignore: unused_local_variable
final addRes = bind.sessionAddSync( final addRes = bind.sessionAddSync(
sessionId: sessionId, sessionId: sessionId,
@@ -1677,8 +1797,25 @@ class FFI {
// Preserved for the rgba data. // Preserved for the rgba data.
stream.listen((message) { stream.listen((message) {
if (closed) return; if (closed) return;
if (isSessionAdded && !isToNewWindowNotified.value) { if (tabWindowId != null && !isToNewWindowNotified.value) {
bind.sessionReadyToNewWindow(sessionId: sessionId); // Session is read to be moved to a new window.
// Get the cached data and handle the cached data.
Future.delayed(Duration.zero, () async {
final cachedData = await DesktopMultiWindow.invokeMethod(
tabWindowId, kWindowEventGetCachedSessionData, id);
if (cachedData == null) {
// unreachable
debugPrint('Unreachable, the cached data is empty.');
return;
}
final data = CachedPeerData.fromString(cachedData);
if (data == null) {
debugPrint('Unreachable, the cached data cannot be decoded.');
return;
}
await ffiModel.handleCachedPeerData(data, id);
await bind.sessionRefresh(sessionId: sessionId);
});
isToNewWindowNotified.value = true; isToNewWindowNotified.value = true;
} }
() async { () async {
@@ -1704,7 +1841,7 @@ class FFI {
} else { } else {
// Fetch the image buffer from rust codes. // Fetch the image buffer from rust codes.
final sz = platformFFI.getRgbaSize(sessionId); final sz = platformFFI.getRgbaSize(sessionId);
if (sz == null || sz == 0) { if (sz == 0) {
return; return;
} }
final rgba = platformFFI.getRgba(sessionId, sz); final rgba = platformFFI.getRgba(sessionId, sz);
@@ -1721,12 +1858,13 @@ class FFI {
} }
void onEvent2UIRgba() async { void onEvent2UIRgba() async {
if (_waitForImageDialogShow[sessionId] == true) { if (ffiModel.waitForImageDialogShow.isTrue) {
_waitForImageDialogShow[sessionId] = false; ffiModel.waitForImageDialogShow.value = false;
ffiModel.waitForImageTimer?.cancel();
clearWaitingForImage(dialogManager, sessionId); clearWaitingForImage(dialogManager, sessionId);
} }
if (_waitForFirstImage[sessionId] == true) { if (ffiModel.waitForFirstImage.value == true) {
_waitForFirstImage[sessionId] = false; ffiModel.waitForFirstImage.value = false;
dialogManager.dismissAll(); dialogManager.dismissAll();
await canvasModel.updateViewStyle(); await canvasModel.updateViewStyle();
await canvasModel.updateScrollStyle(); await canvasModel.updateScrollStyle();
@@ -1841,7 +1979,7 @@ class Features {
bool privacyMode = false; bool privacyMode = false;
} }
class PeerInfo { class PeerInfo with ChangeNotifier {
String version = ''; String version = '';
String username = ''; String username = '';
String hostname = ''; String hostname = '';
@@ -1853,6 +1991,8 @@ class PeerInfo {
List<Resolution> resolutions = []; List<Resolution> resolutions = [];
Map<String, dynamic> platform_additions = {}; Map<String, dynamic> platform_additions = {};
RxBool isSet = false.obs;
bool get is_wayland => platform_additions['is_wayland'] == true; bool get is_wayland => platform_additions['is_wayland'] == true;
bool get is_headless => platform_additions['headless'] == true; bool get is_headless => platform_additions['headless'] == true;
} }

View File

@@ -21,16 +21,8 @@ class RgbaFrame extends Struct {
external Pointer<Uint8> data; external Pointer<Uint8> data;
} }
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>); typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>);
typedef F4 = Uint64 Function(Pointer<Utf8>);
typedef F4Dart = int Function(Pointer<Utf8>);
typedef F5 = Void Function(Pointer<Utf8>);
typedef F5Dart = void Function(Pointer<Utf8>);
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt); typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
// pub fn session_register_texture(id: *const char, ptr: usize)
typedef F6 = Void Function(Pointer<Utf8>, Uint64);
typedef F6Dart = void Function(Pointer<Utf8>, int);
/// FFI wrapper around the native Rust core. /// FFI wrapper around the native Rust core.
/// Hides the platform differences. /// Hides the platform differences.
@@ -38,7 +30,6 @@ class PlatformFFI {
String _dir = ''; String _dir = '';
// _homeDir is only needed for Android and IOS. // _homeDir is only needed for Android and IOS.
String _homeDir = ''; String _homeDir = '';
F2? _translate;
final _eventHandlers = <String, Map<String, HandleEvent>>{}; final _eventHandlers = <String, Map<String, HandleEvent>>{};
late RustdeskImpl _ffiBind; late RustdeskImpl _ffiBind;
late String _appType; late String _appType;
@@ -51,9 +42,6 @@ class PlatformFFI {
RustdeskImpl get ffiBind => _ffiBind; RustdeskImpl get ffiBind => _ffiBind;
F3? _session_get_rgba; F3? _session_get_rgba;
F4Dart? _session_get_rgba_size;
F5Dart? _session_next_rgba;
F6Dart? _session_register_texture;
static get localeName => Platform.localeName; static get localeName => Platform.localeName;
@@ -89,18 +77,8 @@ class PlatformFFI {
} }
} }
String translate(String name, String locale) { String translate(String name, String locale) =>
if (_translate == null) return name; _ffiBind.translate(name: name, locale: locale);
var a = name.toNativeUtf8();
var b = locale.toNativeUtf8();
var p = _translate!(a, b);
assert(p != nullptr);
final res = p.toDartString();
calloc.free(p);
calloc.free(a);
calloc.free(b);
return res;
}
Uint8List? getRgba(SessionID sessionId, int bufSize) { Uint8List? getRgba(SessionID sessionId, int bufSize) {
if (_session_get_rgba == null) return null; if (_session_get_rgba == null) return null;
@@ -118,30 +96,12 @@ class PlatformFFI {
} }
} }
int? getRgbaSize(SessionID sessionId) { int getRgbaSize(SessionID sessionId) =>
if (_session_get_rgba_size == null) return null; _ffiBind.sessionGetRgbaSize(sessionId: sessionId);
final sessionIdStr = sessionId.toString(); void nextRgba(SessionID sessionId) =>
var a = sessionIdStr.toNativeUtf8(); _ffiBind.sessionNextRgba(sessionId: sessionId);
final bufferSize = _session_get_rgba_size!(a); void registerTexture(SessionID sessionId, int ptr) =>
malloc.free(a); _ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
return bufferSize;
}
void nextRgba(SessionID sessionId) {
if (_session_next_rgba == null) return;
final sessionIdStr = sessionId.toString();
final a = sessionIdStr.toNativeUtf8();
_session_next_rgba!(a);
malloc.free(a);
}
void registerTexture(SessionID sessionId, int ptr) {
if (_session_register_texture == null) return;
final sessionIdStr = sessionId.toString();
final a = sessionIdStr.toNativeUtf8();
_session_register_texture!(a, ptr);
malloc.free(a);
}
/// Init the FFI class, loads the native Rust core library. /// Init the FFI class, loads the native Rust core library.
Future<void> init(String appType) async { Future<void> init(String appType) async {
@@ -157,14 +117,7 @@ class PlatformFFI {
: DynamicLibrary.process(); : DynamicLibrary.process();
debugPrint('initializing FFI $_appType'); debugPrint('initializing FFI $_appType');
try { try {
_translate = dylib.lookupFunction<F2, F2>('translate');
_session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba"); _session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba");
_session_get_rgba_size =
dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
_session_next_rgba =
dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
_session_register_texture =
dylib.lookupFunction<F6, F6Dart>("session_register_texture");
try { try {
// SYSTEM user failed // SYSTEM user failed
_dir = (await getApplicationDocumentsDirectory()).path; _dir = (await getApplicationDocumentsDirectory()).path;
@@ -246,7 +199,7 @@ class PlatformFFI {
version = await getVersion(); version = await getVersion();
} }
Future<bool> _tryHandle(Map<String, dynamic> evt) async { Future<bool> tryHandle(Map<String, dynamic> evt) async {
final name = evt['name']; final name = evt['name'];
if (name != null) { if (name != null) {
final handlers = _eventHandlers[name]; final handlers = _eventHandlers[name];
@@ -264,14 +217,15 @@ class PlatformFFI {
/// Start listening to the Rust core's events and frames. /// Start listening to the Rust core's events and frames.
void _startListenEvent(RustdeskImpl rustdeskImpl) { void _startListenEvent(RustdeskImpl rustdeskImpl) {
final appType = _appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType; final appType =
_appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType;
var sink = rustdeskImpl.startGlobalEventStream(appType: appType); var sink = rustdeskImpl.startGlobalEventStream(appType: appType);
sink.listen((message) { sink.listen((message) {
() async { () async {
try { try {
Map<String, dynamic> event = json.decode(message); Map<String, dynamic> event = json.decode(message);
// _tryHandle here may be more flexible than _eventCallback // _tryHandle here may be more flexible than _eventCallback
if (!await _tryHandle(event)) { if (!await tryHandle(event)) {
if (_eventCallback != null) { if (_eventCallback != null) {
await _eventCallback!(event); await _eventCallback!(event);
} }

View File

@@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'platform_model.dart'; import 'platform_model.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@@ -7,7 +8,7 @@ import 'package:collection/collection.dart';
class Peer { class Peer {
final String id; final String id;
String hash; String hash;
String username; String username; // pc username
String hostname; String hostname;
String platform; String platform;
String alias; String alias;
@@ -16,6 +17,7 @@ class Peer {
String rdpPort; String rdpPort;
String rdpUsername; String rdpUsername;
bool online = false; bool online = false;
String loginName; //login username
String getId() { String getId() {
if (alias != '') { if (alias != '') {
@@ -34,7 +36,8 @@ class Peer {
tags = json['tags'] ?? [], tags = json['tags'] ?? [],
forceAlwaysRelay = json['forceAlwaysRelay'] == 'true', forceAlwaysRelay = json['forceAlwaysRelay'] == 'true',
rdpPort = json['rdpPort'] ?? '', rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? ''; rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
@@ -48,6 +51,7 @@ class Peer {
"forceAlwaysRelay": forceAlwaysRelay.toString(), "forceAlwaysRelay": forceAlwaysRelay.toString(),
"rdpPort": rdpPort, "rdpPort": rdpPort,
"rdpUsername": rdpUsername, "rdpUsername": rdpUsername,
'loginName': loginName,
}; };
} }
@@ -63,6 +67,16 @@ class Peer {
}; };
} }
Map<String, dynamic> toGroupCacheJson() {
return <String, dynamic>{
"id": id,
"username": username,
"hostname": hostname,
"platform": platform,
"login_name": loginName,
};
}
Peer({ Peer({
required this.id, required this.id,
required this.hash, required this.hash,
@@ -74,6 +88,7 @@ class Peer {
required this.forceAlwaysRelay, required this.forceAlwaysRelay,
required this.rdpPort, required this.rdpPort,
required this.rdpUsername, required this.rdpUsername,
required this.loginName,
}); });
Peer.loading() Peer.loading()
@@ -88,6 +103,7 @@ class Peer {
forceAlwaysRelay: false, forceAlwaysRelay: false,
rdpPort: '', rdpPort: '',
rdpUsername: '', rdpUsername: '',
loginName: '',
); );
bool equal(Peer other) { bool equal(Peer other) {
return id == other.id && return id == other.id &&
@@ -99,21 +115,24 @@ class Peer {
tags.equals(other.tags) && tags.equals(other.tags) &&
forceAlwaysRelay == other.forceAlwaysRelay && forceAlwaysRelay == other.forceAlwaysRelay &&
rdpPort == other.rdpPort && rdpPort == other.rdpPort &&
rdpUsername == other.rdpUsername; rdpUsername == other.rdpUsername &&
loginName == other.loginName;
} }
Peer.copy(Peer other) Peer.copy(Peer other)
: this( : this(
id: other.id, id: other.id,
hash: other.hash, hash: other.hash,
username: other.username, username: other.username,
hostname: other.hostname, hostname: other.hostname,
platform: other.platform, platform: other.platform,
alias: other.alias, alias: other.alias,
tags: other.tags.toList(), tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay, forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort, rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername); rdpUsername: other.rdpUsername,
loginName: other.loginName,
);
} }
enum UpdateEvent { online, load } enum UpdateEvent { online, load }
@@ -121,11 +140,14 @@ enum UpdateEvent { online, load }
class Peers extends ChangeNotifier { class Peers extends ChangeNotifier {
final String name; final String name;
final String loadEvent; final String loadEvent;
List<Peer> peers; List<Peer> peers = List.empty(growable: true);
final RxList<Peer>? initPeers;
UpdateEvent event = UpdateEvent.load; UpdateEvent event = UpdateEvent.load;
static const _cbQueryOnlines = 'callback_query_onlines'; static const _cbQueryOnlines = 'callback_query_onlines';
Peers({required this.name, required this.peers, required this.loadEvent}) { Peers(
{required this.name, required this.initPeers, required this.loadEvent}) {
peers = initPeers ?? [];
platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async { platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async {
_updateOnlineState(evt); _updateOnlineState(evt);
}); });
@@ -176,7 +198,11 @@ class Peers extends ChangeNotifier {
void _updatePeers(Map<String, dynamic> evt) { void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates(); final onlineStates = _getOnlineStates();
peers = _decodePeers(evt['peers']); if (initPeers != null) {
peers = initPeers!;
} else {
peers = _decodePeers(evt['peers']);
}
for (var peer in peers) { for (var peer in peers) {
final state = onlineStates[peer.id]; final state = onlineStates[peer.id];
peer.online = state != null && state != false; peer.online = state != null && state != false;

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -16,8 +17,6 @@ enum PeerTabIndex {
group, group,
} }
const String defaultGroupTabname = 'Group';
class PeerTabModel with ChangeNotifier { class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent; WeakReference<FFI> parent;
int get currentTab => _currentTab; int get currentTab => _currentTab;
@@ -27,7 +26,7 @@ class PeerTabModel with ChangeNotifier {
'Favorites', 'Favorites',
'Discovered', 'Discovered',
'Address Book', 'Address Book',
//defaultGroupTabname, 'Group',
]; ];
final List<IconData> icons = [ final List<IconData> icons = [
Icons.access_time_filled, Icons.access_time_filled,
@@ -36,7 +35,10 @@ class PeerTabModel with ChangeNotifier {
IconFont.addressBook, IconFont.addressBook,
Icons.group, Icons.group,
]; ];
final List<bool> _isVisible = List.filled(5, true, growable: false);
List<bool> get isVisible => _isVisible;
List<int> get indexs => List.generate(tabNames.length, (index) => index); List<int> get indexs => List.generate(tabNames.length, (index) => index);
List<int> get visibleIndexs => indexs.where((e) => _isVisible[e]).toList();
List<Peer> _selectedPeers = List.empty(growable: true); List<Peer> _selectedPeers = List.empty(growable: true);
List<Peer> get selectedPeers => _selectedPeers; List<Peer> get selectedPeers => _selectedPeers;
bool _multiSelectionMode = false; bool _multiSelectionMode = false;
@@ -49,12 +51,29 @@ class PeerTabModel with ChangeNotifier {
String get lastId => _lastId; String get lastId => _lastId;
PeerTabModel(this.parent) { PeerTabModel(this.parent) {
// visible
try {
final option = bind.getLocalFlutterOption(k: 'peer-tab-visible');
if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == _isVisible.length) {
for (int i = 0; i < _isVisible.length; i++) {
if (decodeList[i] is bool) {
_isVisible[i] = decodeList[i];
}
}
}
}
} catch (e) {
debugPrint("failed to get peer tab visible list:$e");
}
// init currentTab // init currentTab
_currentTab = _currentTab =
int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0; int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0;
if (_currentTab < 0 || _currentTab >= tabNames.length) { if (_currentTab < 0 || _currentTab >= tabNames.length) {
_currentTab = 0; _currentTab = 0;
} }
_trySetCurrentTabToFirstVisible();
} }
setCurrentTab(int index) { setCurrentTab(int index) {
@@ -64,17 +83,9 @@ class PeerTabModel with ChangeNotifier {
} }
} }
String tabTooltip(int index, String groupName) { String tabTooltip(int index) {
if (index >= 0 && index < tabNames.length) { if (index >= 0 && index < tabNames.length) {
if (index == PeerTabIndex.group.index) { return translate(tabNames[index]);
if (gFFI.userModel.isAdmin.value || groupName.isEmpty) {
return translate(defaultGroupTabname);
} else {
return '${translate('Group')}: $groupName';
}
} else {
return translate(tabNames[index]);
}
} }
assert(false); assert(false);
return index.toString(); return index.toString();
@@ -158,4 +169,31 @@ class PeerTabModel with ChangeNotifier {
} }
} }
} }
setTabVisible(int index, bool visible) {
if (index >= 0 && index < _isVisible.length) {
if (_isVisible[index] != visible) {
_isVisible[index] = visible;
if (index == _currentTab && !visible) {
_trySetCurrentTabToFirstVisible();
} else if (visible && visibleIndexs.length == 1) {
_currentTab = index;
}
try {
bind.setLocalFlutterOption(
k: 'peer-tab-visible', v: jsonEncode(_isVisible));
} catch (_) {}
notifyListeners();
}
}
}
_trySetCurrentTabToFirstVisible() {
if (!_isVisible[_currentTab]) {
int firstVisible = _isVisible.indexWhere((e) => e);
if (firstVisible >= 0) {
_currentTab = firstVisible;
}
}
}
} }

View File

@@ -31,11 +31,12 @@ class ServerModel with ChangeNotifier {
bool _audioOk = false; bool _audioOk = false;
bool _fileOk = false; bool _fileOk = false;
bool _showElevation = false; bool _showElevation = false;
bool _hideCm = false; bool hideCm = false;
int _connectStatus = 0; // Rendezvous Server status int _connectStatus = 0; // Rendezvous Server status
String _verificationMethod = ""; String _verificationMethod = "";
String _temporaryPasswordLength = ""; String _temporaryPasswordLength = "";
String _approveMode = ""; String _approveMode = "";
int _zeroClientLengthCounter = 0;
late String _emptyIdShow; late String _emptyIdShow;
late final IDTextEditingController _serverId; late final IDTextEditingController _serverId;
@@ -60,8 +61,6 @@ class ServerModel with ChangeNotifier {
bool get showElevation => _showElevation; bool get showElevation => _showElevation;
bool get hideCm => _hideCm;
int get connectStatus => _connectStatus; int get connectStatus => _connectStatus;
String get verificationMethod { String get verificationMethod {
@@ -120,6 +119,19 @@ class ServerModel with ChangeNotifier {
_emptyIdShow = translate("Generating ..."); _emptyIdShow = translate("Generating ...");
_serverId = IDTextEditingController(text: _emptyIdShow); _serverId = IDTextEditingController(text: _emptyIdShow);
/*
// initital _hideCm at startup
final verificationMethod =
bind.mainGetOptionSync(key: "verification-method");
final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
_hideCm = option2bool(
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
if (!(approveMode == 'password' &&
verificationMethod == kUsePermanentPassword)) {
_hideCm = false;
}
*/
timerCallback() async { timerCallback() async {
final connectionStatus = final connectionStatus =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>; jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
@@ -134,6 +146,17 @@ class ServerModel with ChangeNotifier {
if (res != null) { if (res != null) {
debugPrint("clients not match!"); debugPrint("clients not match!");
updateClientState(res); updateClientState(res);
} else {
if (_clients.isEmpty) {
hideCmWindow();
if (_zeroClientLengthCounter++ == 12) {
// 6 second
windowManager.close();
}
} else {
_zeroClientLengthCounter = 0;
if (!hideCm) showCmWindow();
}
} }
} }
@@ -187,12 +210,14 @@ class ServerModel with ChangeNotifier {
final temporaryPasswordLength = final temporaryPasswordLength =
await bind.mainGetOption(key: "temporary-password-length"); await bind.mainGetOption(key: "temporary-password-length");
final approveMode = await bind.mainGetOption(key: 'approve-mode'); final approveMode = await bind.mainGetOption(key: 'approve-mode');
/*
var hideCm = option2bool( var hideCm = option2bool(
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm')); 'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
if (!(approveMode == 'password' && if (!(approveMode == 'password' &&
verificationMethod == kUsePermanentPassword)) { verificationMethod == kUsePermanentPassword)) {
hideCm = false; hideCm = false;
} }
*/
if (_approveMode != approveMode) { if (_approveMode != approveMode) {
_approveMode = approveMode; _approveMode = approveMode;
update = true; update = true;
@@ -224,6 +249,7 @@ class ServerModel with ChangeNotifier {
_temporaryPasswordLength = temporaryPasswordLength; _temporaryPasswordLength = temporaryPasswordLength;
update = true; update = true;
} }
/*
if (_hideCm != hideCm) { if (_hideCm != hideCm) {
_hideCm = hideCm; _hideCm = hideCm;
if (desktopType == DesktopType.cm) { if (desktopType == DesktopType.cm) {
@@ -235,6 +261,7 @@ class ServerModel with ChangeNotifier {
} }
update = true; update = true;
} }
*/
if (update) { if (update) {
notifyListeners(); notifyListeners();
} }
@@ -422,6 +449,7 @@ class ServerModel with ChangeNotifier {
return; return;
} }
final oldClientLenght = _clients.length;
_clients.clear(); _clients.clear();
tabController.state.value.tabs.clear(); tabController.state.value.tabs.clear();
@@ -434,6 +462,16 @@ class ServerModel with ChangeNotifier {
debugPrint("Failed to decode clientJson '$clientJson', error $e"); debugPrint("Failed to decode clientJson '$clientJson', error $e");
} }
} }
if (desktopType == DesktopType.cm) {
if (_clients.isEmpty) {
hideCmWindow();
} else if (!hideCm) {
showCmWindow();
}
}
if (_clients.length != oldClientLenght) {
notifyListeners();
}
} }
void addConnection(Map<String, dynamic> evt) { void addConnection(Map<String, dynamic> evt) {
@@ -461,6 +499,9 @@ class ServerModel with ChangeNotifier {
_clients.removeAt(index_disconnected); _clients.removeAt(index_disconnected);
tabController.remove(index_disconnected); tabController.remove(index_disconnected);
} }
if (desktopType == DesktopType.cm && !hideCm) {
showCmWindow();
}
scrollToBottom(); scrollToBottom();
notifyListeners(); notifyListeners();
if (isAndroid && !client.authorized) showLoginDialog(client); if (isAndroid && !client.authorized) showLoginDialog(client);
@@ -581,6 +622,9 @@ class ServerModel with ChangeNotifier {
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
parent.target?.invokeMethod("cancel_notification", id); parent.target?.invokeMethod("cancel_notification", id);
} }
if (desktopType == DesktopType.cm && _clients.isEmpty) {
hideCmWindow();
}
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
debugPrint("onClientRemove failed,error:$e"); debugPrint("onClientRemove failed,error:$e");

View File

@@ -20,6 +20,8 @@ class StateGlobal {
final RxBool showRemoteToolBar = false.obs; final RxBool showRemoteToolBar = false.obs;
final RxInt displaysCount = 0.obs; final RxInt displaysCount = 0.obs;
final svcStatus = SvcStatus.notReady.obs; final svcStatus = SvcStatus.notReady.obs;
// Only used for macOS
bool closeOnFullscreen = false;
// Use for desktop -> remote toolbar -> resolution // Use for desktop -> remote toolbar -> resolution
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {}; final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
@@ -64,7 +66,7 @@ class StateGlobal {
setMinimized(bool v) => _isMinimized = v; setMinimized(bool v) => _isMinimized = v;
setFullscreen(bool v) { setFullscreen(bool v, {bool procWnd = true}) {
if (_fullscreen != v) { if (_fullscreen != v) {
_fullscreen = v; _fullscreen = v;
_showTabBar.value = !_fullscreen; _showTabBar.value = !_fullscreen;
@@ -76,20 +78,22 @@ class StateGlobal {
print( print(
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
_windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth; _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth;
WindowController.fromWindowId(windowId) if (procWnd) {
.setFullscreen(_fullscreen) WindowController.fromWindowId(windowId)
.then((_) { .setFullscreen(_fullscreen)
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 .then((_) {
if (Platform.isWindows && !v) { // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
Future.delayed(Duration.zero, () async { if (Platform.isWindows && !v) {
final frame = Future.delayed(Duration.zero, () async {
await WindowController.fromWindowId(windowId).getFrame(); final frame =
final newRect = Rect.fromLTWH( await WindowController.fromWindowId(windowId).getFrame();
frame.left, frame.top, frame.width + 1, frame.height + 1); final newRect = Rect.fromLTWH(
await WindowController.fromWindowId(windowId).setFrame(newRect); frame.left, frame.top, frame.width + 1, frame.height + 1);
}); await WindowController.fromWindowId(windowId).setFrame(newRect);
} });
}); }
});
}
} }
} }

View File

@@ -45,7 +45,7 @@ class UserModel {
refreshingUser = false; refreshingUser = false;
final status = response.statusCode; final status = response.statusCode;
if (status == 401 || status == 400) { if (status == 401 || status == 400) {
reset(clearAbCache: status == 401); reset(resetOther: status == 401);
return; return;
} }
final data = json.decode(utf8.decode(response.bodyBytes)); final data = json.decode(utf8.decode(response.bodyBytes));
@@ -84,11 +84,13 @@ class UserModel {
} }
} }
Future<void> reset({bool clearAbCache = false}) async { Future<void> reset({bool resetOther = false}) async {
await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: ''); await bind.mainSetLocalOption(key: 'user_info', value: '');
if (clearAbCache) await bind.mainClearAb(); if (resetOther) {
await gFFI.groupModel.reset(); await gFFI.abModel.reset();
await gFFI.groupModel.reset();
}
userName.value = ''; userName.value = '';
} }
@@ -120,7 +122,7 @@ class UserModel {
} catch (e) { } catch (e) {
debugPrint("request /api/logout failed: err=$e"); debugPrint("request /api/logout failed: err=$e");
} finally { } finally {
await reset(clearAbCache: true); await reset(resetOther: true);
gFFI.dialogManager.dismissByTag(tag); gFFI.dialogManager.dismissByTag(tag);
} }
} }

View File

@@ -28,6 +28,13 @@ extension Index on int {
} }
} }
class MultiWindowCallResult {
int windowId;
dynamic result;
MultiWindowCallResult(this.windowId, this.result);
}
/// Window Manager /// Window Manager
/// mainly use it in `Main Window` /// mainly use it in `Main Window`
/// use it in sub window is not recommended /// use it in sub window is not recommended
@@ -47,6 +54,7 @@ class RustDeskMultiWindowManager {
var params = { var params = {
'type': WindowType.RemoteDesktop.index, 'type': WindowType.RemoteDesktop.index,
'id': peerId, 'id': peerId,
'tab_window_id': windowId,
'session_id': sessionId, 'session_id': sessionId,
}; };
await _newSession( await _newSession(
@@ -57,17 +65,15 @@ class RustDeskMultiWindowManager {
_remoteDesktopWindows, _remoteDesktopWindows,
jsonEncode(params), jsonEncode(params),
); );
await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventCloseForSeparateWindow, peerId);
} }
newSessionWindow( Future<int> newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async { WindowType type, String remoteId, String msg, List<int> windows) async {
final windowController = await DesktopMultiWindow.createWindow(msg); final windowController = await DesktopMultiWindow.createWindow(msg);
final windowId = windowController.windowId;
windowController windowController
..setFrame(const Offset(0, 0) & ..setFrame(
Size(1280 + windowController.windowId * 20, const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20))
720 + windowController.windowId * 20))
..center() ..center()
..setTitle(getWindowNameWithId( ..setTitle(getWindowNameWithId(
remoteId, remoteId,
@@ -76,11 +82,12 @@ class RustDeskMultiWindowManager {
if (Platform.isMacOS) { if (Platform.isMacOS) {
Future.microtask(() => windowController.show()); Future.microtask(() => windowController.show());
} }
registerActiveWindow(windowController.windowId); registerActiveWindow(windowId);
windows.add(windowController.windowId); windows.add(windowId);
return windowId;
} }
_newSession( Future<MultiWindowCallResult> _newSession(
bool openInTabs, bool openInTabs,
WindowType type, WindowType type,
String methodName, String methodName,
@@ -90,9 +97,10 @@ class RustDeskMultiWindowManager {
) async { ) async {
if (openInTabs) { if (openInTabs) {
if (windows.isEmpty) { if (windows.isEmpty) {
await newSessionWindow(type, remoteId, msg, windows); final windowId = await newSessionWindow(type, remoteId, msg, windows);
return MultiWindowCallResult(windowId, null);
} else { } else {
call(type, methodName, msg); return call(type, methodName, msg);
} }
} else { } else {
if (_inactiveWindows.isNotEmpty) { if (_inactiveWindows.isNotEmpty) {
@@ -103,15 +111,16 @@ class RustDeskMultiWindowManager {
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg); await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
WindowController.fromWindowId(windowId).show(); WindowController.fromWindowId(windowId).show();
registerActiveWindow(windowId); registerActiveWindow(windowId);
return; return MultiWindowCallResult(windowId, null);
} }
} }
} }
await newSessionWindow(type, remoteId, msg, windows); final windowId = await newSessionWindow(type, remoteId, msg, windows);
return MultiWindowCallResult(windowId, null);
} }
} }
Future<dynamic> newSession( Future<MultiWindowCallResult> newSession(
WindowType type, WindowType type,
String methodName, String methodName,
String remoteId, String remoteId,
@@ -143,15 +152,15 @@ class RustDeskMultiWindowManager {
for (final windowId in windows) { for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod( if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) { windowId, kWindowEventActiveSession, remoteId)) {
return; return MultiWindowCallResult(windowId, null);
} }
} }
} }
await _newSession(openInTabs, type, methodName, remoteId, windows, msg); return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
} }
Future<dynamic> newRemoteDesktop( Future<MultiWindowCallResult> newRemoteDesktop(
String remoteId, { String remoteId, {
String? password, String? password,
String? switchUuid, String? switchUuid,
@@ -168,7 +177,7 @@ class RustDeskMultiWindowManager {
); );
} }
Future<dynamic> newFileTransfer(String remoteId, Future<MultiWindowCallResult> newFileTransfer(String remoteId,
{String? password, bool? forceRelay}) async { {String? password, bool? forceRelay}) async {
return await newSession( return await newSession(
WindowType.FileTransfer, WindowType.FileTransfer,
@@ -180,7 +189,7 @@ class RustDeskMultiWindowManager {
); );
} }
Future<dynamic> newPortForward(String remoteId, bool isRDP, Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
{String? password, bool? forceRelay}) async { {String? password, bool? forceRelay}) async {
return await newSession( return await newSession(
WindowType.PortForward, WindowType.PortForward,
@@ -193,18 +202,22 @@ class RustDeskMultiWindowManager {
); );
} }
Future<dynamic> call(WindowType type, String methodName, dynamic args) async { Future<MultiWindowCallResult> call(
WindowType type, String methodName, dynamic args) async {
final wnds = _findWindowsByType(type); final wnds = _findWindowsByType(type);
if (wnds.isEmpty) { if (wnds.isEmpty) {
return; return MultiWindowCallResult(kInvalidWindowId, null);
} }
for (final windowId in wnds) { for (final windowId in wnds) {
if (_activeWindows.contains(windowId)) { if (_activeWindows.contains(windowId)) {
return await DesktopMultiWindow.invokeMethod( final res =
windowId, methodName, args); await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
return MultiWindowCallResult(windowId, res);
} }
} }
return await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args); final res =
await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
return MultiWindowCallResult(wnds[0], res);
} }
List<int> _findWindowsByType(WindowType type) { List<int> _findWindowsByType(WindowType type) {

View File

@@ -37,8 +37,6 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<string>1</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string> <string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>

View File

@@ -237,10 +237,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.2" version: "1.17.1"
colorize: colorize:
dependency: transitive dependency: transitive
description: description:
@@ -300,11 +300,12 @@ packages:
dash_chat_2: dash_chat_2:
dependency: "direct main" dependency: "direct main"
description: description:
name: dash_chat_2 path: "."
sha256: e9e08b2a030d340d60f7adbeb977d3d6481db1f172b51440bfa02488b92fa19c ref: HEAD
url: "https://pub.dev" resolved-ref: bd6b5b41254e57c5bcece202ebfb234de63e6487
source: hosted url: "https://github.com/rustdesk-org/Dash-Chat-2"
version: "0.0.17" source: git
version: "0.0.18"
debounce_throttle: debounce_throttle:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -327,7 +328,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "6c4181330f4ed80c1cb5670bd61aa75115f9f748" resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git source: git
version: "0.1.0" version: "0.1.0"
@@ -395,6 +396,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
dynamic_layouts:
dependency: "direct main"
description:
path: "."
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
url: "https://github.com/21pages/dynamic_layouts.git"
source: git
version: "0.0.1+1"
event_bus: event_bus:
dependency: transitive dependency: transitive
description: description:
@@ -451,6 +461,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
url: "https://pub.dev"
source: hosted
version: "3.3.0"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -743,10 +769,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.1" version: "0.18.0"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -799,10 +825,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.2.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -1019,6 +1045,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
pull_down_button:
dependency: "direct main"
description:
name: pull_down_button
sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f"
url: "https://pub.dev"
source: hosted
version: "0.9.3"
puppeteer: puppeteer:
dependency: transitive dependency: transitive
description: description:
@@ -1054,12 +1088,11 @@ packages:
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
path: "." name: screen_retriever
ref: "406b9b0" sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
resolved-ref: "406b9b038b2c1d779f1e7bf609c8c248be247372" url: "https://pub.dev"
url: "https://github.com/Kingtous/rustdesk_screen_retriever.git" source: hosted
source: git version: "0.1.9"
version: "0.1.2"
scroll_pos: scroll_pos:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1382,10 +1415,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: video_player name: video_player
sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28" sha256: d3910a8cefc0de8a432a4411dcf85030e885d8fef3ddea291f162253a05dbf01
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.7.1"
video_player_android: video_player_android:
dependency: transitive dependency: transitive
description: description:
@@ -1406,10 +1439,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: video_player_platform_interface name: video_player_platform_interface
sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae" sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" version: "6.2.1"
video_player_web: video_player_web:
dependency: transitive dependency: transitive
description: description:
@@ -1474,14 +1507,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@@ -1511,10 +1536,10 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "2c4b242e668acf4e652b09b13f650bcfbbaa3871" resolved-ref: f19acdb008645366339444a359a45c3257c8b32e
url: "https://github.com/rustdesk-org/window_manager" url: "https://github.com/rustdesk-org/window_manager"
source: git source: git
version: "0.3.4" version: "0.3.6"
window_size: window_size:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1565,5 +1590,5 @@ packages:
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" dart: ">=3.0.0 <4.0.0"
flutter: ">=3.7.0-0" flutter: ">=3.10.0"

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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 # 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.2.2 version: 1.2.3+39
environment: environment:
sdk: ">=2.17.0" sdk: ">=2.17.0"
@@ -39,7 +39,9 @@ dependencies:
package_info_plus: ^3.1.2 package_info_plus: ^3.1.2
url_launcher: ^6.0.9 url_launcher: ^6.0.9
toggle_switch: ^2.1.0 toggle_switch: ^2.1.0
dash_chat_2: ^0.0.17 dash_chat_2:
git:
url: https://github.com/rustdesk-org/Dash-Chat-2
draggable_float_widget: ^0.0.2 draggable_float_widget: ^0.0.2
settings_ui: ^2.0.2 settings_ui: ^2.0.2
flutter_breadcrumb: ^1.0.1 flutter_breadcrumb: ^1.0.1
@@ -97,6 +99,12 @@ dependencies:
dropdown_button2: ^2.0.0 dropdown_button2: ^2.0.0
uuid: ^3.0.7 uuid: ^3.0.7
auto_size_text_field: ^2.2.1 auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0
dynamic_layouts:
git:
url: https://github.com/21pages/dynamic_layouts.git
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3
dev_dependencies: dev_dependencies:
icons_launcher: ^2.0.4 icons_launcher: ^2.0.4
@@ -149,9 +157,6 @@ flutter:
- family: AddressBook - family: AddressBook
fonts: fonts:
- asset: assets/address_book.ttf - asset: assets/address_book.ttf
- family: CheckBox
fonts:
- asset: assets/checkbox.ttf
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware. # https://flutter.dev/assets-and-images/#resolution-aware.

View File

@@ -16,7 +16,7 @@ final testClients = [
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
]; ];
/// flutter run -d {platform} -t lib/cm_test.dart to test cm /// flutter run -d {platform} -t test/cm_test.dart to test cm
void main(List<String> args) async { void main(List<String> args) async {
isTest = true; isTest = true;
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();

View File

@@ -93,7 +93,7 @@ BEGIN
VALUE "FileDescription", "rustdesk" "\0" VALUE "FileDescription", "rustdesk" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "rustdesk" "\0" VALUE "InternalName", "rustdesk" "\0"
VALUE "LegalCopyright", "Copyright (C) 2022 com.carriez. All rights reserved." "\0" VALUE "LegalCopyright", "Copyright (C) 2023 com.carriez. All rights reserved." "\0"
VALUE "OriginalFilename", "rustdesk.exe" "\0" VALUE "OriginalFilename", "rustdesk.exe" "\0"
VALUE "ProductName", "rustdesk" "\0" VALUE "ProductName", "rustdesk" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"

View File

@@ -70,6 +70,8 @@ pub use win::ENIGO_INPUT_EXTRA_VALUE;
mod macos; mod macos;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub use macos::Enigo; pub use macos::Enigo;
#[cfg(target_os = "macos")]
pub use macos::ENIGO_INPUT_EXTRA_VALUE;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod linux; mod linux;

View File

@@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3;
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
const BUF_LEN: usize = 4; const BUF_LEN: usize = 4;
/// The event source user data value of cgevent.
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
#[allow(improper_ctypes)] #[allow(improper_ctypes)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[link(name = "ApplicationServices", kind = "framework")] #[link(name = "ApplicationServices", kind = "framework")]
@@ -131,6 +134,7 @@ impl Enigo {
fn post(&self, event: CGEvent) { fn post(&self, event: CGEvent) {
event.set_flags(self.flags); event.set_flags(self.flags);
event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE);
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
} }
} }

View File

@@ -1,4 +1,4 @@
mod macos_impl; mod macos_impl;
pub mod keycodes; pub mod keycodes;
pub use self::macos_impl::Enigo; pub use self::macos_impl::{Enigo, ENIGO_INPUT_EXTRA_VALUE};

View File

@@ -1,5 +1,4 @@
mod win_impl; mod win_impl;
pub mod keycodes; pub mod keycodes;
pub use self::win_impl::Enigo; pub use self::win_impl::{Enigo, ENIGO_INPUT_EXTRA_VALUE};
pub use self::win_impl::ENIGO_INPUT_EXTRA_VALUE;

View File

@@ -118,9 +118,29 @@ message TouchScaleUpdate {
int32 scale = 1; int32 scale = 1;
} }
message TouchPanStart {
int32 x = 1;
int32 y = 2;
}
message TouchPanUpdate {
// The delta x position relative to the previous position.
int32 x = 1;
// The delta y position relative to the previous position.
int32 y = 2;
}
message TouchPanEnd {
int32 x = 1;
int32 y = 2;
}
message TouchEvent { message TouchEvent {
oneof union { oneof union {
TouchScaleUpdate scale_update = 1; TouchScaleUpdate scale_update = 1;
TouchPanStart pan_start = 2;
TouchPanUpdate pan_update = 3;
TouchPanEnd pan_end = 4;
} }
} }
@@ -377,6 +397,7 @@ message FileTransferReceiveRequest {
string path = 2; // path written to string path = 2; // path written to
repeated FileEntry files = 3; repeated FileEntry files = 3;
int32 file_num = 4; int32 file_num = 4;
uint64 total_size = 5;
} }
message FileRemoveDir { message FileRemoveDir {
@@ -603,6 +624,8 @@ message BackNotification {
PrivacyModeState privacy_mode_state = 1; PrivacyModeState privacy_mode_state = 1;
BlockInputState block_input_state = 2; BlockInputState block_input_state = 2;
} }
// Supplementary message, for "PrvOnFailed" and "PrvOffFailed"
string details = 3;
} }
message ElevationRequestWithLogon { message ElevationRequestWithLogon {

View File

@@ -214,7 +214,7 @@ pub struct Resolution {
pub h: i32, pub h: i32,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig { pub struct PeerConfig {
#[serde(default, deserialize_with = "deserialize_vec_u8")] #[serde(default, deserialize_with = "deserialize_vec_u8")]
pub password: Vec<u8>, pub password: Vec<u8>,
@@ -230,6 +230,7 @@ pub struct PeerConfig {
skip_serializing_if = "String::is_empty" skip_serializing_if = "String::is_empty"
)] )]
pub view_style: String, pub view_style: String,
// Image scroll style, scrollbar or scroll auto
#[serde( #[serde(
default = "PeerConfig::default_scroll_style", default = "PeerConfig::default_scroll_style",
deserialize_with = "PeerConfig::deserialize_scroll_style", deserialize_with = "PeerConfig::deserialize_scroll_style",
@@ -276,6 +277,13 @@ pub struct PeerConfig {
pub keyboard_mode: String, pub keyboard_mode: String,
#[serde(flatten)] #[serde(flatten)]
pub view_only: ViewOnly, pub view_only: ViewOnly,
// Mouse wheel or touchpad scroll mode
#[serde(
default = "PeerConfig::default_reverse_mouse_wheel",
deserialize_with = "PeerConfig::deserialize_reverse_mouse_wheel",
skip_serializing_if = "String::is_empty"
)]
pub reverse_mouse_wheel: String,
#[serde( #[serde(
default, default,
@@ -296,6 +304,39 @@ pub struct PeerConfig {
pub transfer: TransferSerde, pub transfer: TransferSerde,
} }
impl Default for PeerConfig {
fn default() -> Self {
Self {
password: Default::default(),
size: Default::default(),
size_ft: Default::default(),
size_pf: Default::default(),
view_style: Self::default_view_style(),
scroll_style: Self::default_scroll_style(),
image_quality: Self::default_image_quality(),
custom_image_quality: Self::default_custom_image_quality(),
show_remote_cursor: Default::default(),
lock_after_session_end: Default::default(),
privacy_mode: Default::default(),
allow_swap_key: Default::default(),
port_forwards: Default::default(),
direct_failures: Default::default(),
disable_audio: Default::default(),
disable_clipboard: Default::default(),
enable_file_transfer: Default::default(),
show_quality_monitor: Default::default(),
keyboard_mode: Default::default(),
view_only: Default::default(),
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
custom_resolutions: Default::default(),
options: Self::default_options(),
ui_flutter: Default::default(),
info: Default::default(),
transfer: Default::default(),
}
}
}
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
pub struct PeerInfoSerde { pub struct PeerInfoSerde {
#[serde(default, deserialize_with = "deserialize_string")] #[serde(default, deserialize_with = "deserialize_string")]
@@ -1098,6 +1139,11 @@ impl PeerConfig {
deserialize_image_quality, deserialize_image_quality,
UserDefaultConfig::read().get("image_quality") UserDefaultConfig::read().get("image_quality")
); );
serde_field_string!(
default_reverse_mouse_wheel,
deserialize_reverse_mouse_wheel,
UserDefaultConfig::read().get("reverse_mouse_wheel")
);
fn default_custom_image_quality() -> Vec<i32> { fn default_custom_image_quality() -> Vec<i32> {
let f: f64 = UserDefaultConfig::read() let f: f64 = UserDefaultConfig::read()
@@ -1124,6 +1170,17 @@ impl PeerConfig {
D: de::Deserializer<'de>, D: de::Deserializer<'de>,
{ {
let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?; let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?;
Self::insert_default_options(&mut mp);
Ok(mp)
}
fn default_options() -> HashMap<String, String> {
let mut mp: HashMap<String, String> = Default::default();
Self::insert_default_options(&mut mp);
return mp;
}
fn insert_default_options(mp: &mut HashMap<String, String>) {
let mut key = "codec-preference"; let mut key = "codec-preference";
if !mp.contains_key(key) { if !mp.contains_key(key) {
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
@@ -1136,7 +1193,10 @@ impl PeerConfig {
if !mp.contains_key(key) { if !mp.contains_key(key) {
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
} }
Ok(mp) key = "touch-mode";
if !mp.contains_key(key) {
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
}
} }
} }
@@ -1437,7 +1497,11 @@ impl UserDefaultConfig {
} }
pub fn set(&mut self, key: String, value: String) { pub fn set(&mut self, key: String, value: String) {
self.options.insert(key, value); if value.is_empty() {
self.options.remove(&key);
} else {
self.options.insert(key, value);
}
self.store(); self.store();
} }
@@ -1525,6 +1589,12 @@ pub struct Ab {
pub peers: Vec<AbPeer>, pub peers: Vec<AbPeer>,
#[serde(default, deserialize_with = "deserialize_vec_string")] #[serde(default, deserialize_with = "deserialize_vec_string")]
pub tags: Vec<String>, pub tags: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub tag_colors: String,
} }
impl Ab { impl Ab {
@@ -1580,6 +1650,106 @@ macro_rules! deserialize_default {
}; };
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct GroupPeer {
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub id: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub username: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub hostname: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub platform: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub login_name: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct GroupUser {
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub name: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Group {
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub access_token: String,
#[serde(default, deserialize_with = "deserialize_vec_groupuser")]
pub users: Vec<GroupUser>,
#[serde(default, deserialize_with = "deserialize_vec_grouppeer")]
pub peers: Vec<GroupPeer>,
}
impl Group {
fn path() -> PathBuf {
let filename = format!("{}_group", APP_NAME.read().unwrap().clone());
Config::path(filename)
}
pub fn store(json: String) {
if let Ok(mut file) = std::fs::File::create(Self::path()) {
let data = compress(json.as_bytes());
let max_len = 64 * 1024 * 1024;
if data.len() > max_len {
// maxlen of function decompress
return;
}
if let Ok(data) = symmetric_crypt(&data, true) {
file.write_all(&data).ok();
}
};
}
pub fn load() -> Self {
if let Ok(mut file) = std::fs::File::open(Self::path()) {
let mut data = vec![];
if file.read_to_end(&mut data).is_ok() {
if let Ok(data) = symmetric_crypt(&data, false) {
let data = decompress(&data);
if let Ok(group) = serde_json::from_str::<Self>(&String::from_utf8_lossy(&data))
{
return group;
}
}
}
};
Self::remove();
Self::default()
}
pub fn remove() {
std::fs::remove_file(Self::path()).ok();
}
}
deserialize_default!(deserialize_string, String); deserialize_default!(deserialize_string, String);
deserialize_default!(deserialize_bool, bool); deserialize_default!(deserialize_bool, bool);
deserialize_default!(deserialize_i32, i32); deserialize_default!(deserialize_i32, i32);
@@ -1588,6 +1758,8 @@ deserialize_default!(deserialize_vec_string, Vec<String>);
deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>);
deserialize_default!(deserialize_vec_discoverypeer, Vec<DiscoveryPeer>); deserialize_default!(deserialize_vec_discoverypeer, Vec<DiscoveryPeer>);
deserialize_default!(deserialize_vec_abpeer, Vec<AbPeer>); deserialize_default!(deserialize_vec_abpeer, Vec<AbPeer>);
deserialize_default!(deserialize_vec_groupuser, Vec<GroupUser>);
deserialize_default!(deserialize_vec_grouppeer, Vec<GroupPeer>);
deserialize_default!(deserialize_keypair, KeyPair); deserialize_default!(deserialize_keypair, KeyPair);
deserialize_default!(deserialize_size, Size); deserialize_default!(deserialize_size, Size);
deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>); deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>);

View File

@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use tokio::{fs::File, io::*}; use tokio::{fs::File, io::*};
use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream}; use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
@@ -194,7 +195,8 @@ pub fn can_enable_overwrite_detection(version: i64) -> bool {
version >= get_version_number("1.1.10") version >= get_version_number("1.1.10")
} }
#[derive(Default)] #[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransferJob { pub struct TransferJob {
pub id: i32, pub id: i32,
pub remote: String, pub remote: String,
@@ -203,10 +205,13 @@ pub struct TransferJob {
pub is_remote: bool, pub is_remote: bool,
pub is_last_job: bool, pub is_last_job: bool,
pub file_num: i32, pub file_num: i32,
#[serde(skip_serializing)]
pub files: Vec<FileEntry>, pub files: Vec<FileEntry>,
pub conn_id: i32, // server only
#[serde(skip_serializing)]
file: Option<File>, file: Option<File>,
total_size: u64, pub total_size: u64,
finished_size: u64, finished_size: u64,
transferred: u64, transferred: u64,
enable_overwrite_detection: bool, enable_overwrite_detection: bool,
@@ -695,13 +700,20 @@ pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
} }
#[inline] #[inline]
pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message { pub fn new_receive(
id: i32,
path: String,
file_num: i32,
files: Vec<FileEntry>,
total_size: u64,
) -> Message {
let mut action = FileAction::new(); let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest { action.set_receive(FileTransferReceiveRequest {
id, id,
path, path,
files, files,
file_num, file_num,
total_size,
..Default::default() ..Default::default()
}); });
let mut msg_out = Message::new(); let mut msg_out = Message::new();
@@ -748,10 +760,16 @@ pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
jobs.iter_mut().find(|x| x.id() == id) jobs.iter_mut().find(|x| x.id() == id)
} }
#[inline]
pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
jobs.iter().find(|x| x.id() == id)
}
pub async fn handle_read_jobs( pub async fn handle_read_jobs(
jobs: &mut Vec<TransferJob>, jobs: &mut Vec<TransferJob>,
stream: &mut crate::Stream, stream: &mut crate::Stream,
) -> ResultType<()> { ) -> ResultType<String> {
let mut job_log = Default::default();
let mut finished = Vec::new(); let mut finished = Vec::new();
for job in jobs.iter_mut() { for job in jobs.iter_mut() {
if job.is_last_job { if job.is_last_job {
@@ -768,9 +786,11 @@ pub async fn handle_read_jobs(
} }
Ok(None) => { Ok(None) => {
if job.job_completed() { if job.job_completed() {
job_log = serialize_transfer_job(job, true, false, "");
finished.push(job.id()); finished.push(job.id());
match job.job_error() { match job.job_error() {
Some(err) => { Some(err) => {
job_log = serialize_transfer_job(job, false, false, &err);
stream stream
.send(&new_error(job.id(), err, job.file_num())) .send(&new_error(job.id(), err, job.file_num()))
.await? .await?
@@ -786,7 +806,7 @@ pub async fn handle_read_jobs(
for id in finished { for id in finished {
remove_job(id, jobs); remove_job(id, jobs);
} }
Ok(()) Ok(job_log)
} }
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
@@ -861,3 +881,20 @@ pub fn is_write_need_confirmation(
Ok(DigestCheckResult::NoSuchFile) Ok(DigestCheckResult::NoSuchFile)
} }
} }
pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
let mut v = vec![];
for job in jobs {
let value = serde_json::to_value(job).unwrap_or_default();
v.push(value);
}
serde_json::to_string(&v).unwrap_or_default()
}
pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
let mut value = serde_json::to_value(job).unwrap_or_default();
value["done"] = json!(done);
value["cancel"] = json!(cancel);
value["error"] = json!(error);
serde_json::to_string(&value).unwrap_or_default()
}

View File

@@ -103,15 +103,16 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String
// String: password // String: password
// bool: whether decryption is successful // bool: whether decryption is successful
// bool: whether should store to re-encrypt when load // bool: whether should store to re-encrypt when load
// note: s.len() return length in bytes, s.chars().count() return char count
// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars
pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) { pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
if s.len() > VERSION_LEN { if s.len() > VERSION_LEN {
let version = &s[..VERSION_LEN]; if s.starts_with("00") {
if version == "00" {
if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
return ( return (
String::from_utf8_lossy(&v).to_string(), String::from_utf8_lossy(&v).to_string(),
true, true,
version != current_version, "00" != current_version,
); );
} }
} }
@@ -198,7 +199,7 @@ mod test {
let max_len = 128; let max_len = 128;
println!("test str"); println!("test str");
let data = "Hello World"; let data = "1ü1111";
let encrypted = encrypt_str_or_original(data, version, max_len); let encrypted = encrypt_str_or_original(data, version, max_len);
let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
println!("data: {data}"); println!("data: {data}");
@@ -217,7 +218,7 @@ mod test {
); );
println!("test vec"); println!("test vec");
let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6]; let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
let encrypted = encrypt_vec_or_original(&data, version, max_len); let encrypted = encrypt_vec_or_original(&data, version, max_len);
let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
println!("data: {data:?}"); println!("data: {data:?}");
@@ -253,6 +254,10 @@ mod test {
let (_, succ, store) = decrypt_vec_or_original(&[], version); let (_, succ, store) = decrypt_vec_or_original(&[], version);
assert!(!store); assert!(!store);
assert!(!succ); assert!(!succ);
let data = "1ü1111";
assert_eq!(decrypt_str_or_original(data, version).0, data);
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
assert_eq!(decrypt_vec_or_original(&data, version).0, data);
println!("test speed"); println!("test speed");
let test_speed = |len: usize, name: &str| { let test_speed = |len: usize, name: &str| {

View File

@@ -183,6 +183,15 @@ pub fn is_active(sid: &str) -> bool {
} }
} }
pub fn is_active_and_seat0(sid: &str) -> bool {
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
String::from_utf8_lossy(&output.stdout).contains("State=active")
&& String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
} else {
false
}
}
pub fn run_cmds(cmds: &str) -> ResultType<String> { pub fn run_cmds(cmds: &str) -> ResultType<String> {
let output = std::process::Command::new("sh") let output = std::process::Command::new("sh")
.args(vec!["-c", cmds]) .args(vec!["-c", cmds])

View File

@@ -1,5 +1,10 @@
extern crate embed_resource; extern crate embed_resource;
use std::fs;
fn main() { fn main() {
embed_resource::compile("icon.rc", embed_resource::NONE); let runner_res_path = "Runner.res";
match fs::metadata(runner_res_path) {
Ok(_) => println!("cargo:rustc-link-lib=dylib:+verbatim=./libs/portable/Runner.res"),
Err(_) => embed_resource::compile("icon.rc", embed_resource::NONE),
}
} }

View File

@@ -154,17 +154,18 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init(
} }
} }
pub fn call_main_service_mouse_input(mask: i32, x: i32, y: i32) -> JniResult<()> { pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = ( if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(), JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(), MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) { ) {
let mut env = jvm.attach_current_thread_as_daemon()?; let mut env = jvm.attach_current_thread_as_daemon()?;
let kind = env.new_string(kind)?;
env.call_method( env.call_method(
ctx, ctx,
"rustMouseInput", "rustPointerInput",
"(III)V", "(Ljava/lang/String;III)V",
&[JValue::Int(mask), JValue::Int(x), JValue::Int(y)], &[JValue::Object(&JObject::from(kind)), JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
)?; )?;
return Ok(()); return Ok(());
} else { } else {

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