338 Commits
1.2.6 ... 1.3.0

257 changed files with 15696 additions and 5478 deletions

View File

@@ -1,8 +1,14 @@
[target.x86_64-pc-windows-msvc] [target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"] rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc] [target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.'cfg(target_os="macos")'] [target.'cfg(target_os="macos")']
rustflags = [ rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
] ]
#[target.'cfg(target_os="linux")']
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
# this is unlikely to help also, because the other so files still use libc dynamically
#rustflags = [
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
#]

View File

@@ -4,9 +4,9 @@ env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0" # MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19 # vcpkg version: 2024.06.15
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50" VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
on: on:
workflow_dispatch: workflow_dispatch:
@@ -112,6 +112,8 @@ jobs:
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \ libgtk-3-dev \
libpulse-dev \ libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \

39
.github/workflows/fdroid.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Fdroid version file generation
on:
workflow_dispatch:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
jobs:
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
# Finds latest release and transforms F-Droid version code from version as follows:
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater
runs-on: ubuntu-latest
steps:
- name: Generate RustDesk version file
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}"
UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}"
else
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
fi
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"
files: |
./rustdesk-version.txt

File diff suppressed because it is too large Load Diff

View File

@@ -10,15 +10,15 @@ env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
CARGO_NDK_VERSION: "3.1.2" CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6" LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.13.9" FLUTTER_VERSION: "3.22.2"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work # for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly" TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.03.25 # vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff" VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.2.6" VERSION: "1.3.0"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -31,7 +31,207 @@ env:
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs: jobs:
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
keychain: rustdesk
- name: Check sign and import sign key
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
fileDir: ${{ github.workspace }}
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
popd
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
shell: bash
run: |
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
# below works for mac to make buildable on 3.13.9
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
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/macos/Runner/bridge_generated.h
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Restore from cache and install vcpkg
uses: lukka/run-vcpkg@v7
if: false
with:
setupOnly: true
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
if: false
run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
- name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
clang --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build rustdesk
run: |
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
run: |
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null
run: |
# Patch create-dmg to give more attempts to unmount image
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
# Unlock keychain
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
done
- name: Publish DMG package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-${{ matrix.job.arch }}*.dmg
build-rustdesk-android: build-rustdesk-android:
if: false
name: build rustdesk android apk ${{ matrix.job.target }} name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
strategy: strategy:

View File

@@ -6,7 +6,7 @@ jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v2 - uses: vedantmgoyal9/winget-releaser@main
with: with:
identifier: RustDesk.RustDesk identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }} version: ${{ github.event.release.tag_name }}

2699
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.6" version = "1.3.0"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -66,7 +66,7 @@ default-net = "0.14"
wol-rs = "1.0" wol-rs = "1.0"
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true} flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
errno = "0.3" errno = "0.3"
rdev = { git = "https://github.com/fufesou/rdev" } rdev = { git = "https://github.com/rustdesk-org/rdev" }
url = { version = "2.3", features = ["serde"] } url = { version = "2.3", features = ["serde"] }
crossbeam-queue = "0.3" crossbeam-queue = "0.3"
hex = "0.4" hex = "0.4"
@@ -89,7 +89,10 @@ sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] } enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" } clipboard = { path = "libs/clipboard" }
ctrlc = "3.2" ctrlc = "3.2"
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] } # arboard = { version = "3.4.0", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
system_shutdown = "4.0" system_shutdown = "4.0"
qrcode-generator = "4.1" qrcode-generator = "4.1"
@@ -111,7 +114,7 @@ winapi = { version = "0.3", features = [
winreg = "0.11" winreg = "0.11"
windows-service = "0.6" windows-service = "0.6"
virtual_display = { path = "libs/virtual_display" } virtual_display = { path = "libs/virtual_display" }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
tauri-winrt-notification = "0.1.2" tauri-winrt-notification = "0.1.2"
runas = "1.2" runas = "1.2"
@@ -135,7 +138,7 @@ image = "0.24"
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" } wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
@@ -149,11 +152,10 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.7" async-process = "1.7"
mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/rustdesk-org/evdev" }
evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9" dbus = "0.9"
dbus-crossroads = "0.5" dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam" } pam = { git="https://github.com/rustdesk-org/pam" }
users = { version = "0.11" } users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
@@ -163,7 +165,7 @@ once_cell = {version = "1.18", optional = true}
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"
jni = "0.21" jni = "0.21"
android-wakelock = { git = "https://github.com/21pages/android-wakelock" } android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace] [workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]

View File

@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
```sh ```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ 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 \ 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 libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
``` ```
### openSUSE Tumbleweed ### openSUSE Tumbleweed
```sh ```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 sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
```sh ```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 sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.6 version: 1.3.0
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.6 version: 1.3.0
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -37,6 +37,9 @@ AppDir:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse universe multiverse
include: include:
# https://github.com/rustdesk/rustdesk/issues/9103
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
- libc6:amd64 - libc6:amd64
- libgtk-3-0 - libgtk-3-0
- libxcb-randr0 - libxcb-randr0

View File

@@ -25,12 +25,17 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
skip_cargo = False skip_cargo = False
def get_arch() -> str: def get_deb_arch() -> str:
custom_arch = os.environ.get("ARCH") custom_arch = os.environ.get("DEB_ARCH")
if custom_arch is None: if custom_arch is None:
return "amd64" return "amd64"
return custom_arch return custom_arch
def get_deb_extra_depends() -> str:
custom_arch = os.environ.get("DEB_ARCH")
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
return ", libatomic1"
return ""
def system2(cmd): def system2(cmd):
exit_code = os.system(cmd) exit_code = os.system(cmd)
@@ -48,15 +53,7 @@ def get_version():
def parse_rc_features(feature): def parse_rc_features(feature):
available_features = { available_features = {}
'PrivacyMode': {
'platform': ['windows'],
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
'/TempTopMostWindow_x64.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
'include': ['WindowInjection.dll'],
}
}
apply_features = {} apply_features = {}
if not feature: if not feature:
feature = [] feature = []
@@ -81,7 +78,6 @@ def parse_rc_features(feature):
elif isinstance(feature, list): elif isinstance(feature, list):
if windows: if windows:
# download third party is deprecated, we use github ci instead. # download third party is deprecated, we use github ci instead.
# force add PrivacyMode
# feature.append('PrivacyMode') # feature.append('PrivacyMode')
pass pass
for feat in feature: for feat in feature:
@@ -108,7 +104,7 @@ def make_parser():
nargs='+', nargs='+',
default='', default='',
help='Integrate features, windows only.' help='Integrate features, windows only.'
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.') 'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
parser.add_argument('--flutter', action='store_true', parser.add_argument('--flutter', action='store_true',
help='Build flutter package', default=False) help='Build flutter package', default=False)
parser.add_argument( parser.add_argument(
@@ -291,10 +287,10 @@ Version: %s
Architecture: %s Architecture: %s
Maintainer: rustdesk <info@rustdesk.com> Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire%s
Description: A remote control software. Description: A remote control software.
""" % (version, get_arch()) """ % (version, get_deb_arch(), get_deb_extra_depends())
file = open(control_file_path, "w") file = open(control_file_path, "w")
file.write(content) file.write(content)
file.close() file.close()

View File

@@ -1,60 +1,73 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
<a href="#free-public-servers">Servers</a> • <a href="#free-public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<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="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-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>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b> <b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
</p> </p>
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 私たちと話す: [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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。 Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/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)
RustDeskは誰からの貢献歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。 RustDeskは皆さんの貢献歓迎します。
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
[**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**パッケージのダウンロード**](https://github.com/rustdesk/rustdesk/releases)
[**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="F-Droidで入手する"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## 依存関係 ## 依存関係
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。 デスクトップ版ではGUIにFlutterまたはSciter(非推奨)を使用しますが、チュートリアルでは分かりやすく、簡単なSciterのみを対象に解説しています。Flutterでのビルド方法については[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)をご覧ください。
Sciter dynamic libraryを事前にダウンロードしてください。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [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) | [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) [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
モバイル版はFlutterを利用します。デスクトップ版もSciterからFlutterへマイグレーション予定です。
## ビルド手順 ## ビルド手順
- Rust開発環境とC ++ビルド環境を準備します - Rust開発環境とC++ビルド環境を準備します
- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。 - [vcpkg](https://github.com/microsoft/vcpkg)をインストールし、環境変数に`VCPKG_ROOT`設定します。
その後、以下のコマンドを実行します。
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
- run `cargo run`
- 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`を実行します。
## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## [ビルド](https://rustdesk.com/docs/en/dev/build/)
## Linuxでのビルド手順 ## Linuxでのビルド方法
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
```sh ```sh
sudo apt install -y 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 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) ### Fedora 28 (CentOS 8)
@@ -69,7 +82,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
``` ```
### Install vcpkg ### vcpkgのインストール
```sh ```sh
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
@@ -81,7 +94,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
``` ```
### Fix libvpx (For Fedora) ### libvpxの修正 (Fedoraのみ)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src
@@ -107,9 +120,9 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
## Dockerでビルドする方法 ## Dockerでビルド方法
リポジトリクローンを作成し、Dockerコンテナを構築することから始めます リポジトリクローンし、Dockerコンテナを構築ます:
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
@@ -117,44 +130,50 @@ cd rustdesk
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します 以下のコマンドを実行します:
```sh ```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 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
``` ```
このコマンドはRustDeskをビルドする度に実行する必要があります。
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に 初回ビルドは時間がかかるかもしれませんが、2回目以降は依存関係がキャッシュされるため、ビルドにかかる時間が短くなります。
`--release` を実行します。できあがった実行ファイルはシステムのターゲットフォルダに格納され、のコマンドで実行できます。 ビルドコマンドに追加の引数を指定する必要がある場合は、コマンドの最後(`<OPTIONAL-ARGS>`の位置)で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に `--release`追記し実行します。ビルドされた実行ファイルはあなたのシステムのターゲットフォルダに保存され、下記のコマンドで実行することができます。
デバッグビルドを起動する場合:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
``` ```
あるいは、リリース用の実行ファイルを実行している場合: リリースビルドを起動する場合:
```sh ```sh
target/release/rustdesk target/release/rustdesk
``` ```
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install``run` などの他の cargo サブコマンドは、ホストではなくコンテナ内プログラムをインストールまたは実行するため、現在の方法ではサポートされていないことに注意してください コマンドをRustDeskリポジトリのルートから実行していることを確認してください。また、`install``run` などの他のcargoサブコマンドは、ホストではなくコンテナ内プログラムをインストール実行するため、現在の方法ではサポートされていません
## ファイル構造 ## ファイル構造
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数その他のユーティリティ関数 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、設定、tcp/udpラッパー、protobuf、ファイル転送に利用されるfs関数その他のユーティリティ関数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウス操作
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS向けのファイルのコピーと貼り付けの実装
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続 - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止された Sciter UI (非推奨)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**:
オーディオ/クリップボード/入力/ビデオ サービスとネットワーク接続
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
## スナップショット ## スクリーンショット
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -18,7 +18,9 @@ import android.widget.EditText
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import android.view.KeyEvent as KeyEventAndroid
import android.graphics.Rect import android.graphics.Rect
import android.media.AudioManager
import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
private var fakeEditTextForTextStateCalculation: EditText? = null private var fakeEditTextForTextStateCalculation: EditText? = null
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) { fun onMouseInput(mask: Int, _x: Int, _y: Int) {
val x = max(0, _x) val x = max(0, _x)
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit") Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
var ke: KeyEventAndroid? = null
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
}
ke?.let { event ->
if (tryHandleVolumeKeyEvent(event)) {
return
} else if (tryHandlePowerKeyEvent(event)) {
return
}
}
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
getInputMethod()?.let { inputMethod -> getInputMethod()?.let { inputMethod ->
inputMethod.getCurrentInputConnection()?.let { inputConnection -> inputMethod.getCurrentInputConnection()?.let { inputConnection ->
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
inputConnection.commitText(text, 1, null) inputConnection.commitText(text, 1, null)
} }
} else { } else {
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event -> ke?.let { event ->
inputConnection.sendKeyEvent(event) inputConnection.sendKeyEvent(event)
} }
} }
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
} else { } else {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
handler.post { handler.post {
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event -> ke?.let { event ->
val possibleNodes = possibleAccessibiltyNodes() val possibleNodes = possibleAccessibiltyNodes()
Log.d(logTag, "possibleNodes:$possibleNodes") Log.d(logTag, "possibleNodes:$possibleNodes")
for (item in possibleNodes) { for (item in possibleNodes) {
@@ -325,6 +341,43 @@ class InputService : AccessibilityService() {
} }
} }
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
when (event.keyCode) {
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
}
return true
}
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
}
return true
}
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
}
return true
}
else -> {
return false
}
}
}
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
// Perform power dialog action when action is up
if (event.action == KeyEventAndroid.ACTION_UP) {
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
}
return true
}
return false
}
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) { private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
if (node == null) { if (node == null) {
return return
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
return linkedList return linkedList
} }
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean { private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
node.refresh() node.refresh()
this.fakeEditTextForTextStateCalculation?.setSelection(0,0) this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
this.fakeEditTextForTextStateCalculation?.setText(null) this.fakeEditTextForTextStateCalculation?.setText(null)
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
it.layout(rect.left, rect.top, rect.right, rect.bottom) it.layout(rect.left, rect.top, rect.right, rect.bottom)
it.onPreDraw() it.onPreDraw()
if (event.action == android.view.KeyEvent.ACTION_DOWN) { if (event.action == KeyEventAndroid.ACTION_DOWN) {
val succ = it.onKeyDown(event.getKeyCode(), event) val succ = it.onKeyDown(event.getKeyCode(), event)
Log.d(logTag, "onKeyDown $succ") Log.d(logTag, "onKeyDown $succ")
} else if (event.action == android.view.KeyEvent.ACTION_UP) { } else if (event.action == KeyEventAndroid.ACTION_UP) {
val success = it.onKeyUp(event.getKeyCode(), event) val success = it.onKeyUp(event.getKeyCode(), event)
Log.d(logTag, "keyup $success") Log.d(logTag, "keyup $success")
} else {} } else {}

View File

@@ -37,6 +37,8 @@ object KeyEventConverter {
action = KeyEvent.ACTION_UP action = KeyEvent.ACTION_UP
} }
// FIXME: The last parameter is the repeat count, not modifiers ?
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
return KeyEvent(0, 0, action, chrValue, 0, modifiers) return KeyEvent(0, 0, action, chrValue, 0, modifiers)
} }
@@ -112,6 +114,10 @@ object KeyEventConverter {
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
ControlKey.Power -> KeyEvent.KEYCODE_POWER
else -> 0 // Default to unknown. else -> 0 // Default to unknown.
} }
} }

View File

@@ -0,0 +1,78 @@
package com.carriez.flutter_hbb
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
import android.media.AudioManager
import android.os.Build
import android.util.Log
class VolumeController(private val audioManager: AudioManager) {
private val logTag = "volume controller"
fun getVolume(streamType: Int): Double {
val current = audioManager.getStreamVolume(streamType)
val max = audioManager.getStreamMaxVolume(streamType)
return current.toDouble() / max
}
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
val max = audioManager.getStreamMaxVolume(streamType)
audioManager.setStreamVolume(
streamType,
(max * volume).toInt(),
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
}
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
if (step == null) {
audioManager.adjustStreamVolume(
streamType,
AudioManager.ADJUST_RAISE,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
val target = getVolume(streamType) + step
setVolume(target, showSystemUI, streamType)
}
}
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
if (step == null) {
audioManager.adjustStreamVolume(
streamType,
AudioManager.ADJUST_LOWER,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
val target = getVolume(streamType) - step
setVolume(target, showSystemUI, streamType)
}
}
fun getMute(streamType: Int): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.isStreamMute(streamType)
} else {
audioManager.getStreamVolume(streamType) == 0
}
}
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.adjustStreamVolume(
streamType,
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
audioManager.setStreamMute(streamType, isMuted)
}
}
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
val isMuted = getMute(streamType)
setMute(!isMuted, showSystemUI, streamType)
}
}

563
flutter/build_fdroid.sh Executable file
View File

@@ -0,0 +1,563 @@
#!/bin/bash
set -x
#
# Script to build F-Droid release of RustDesk
#
# Copyright (C) 2024, The RustDesk Authors
# 2024, Vasyl Gello <vasek.gello@gmail.com>
#
# The script is invoked by F-Droid builder system ste-by-step.
#
# It accepts the following arguments:
#
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
# - The build step to execute:
#
# + sudo-deps: as root, install needed Debian packages into builder VM
# + prebuild: patch sources and do other stuff before the build
# + build: perform actual build of APK file
#
# Parse command-line arguments
VERNAME="${1}"
VERCODE="${2}"
ANDROID_ABI="${3}"
BUILDSTEP="${4}"
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
[ -z "${BUILDSTEP}" ]; then
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
exit 1
fi
# Set various architecture-specific identifiers
case "${ANDROID_ABI}" in
arm64-v8a)
FLUTTER_TARGET=android-arm64
NDK_TARGET=aarch64-linux-android
RUST_TARGET=aarch64-linux-android
RUSTDESK_FEATURES='flutter,hwcodec'
;;
armeabi-v7a)
FLUTTER_TARGET=android-arm
NDK_TARGET=arm-linux-androideabi
RUST_TARGET=armv7-linux-androideabi
RUSTDESK_FEATURES='flutter,hwcodec'
;;
x86_64)
FLUTTER_TARGET=android-x64
NDK_TARGET=x86_64-linux-android
RUST_TARGET=x86_64-linux-android
RUSTDESK_FEATURES='flutter'
;;
x86)
FLUTTER_TARGET=android-x86
NDK_TARGET=i686-linux-android
RUST_TARGET=i686-linux-android
RUSTDESK_FEATURES='flutter'
;;
*)
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
exit 1
;;
esac
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
echo "ERROR: Can not find Android SDK!" >&2
exit 1
fi
# Export necessary variables
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
export VCPKG_ROOT="${HOME}/vcpkg"
# Now act depending on build step
# NOTE: F-Droid maintainers require explicit declaration of dependencies
# as root via `Builds.sudo` F-Droid metadata directive:
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
case "${BUILDSTEP}" in
prebuild)
# prebuild: patch sources and do other stuff before the build
#
# Extract required versions for NDK, Rust, Flutter from
# '.github/workflows/flutter-build.yml'
#
CARGO_NDK_VERSION="$(yq -r \
.env.CARGO_NDK_VERSION \
.github/workflows/flutter-build.yml)"
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_RUST_BRIDGE_VERSION \
.github/workflows/flutter-build.yml)"
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
.github/workflows/flutter-build.yml)"
RUST_VERSION="$(yq -r \
.env.RUST_VERSION \
.github/workflows/flutter-build.yml)"
VCPKG_COMMIT_ID="$(yq -r \
.env.VCPKG_COMMIT_ID \
.github/workflows/flutter-build.yml)"
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
[ -z "${VCPKG_COMMIT_ID}" ]; then
echo "ERROR: Can not identify all required versions!" >&2
exit 1
fi
# Map NDK version to revision
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
exit 1
fi
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
#
# Install the components
#
set -e
# Install Android NDK
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
sdkmanager --install "ndk;${NDK_VERSION}"
fi
# Install Flutter
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
pushd "${HOME}"
git clone https://github.com/flutter/flutter
pushd flutter
git reset --hard "${FLUTTER_VERSION}"
flutter config --no-analytics
popd # flutter
popd # ${HOME}
fi
# Install Rust
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
pushd "${HOME}"
git clone --depth 1 https://github.com/rust-lang/rustup
popd # ${HOME}
fi
pushd "${HOME}/rustup"
bash rustup-init.sh -y \
--target "${RUST_TARGET}" \
--default-toolchain "${RUST_VERSION}"
popd
if ! command -v cargo 1>/dev/null 2>&1; then
. "${HOME}/.cargo/env"
fi
# Install cargo-ndk
cargo install \
cargo-ndk \
--version "${CARGO_NDK_VERSION}"
# Install rust bridge generator
cargo install cargo-expand
cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
--features "uuid"
# Populate native vcpkg dependencies
if [ ! -d "${VCPKG_ROOT}" ]; then
pushd "${HOME}"
git clone \
https://github.com/Microsoft/vcpkg.git
git clone \
https://github.com/Microsoft/vcpkg-tool.git
pushd vcpkg-tool
mkdir build
pushd build
cmake \
-DCMAKE_BUILD_TYPE=Release \
-G 'Ninja' \
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
..
cmake --build .
popd # build
popd # vcpkg-tool
pushd vcpkg
git reset --hard "${VCPKG_COMMIT_ID}"
cp -a ../vcpkg-tool/build/vcpkg vcpkg
# disable telemetry
touch "vcpkg.disable-metrics"
popd # vcpkg
popd # ${HOME}
fi
# Install depot-tools for x86
if [ "${ANDROID_ABI}" = "x86" ]; then
if [ ! -d "${HOME}/depot_tools" ]; then
pushd "${HOME}"
git clone \
--depth 1 \
https://chromium.googlesource.com/chromium/tools/depot_tools.git
popd # ${HOME}
fi
fi
# Patch the RustDesk sources
git apply res/fdroid/patches/*.patch
sed \
-i \
-e '/gms/d' \
flutter/android/build.gradle \
flutter/android/app/build.gradle
sed \
-i \
-e '/firebase_analytics/d' \
flutter/pubspec.yaml
sed \
-i \
-e '/ firebase/,/ version/d' \
flutter/pubspec.lock
sed \
-i \
-e '/firebase/Id' \
flutter/lib/main.dart
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
# Fix for android 3.13.9
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
sed \
-i \
-e 's/uni_links_desktop/#uni_links_desktop/g' \
flutter/pubspec.yaml
set --
while read -r _1; do
set -- "$@" "${_1}"
done 0<<.a
$(find flutter/lib/ -type f -name "*dart*")
.a
sed \
-i \
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
"$@"
set --
fi
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
;;
build)
# build: perform actual build of APK file
set -e
#
# Extract required versions for NDK, Rust, Flutter from
# '.github/workflows/flutter-build.yml'
#
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
.github/workflows/flutter-build.yml)"
# Map NDK version to revision
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
exit 1
fi
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
if ! command -v cargo 1>/dev/null 2>&1; then
. "${HOME}/.cargo/env"
fi
# Download Flutter dependencies
pushd flutter
flutter packages pub get
popd # flutter
# Generate FFI bindings
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart
# Build host android deps
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
# Build rustdesk lib
cargo ndk \
--platform 21 \
--target "${RUST_TARGET}" \
--bindgen \
build \
--release \
--features "${RUSTDESK_FEATURES}"
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
# Build flutter-jit-release for x86
if [ "${ANDROID_ABI}" = "x86" ]; then
pushd flutter-sdk
echo "## Sync flutter engine sources"
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
export FAKEROOTDONTTRYCHOWN=1
fakeroot gclient sync
unset FAKEROOTDONTTRYCHOWN
pushd src
echo "## Patch away Google Play dependencies"
rm \
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
sed \
-i \
-e '/PlayStore/d' \
flutter/tools/android_lint/project.xml \
flutter/shell/platform/android/BUILD.gn
sed \
-i \
-e '/com.google.android.play/d' \
flutter/tools/androidx/files.json
echo "## Configure android engine build"
flutter/tools/gn \
--android --android-cpu x86 --runtime-mode=jit_release \
--no-goma --no-enable-unittests
echo "## Perform android engine build"
ninja -C out/android_jit_release_x86
echo "## Configure host engine build"
flutter/tools/gn \
--android-cpu x86 --runtime-mode=jit_release \
--no-goma --no-enable-unittests
echo "## Perform android engine build"
ninja -C out/host_jit_release_x86
echo "## Rename host engine"
mv out/host_jit_release_x86 out/host_jit_release
echo "## Mimic jit_release engine to debug to use with flutter build apk"
pushd out/android_jit_release_x86
sed \
-e 's/jit_release/debug/' \
flutter_embedding_jit_release.maven-metadata.xml \
1>flutter_embedding_debug.maven-metadata.xml
sed \
-e 's/jit_release/debug/' \
flutter_embedding_jit_release.pom \
1>flutter_embedding_debug.pom
sed \
-e 's/jit_release/debug/' \
x86_jit_release.maven-metadata.xml \
1>x86_debug.maven-metadata.xml
sed \
-e 's/jit_release/debug/' \
x86_jit_release.pom \
1>x86_debug.pom
cp -a \
flutter_embedding_jit_release-sources.jar \
flutter_embedding_debug-sources.jar
cp -a \
flutter_embedding_jit_release.jar \
flutter_embedding_debug.jar
cp -a \
x86_jit_release.jar \
x86_debug.jar
popd # out/android_jit_release_x86
popd # src
popd # flutter-sdk
echo "# Clean up intermediate engine files and show free space"
rm -rf \
flutter-sdk/src/out/android_jit_release_x86/obj \
flutter-sdk/src/out/host_jit_release/obj
mv flutter-sdk/src/out flutter-out
rm -rf flutter-sdk
mkdir -p flutter-sdk/src/
mv flutter-out flutter-sdk/src/out
fi
# Build the apk
pushd flutter
if [ "${ANDROID_ABI}" = "x86" ]; then
flutter build apk \
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
--local-engine=android_jit_release_x86 \
--debug \
--build-number="${VERCODE}" \
--build-name="${VERNAME}" \
--target-platform "${FLUTTER_TARGET}"
else
flutter build apk \
--release \
--build-number="${VERCODE}" \
--build-name="${VERNAME}" \
--target-platform "${FLUTTER_TARGET}"
fi
popd # flutter
rm -rf flutter-sdk
# Special step for fdroiddata CI builds to remove .gitconfig
rm -f /home/vagrant/.gitconfig
;;
*)
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
exit 1
;;
esac
# Report success
echo "All done!"

View File

@@ -31,7 +31,6 @@ import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart'; import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/input_model.dart';
import 'models/model.dart'; import 'models/model.dart';
import 'models/platform_model.dart'; import 'models/platform_model.dart';
@@ -630,10 +629,30 @@ List<Locale> supportedLocales = const [
Locale('da'), Locale('da'),
Locale('eo'), Locale('eo'),
Locale('tr'), Locale('tr'),
Locale('vi'),
Locale('pl'),
Locale('kz'), Locale('kz'),
Locale('es'), Locale('es'),
Locale('nl'),
Locale('nb'),
Locale('et'),
Locale('eu'),
Locale('bg'),
Locale('be'),
Locale('vn'),
Locale('uk'),
Locale('fa'),
Locale('ca'),
Locale('el'),
Locale('sv'),
Locale('sq'),
Locale('sr'),
Locale('th'),
Locale('sl'),
Locale('ro'),
Locale('lt'),
Locale('lv'),
Locale('ar'),
Locale('he'),
Locale('hr'),
]; ];
String formatDurationToTime(Duration duration) { String formatDurationToTime(Duration duration) {
@@ -647,8 +666,12 @@ String formatDurationToTime(Duration duration) {
closeConnection({String? id}) { closeConnection({String? id}) {
if (isAndroid || isIOS) { if (isAndroid || isIOS) {
gFFI.chatModel.hideChatOverlay(); () async {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
}();
} else { } else {
if (isWeb) { if (isWeb) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
@@ -928,13 +951,9 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
position: draggablePositions.mobileActions, position: draggablePositions.mobileActions,
width: overlayW, width: overlayW,
height: overlayH, height: overlayH,
onBackPressed: () => session.inputModel.tap(MouseButtons.right), onBackPressed: session.inputModel.onMobileBack,
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel), onHomePressed: session.inputModel.onMobileHome,
onRecentPressed: () async { onRecentPressed: session.inputModel.onMobileApps,
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
session.inputModel.sendMouse('up', MouseButtons.wheel);
},
onHidePressed: onHide, onHidePressed: onHide,
); );
} }
@@ -1058,6 +1077,49 @@ class CustomAlertDialog extends StatelessWidget {
} }
} }
Widget createDialogContent(String text) {
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
final List<TextSpan> spans = [];
int start = 0;
bool hasLink = false;
linkRegExp.allMatches(text).forEach((match) {
hasLink = true;
if (match.start > start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
spans.add(TextSpan(
text: match.group(0) ?? '',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String linkText = match.group(0) ?? '';
linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
launchUrl(Uri.parse(linkText));
},
));
start = match.end;
});
if (start < text.length) {
spans.add(TextSpan(text: text.substring(start)));
}
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
return SelectableText.rich(
TextSpan(
style: TextStyle(color: Colors.black, fontSize: 15),
children: spans,
),
);
}
void msgBox(SessionID sessionId, String type, String title, String text, void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager, String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
@@ -1066,7 +1128,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
bool hasOk = false; bool hasOk = false;
submit() { submit() {
dialogManager.dismissAll(); dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom") && desktopType != DesktopType.portForward) { if (!type.contains("custom") && desktopType != DesktopType.portForward) {
closeConnection(); closeConnection();
} }
@@ -1100,21 +1162,33 @@ void msgBox(SessionID sessionId, String type, String title, String text,
dialogManager.dismissAll(); dialogManager.dismissAll();
})); }));
} }
if (reconnect != null && if (reconnect != null && title == "Connection Error") {
title == "Connection Error" &&
reconnectTimeout != null) {
// `enabled` is used to disable the dialog button once the button is clicked. // `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs; final enabled = true.obs;
final button = Obx(() => _ReconnectCountDownButton( final button = reconnectTimeout != null
second: reconnectTimeout, ? Obx(() => _ReconnectCountDownButton(
onPressed: enabled.isTrue second: reconnectTimeout,
? () { onPressed: enabled.isTrue
// Disable the button ? () {
enabled.value = false; // Disable the button
reconnect(dialogManager, sessionId, false); enabled.value = false;
} reconnect(dialogManager, sessionId, false);
: null, }
)); : null,
))
: Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button); buttons.insert(0, button);
} }
if (link.isNotEmpty) { if (link.isNotEmpty) {
@@ -1203,7 +1277,7 @@ Widget msgboxContent(String type, String title, String text) {
translate(title), translate(title),
style: TextStyle(fontSize: 21), style: TextStyle(fontSize: 21),
).marginOnly(bottom: 10), ).marginOnly(bottom: 10),
Text(translateText(text), style: const TextStyle(fontSize: 15)), createDialogContent(translateText(text)),
], ],
), ),
), ),
@@ -1398,14 +1472,10 @@ class AndroidPermissionManager {
} }
} }
// TODO move this to mobile/widgets.
// Used only for mobile, pages remote, settings, dialog
// TODO remove argument contentPadding, its not used, getToggle() has not
RadioListTile<T> getRadio<T>( RadioListTile<T> getRadio<T>(
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange, Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
{EdgeInsetsGeometry? contentPadding, bool? dense}) { {bool? dense}) {
return RadioListTile<T>( return RadioListTile<T>(
contentPadding: contentPadding ?? EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
controlAffinity: ListTileControlAffinity.trailing, controlAffinity: ListTileControlAffinity.trailing,
title: title, title: title,
@@ -1432,7 +1502,7 @@ Future<void> initGlobalFFI() async {
_globalFFI = FFI(null); _globalFFI = FFI(null);
debugPrint("_globalFFI init end"); debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>(); // after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true); Get.put<FFI>(_globalFFI, permanent: true);
} }
String translate(String name) { String translate(String name) {
@@ -2719,20 +2789,26 @@ Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
} }
typedef WhetherUseRemoteBlock = Future<bool> Function(); typedef WhetherUseRemoteBlock = Future<bool> Function();
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) { Widget buildRemoteBlock(
var block = false.obs; {required Widget child,
required RxBool block,
required bool mask,
WhetherUseRemoteBlock? use}) {
return Obx(() => MouseRegion( return Obx(() => MouseRegion(
onEnter: (_) async { onEnter: (_) async {
await shouldBeBlocked(block, use); await shouldBeBlocked(block, use);
}, },
onExit: (event) => block.value = false, onExit: (event) => block.value = false,
child: Stack(children: [ child: Stack(children: [
child, // scope block tab
Offstage( FocusScope(child: child, canRequestFocus: !block.value),
offstage: !block.value, // mask block click, cm not block click and still use check_click_time to avoid block local click
child: Container( if (mask)
color: Colors.black.withOpacity(0.5), Offstage(
)), offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
]), ]),
)); ));
} }
@@ -2800,7 +2876,7 @@ Widget buildErrorBanner(BuildContext context,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Tooltip( child: Tooltip(
message: translate(err.value), message: translate(err.value),
child: Text( child: SelectableText(
translate(err.value), translate(err.value),
), ),
)).marginSymmetric(vertical: 2), )).marginSymmetric(vertical: 2),
@@ -2921,6 +2997,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
final displays = i == kAllDisplayValue final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index) ? List.generate(pi.displays.length, (index) => index)
: [i]; : [i];
// Try clear image model before switching from all displays
// 1. The remote side has multiple displays.
// 2. Do not use texture render.
// 3. Connect to Display 1.
// 4. Switch to multi-displays `kAllDisplayValue`
// 5. Switch to Display 2.
// Then the remote page will display last picture of Display 1 at the beginning.
if (pi.forceTextureRender && i != kAllDisplayValue) {
ffi.imageModel.clearImage();
}
bind.sessionSwitchDisplay( bind.sessionSwitchDisplay(
isDesktop: isDesktop, isDesktop: isDesktop,
sessionId: ffi.sessionId, sessionId: ffi.sessionId,
@@ -2954,11 +3040,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args)); kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
} }
setNewConnectWindowFrame( setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
int windowId, String peerId, int? display, Rect? screenRect) async { int? display, Rect? screenRect) async {
if (screenRect == null) { if (screenRect == null) {
await restoreWindowPosition(WindowType.RemoteDesktop, // Do not restore window position to new connection if there's a pre-session.
windowId: windowId, display: display, peerId: peerId); // https://github.com/rustdesk/rustdesk/discussions/8825
if (preSessionCount == 0) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId, display: display, peerId: peerId);
}
} else { } else {
await tryMoveToScreenAndSetFullscreen(screenRect); await tryMoveToScreenAndSetFullscreen(screenRect);
} }
@@ -3084,9 +3174,16 @@ Future<bool> setServerConfig(
List<RxString>? errMsgs, List<RxString>? errMsgs,
ServerConfig config, ServerConfig config,
) async { ) async {
config.idServer = config.idServer.trim(); String removeEndSlash(String input) {
config.relayServer = config.relayServer.trim(); if (input.endsWith('/')) {
config.apiServer = config.apiServer.trim(); return input.substring(0, input.length - 1);
}
return input;
}
config.idServer = removeEndSlash(config.idServer.trim());
config.relayServer = removeEndSlash(config.relayServer.trim());
config.apiServer = removeEndSlash(config.apiServer.trim());
config.key = config.key.trim(); config.key = config.key.trim();
if (controllers != null) { if (controllers != null) {
controllers[0].text = config.idServer; controllers[0].text = config.idServer;
@@ -3295,6 +3392,42 @@ bool isInHomePage() {
return controller.state.value.selected == 0; return controller.state.value.selected == 0;
} }
Widget _buildPresetPasswordWarning() {
if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
'N') {
return SizedBox.shrink();
}
return Container(
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize:
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
fontWeight: FontWeight.bold,
),
)).paddingOnly(bottom: 8),
Text(
translate("preset_password_warning"),
style: TextStyle(color: Colors.red),
)
],
).paddingAll(8),
); // Show a warning message if the Future completed with true
}
Widget buildPresetPasswordWarningMobile() {
if (bind.isPresetPasswordMobileOnly()) {
return _buildPresetPasswordWarning();
} else {
return SizedBox.shrink();
}
}
Widget buildPresetPasswordWarning() { Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: bind.isPresetPassword(), future: bind.isPresetPassword(),
@@ -3305,27 +3438,7 @@ Widget buildPresetPasswordWarning() {
return Text( return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) { } else if (snapshot.hasData && snapshot.data == true) {
return Container( return _buildPresetPasswordWarning();
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize:
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
fontWeight: FontWeight.bold,
),
)).paddingOnly(bottom: 8),
Text(
translate("preset_password_warning"),
style: TextStyle(color: Colors.red),
)
],
).paddingAll(8),
); // Show a warning message if the Future completed with true
} else { } else {
return SizedBox return SizedBox
.shrink(); // Show nothing if the Future completed with false or null .shrink(); // Show nothing if the Future completed with false or null
@@ -3379,7 +3492,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
); );
} }
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize; get windowResizeEdgeSize =>
isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable. // `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
// See _linuxWindowResizable for more details. // See _linuxWindowResizable for more details.
@@ -3397,7 +3511,12 @@ setResizable(bool resizable) {
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key); isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
final isCustomClient = bind.isCustomClient(); bool? _isCustomClient;
bool get isCustomClient {
_isCustomClient ??= bind.isCustomClient();
return _isCustomClient!;
}
get defaultOptionLang => isCustomClient ? 'default' : ''; get defaultOptionLang => isCustomClient ? 'default' : '';
get defaultOptionTheme => isCustomClient ? 'system' : ''; get defaultOptionTheme => isCustomClient ? 'system' : '';
get defaultOptionYes => isCustomClient ? 'Y' : ''; get defaultOptionYes => isCustomClient ? 'Y' : '';
@@ -3406,6 +3525,12 @@ get defaultOptionWhitelist => isCustomClient ? ',' : '';
get defaultOptionAccessMode => isCustomClient ? 'custom' : ''; get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
get defaultOptionApproveMode => isCustomClient ? 'password-click' : ''; get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
bool whitelistNotEmpty() {
// https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
final v = bind.mainGetOptionSync(key: kOptionWhitelist);
return v != '' && v != ',';
}
// `setMovable()` is only supported on macOS. // `setMovable()` is only supported on macOS.
// //
// On macOS, the window can be dragged by the tab bar by default. // On macOS, the window can be dragged by the tab bar by default.
@@ -3429,3 +3554,36 @@ disableWindowMovable(int? windowId) {
WindowController.fromWindowId(windowId).setMovable(false); WindowController.fromWindowId(windowId).setMovable(false);
} }
} }
Widget netWorkErrorWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(translate("network_error_tip")),
ElevatedButton(
onPressed: gFFI.userModel.refreshCurrentUser,
child: Text(translate("Retry")))
.marginSymmetric(vertical: 16),
SelectableText(gFFI.userModel.networkError.value,
style: TextStyle(fontSize: 11, color: Colors.red)),
],
));
}
List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
? [
ResizeEdge.topLeft,
ResizeEdge.top,
ResizeEdge.topRight,
]
: null;
List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
? [
SubWindowResizeEdge.topLeft,
SubWindowResizeEdge.top,
SubWindowResizeEdge.topRight,
]
: null;

View File

@@ -10,16 +10,16 @@ class PrivacyModeState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs; final RxString state = ''.obs;
Get.put(state, tag: key); Get.put<RxString>(state, tag: key);
} }
} }
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxString>(tag: key)) {
Get.delete(tag: key); Get.delete<RxString>(tag: key);
} else { } else {
Get.find<RxString>(tag: key).value = ''; Get.find<RxString>(tag: key).value = '';
} }
@@ -33,9 +33,9 @@ class BlockInputState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -43,8 +43,8 @@ class BlockInputState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -56,9 +56,9 @@ class CurrentDisplayState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0); final RxInt state = RxInt(0);
Get.put(state, tag: key); Get.put<RxInt>(state, tag: key);
} else { } else {
Get.find<RxInt>(tag: key).value = 0; Get.find<RxInt>(tag: key).value = 0;
} }
@@ -66,8 +66,8 @@ class CurrentDisplayState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete(tag: key); Get.delete<RxInt>(tag: key);
} }
} }
@@ -105,16 +105,16 @@ class ConnectionTypeState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<ConnectionType>(tag: key)) {
final ConnectionType collectionType = ConnectionType(); final ConnectionType collectionType = ConnectionType();
Get.put(collectionType, tag: key); Get.put<ConnectionType>(collectionType, tag: key);
} }
} }
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<ConnectionType>(tag: key)) {
Get.delete(tag: key); Get.delete<ConnectionType>(tag: key);
} }
} }
@@ -127,9 +127,9 @@ class FingerprintState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs; final RxString state = ''.obs;
Get.put(state, tag: key); Get.put<RxString>(state, tag: key);
} else { } else {
Get.find<RxString>(tag: key).value = ''; Get.find<RxString>(tag: key).value = '';
} }
@@ -137,8 +137,8 @@ class FingerprintState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxString>(tag: key)) {
Get.delete(tag: key); Get.delete<RxString>(tag: key);
} }
} }
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -160,8 +160,8 @@ class ShowRemoteCursorState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -173,9 +173,9 @@ class ShowRemoteCursorLockState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -183,8 +183,8 @@ class ShowRemoteCursorLockState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -196,10 +196,10 @@ class KeyboardEnabledState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
// Server side, default true // Server side, default true
final RxBool state = true.obs; final RxBool state = true.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = true; Get.find<RxBool>(tag: key).value = true;
} }
@@ -207,8 +207,8 @@ class KeyboardEnabledState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -220,9 +220,9 @@ class RemoteCursorMovedState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -230,8 +230,8 @@ class RemoteCursorMovedState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -243,9 +243,9 @@ class RemoteCountState {
static void init() { static void init() {
final key = tag(); final key = tag();
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = 1.obs; final RxInt state = 1.obs;
Get.put(state, tag: key); Get.put<RxInt>(state, tag: key);
} else { } else {
Get.find<RxInt>(tag: key).value = 1; Get.find<RxInt>(tag: key).value = 1;
} }
@@ -253,8 +253,8 @@ class RemoteCountState {
static void delete() { static void delete() {
final key = tag(); final key = tag();
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete(tag: key); Get.delete<RxInt>(tag: key);
} }
} }
@@ -266,9 +266,9 @@ class PeerBoolOption {
static void init(String id, String opt, bool Function() init_getter) { static void init(String id, String opt, bool Function() init_getter) {
final key = tag(id, opt); final key = tag(id, opt);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool value = RxBool(init_getter()); final RxBool value = RxBool(init_getter());
Get.put(value, tag: key); Get.put<RxBool>(value, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = init_getter(); Get.find<RxBool>(tag: key).value = init_getter();
} }
@@ -276,8 +276,8 @@ class PeerBoolOption {
static void delete(String id, String opt) { static void delete(String id, String opt) {
final key = tag(id, opt); final key = tag(id, opt);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -290,9 +290,9 @@ class PeerStringOption {
static void init(String id, String opt, String Function() init_getter) { static void init(String id, String opt, String Function() init_getter) {
final key = tag(id, opt); final key = tag(id, opt);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxString>(tag: key)) {
final RxString value = RxString(init_getter()); final RxString value = RxString(init_getter());
Get.put(value, tag: key); Get.put<RxString>(value, tag: key);
} else { } else {
Get.find<RxString>(tag: key).value = init_getter(); Get.find<RxString>(tag: key).value = init_getter();
} }
@@ -300,8 +300,8 @@ class PeerStringOption {
static void delete(String id, String opt) { static void delete(String id, String opt) {
final key = tag(id, opt); final key = tag(id, opt);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxString>(tag: key)) {
Get.delete(tag: key); Get.delete<RxString>(tag: key);
} }
} }
@@ -314,9 +314,9 @@ class UnreadChatCountState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0); final RxInt state = RxInt(0);
Get.put(state, tag: key); Get.put<RxInt>(state, tag: key);
} else { } else {
Get.find<RxInt>(tag: key).value = 0; Get.find<RxInt>(tag: key).value = 0;
} }
@@ -324,8 +324,8 @@ class UnreadChatCountState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete(tag: key); Get.delete<RxInt>(tag: key);
} }
} }

View File

@@ -35,17 +35,14 @@ class AddressBook extends StatefulWidget {
class _AddressBookState extends State<AddressBook> { class _AddressBookState extends State<AddressBook> {
var menuPos = RelativeRect.fill; var menuPos = RelativeRect.fill;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
if (!gFFI.userModel.isLogin) { if (!gFFI.userModel.isLogin) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else { } else {
return Column( return Column(
children: [ children: [
@@ -425,7 +422,8 @@ class _AddressBookState extends State<AddressBook> {
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb), if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
if (canWrite) getEntry(translate("Add Tag"), abAddTag), if (canWrite) getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(), if (gFFI.abModel.legacyMode.value)
sortMenuItem(), // It's already sorted after pulling down
if (canWrite) syncMenuItem(), if (canWrite) syncMenuItem(),
filterMenuItem(), filterMenuItem(),
if (!gFFI.abModel.legacyMode.value && canWrite) if (!gFFI.abModel.legacyMode.value && canWrite)

View File

@@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
const _kWindowsSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device); typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function( typedef AudioInputBuilder = Widget Function(
List<String> devices, String currentDevice, AudioINputSetDevice setDevice); List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
class AudioInput extends StatelessWidget { class AudioInput extends StatelessWidget {
final AudioInputBuilder builder; final AudioInputBuilder builder;
final bool isCm;
final bool isVoiceCall;
const AudioInput({Key? key, required this.builder}) : super(key: key); const AudioInput(
{Key? key,
required this.builder,
required this.isCm,
required this.isVoiceCall})
: super(key: key);
static String getDefault() { static String getDefault() {
if (isWindows) return translate('System Sound'); if (isWindows) return translate('System Sound');
return ''; return '';
} }
static Future<String> getValue() async { static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
String device = await bind.mainGetOption(key: 'audio-input'); if (isVoiceCall) {
return bind.getVoiceCallInputDevice(isCm: isCm);
} else {
return bind.mainGetOption(key: 'audio-input');
}
}
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
String device = await getAudioInput(isCm, isVoiceCall);
if (device.isNotEmpty) { if (device.isNotEmpty) {
return device; return device;
} else { } else {
@@ -25,31 +42,39 @@ class AudioInput extends StatelessWidget {
} }
} }
static Future<void> setDevice(String device) async { static Future<void> setDevice(
String device, bool isCm, bool isVoiceCall) async {
if (device == getDefault()) device = ''; if (device == getDefault()) device = '';
await bind.mainSetOption(key: 'audio-input', value: device); if (isVoiceCall) {
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
} else {
await bind.mainSetOption(key: 'audio-input', value: device);
}
} }
static Future<Map<String, Object>> getDevicesInfo() async { static Future<Map<String, Object>> getDevicesInfo(
bool isCm, bool isVoiceCall) async {
List<String> devices = (await bind.mainGetSoundInputs()).toList(); List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) { if (isWindows) {
devices.insert(0, translate('System Sound')); devices.insert(0, translate(_kWindowsSystemSound));
} }
String current = await getValue(); String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current}; return {'devices': devices, 'current': current};
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return futureBuilder( return futureBuilder(
future: getDevicesInfo(), future: getDevicesInfo(isCm, isVoiceCall),
hasData: (data) { hasData: (data) {
String currentDevice = data['current']; String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>; List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) { if (devices.isEmpty) {
return const Offstage(); return const Offstage();
} }
return builder(devices, currentDevice, setDevice); return builder(devices, currentDevice, (devices) {
setDevice(devices, isCm, isVoiceCall);
});
}, },
); );
} }

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
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/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.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:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@@ -218,50 +219,53 @@ void changeWhiteList({Function()? callback}) async {
), ),
actions: [ actions: [
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
if (!isOptFixed)dialogButton("Clear", onPressed: () async { if (!isOptFixed)
await bind.mainSetOption( dialogButton("Clear", onPressed: () async {
key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call();
close();
}, isOutline: true),
if (!isOptFixed) dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption( await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList); key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call(); callback?.call();
close(); close();
}, }, isOutline: true),
), if (!isOptFixed)
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips =
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
callback?.call();
close();
},
),
], ],
onCancel: close, onCancel: close,
); );
@@ -675,6 +679,7 @@ class PasswordWidget extends StatefulWidget {
this.reRequestFocus = false, this.reRequestFocus = false,
this.hintText, this.hintText,
this.errorText, this.errorText,
this.title,
}) : super(key: key); }) : super(key: key);
final TextEditingController controller; final TextEditingController controller;
@@ -682,6 +687,7 @@ class PasswordWidget extends StatefulWidget {
final bool reRequestFocus; final bool reRequestFocus;
final String? hintText; final String? hintText;
final String? errorText; final String? errorText;
final String? title;
@override @override
State<PasswordWidget> createState() => _PasswordWidgetState(); State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -725,7 +731,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DialogTextField( return DialogTextField(
title: translate(DialogTextField.kPasswordTitle), title: translate(widget.title ?? DialogTextField.kPasswordTitle),
hintText: translate(widget.hintText ?? 'Enter your password'), hintText: translate(widget.hintText ?? 'Enter your password'),
controller: widget.controller, controller: widget.controller,
prefixIcon: DialogTextField.kPasswordIcon, prefixIcon: DialogTextField.kPasswordIcon,
@@ -1762,9 +1768,70 @@ void renameDialog(
}); });
} }
void changeBot({Function()? callback}) async {
if (bind.mainHasValidBotSync()) {
await bind.mainSetOption(key: "bot", value: "");
callback?.call();
return;
}
String errorText = '';
bool loading = false;
final controller = TextEditingController();
gFFI.dialogManager.show((setState, close, context) {
onVerify() async {
final token = controller.text.trim();
if (token == "") return;
loading = true;
errorText = '';
setState(() {});
final error = await bind.mainVerifyBot(token: token);
if (error == "") {
callback?.call();
close();
} else {
errorText = translate(error);
loading = false;
setState(() {});
}
}
final codeField = TextField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
hintText: translate('Token'),
),
);
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(translate("enable-bot-desc"),
style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
Row(children: [Expanded(child: codeField)]),
if (errorText != '')
Text(errorText, style: TextStyle(color: Colors.red))
.marginOnly(top: 12),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
loading
? CircularProgressIndicator()
: dialogButton("OK", onPressed: onVerify),
],
onCancel: close,
);
});
}
void change2fa({Function()? callback}) async { void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) { if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: ""); await bind.mainSetOption(key: "2fa", value: "");
await bind.mainClearTrustedDevices();
callback?.call(); callback?.call();
return; return;
} }
@@ -1832,6 +1899,7 @@ void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async { SessionID sessionId, OverlayDialogManager dialogManager) async {
final controller = TextEditingController(); final controller = TextEditingController();
final RxBool submitReady = false.obs; final RxBool submitReady = false.obs;
final RxBool trustThisDevice = false.obs;
dialogManager.dismissAll(); dialogManager.dismissAll();
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
@@ -1841,7 +1909,7 @@ void enter2FaDialog(
} }
submit() { submit() {
gFFI.send2FA(sessionId, controller.text.trim()); gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
close(); close();
dialogManager.showLoading(translate('Logging in...'), dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection); onCancel: closeConnection);
@@ -1855,9 +1923,27 @@ void enter2FaDialog(
onChanged: () => submitReady.value = codeField.isReady, onChanged: () => submitReady.value = codeField.isReady,
); );
final trustField = Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate("Trust this device")),
value: trustThisDevice.value,
onChanged: (value) {
if (value == null) return;
trustThisDevice.value = value;
},
));
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('enter-2fa-title')), title: Text(translate('enter-2fa-title')),
content: codeField, content: Column(
children: [
codeField,
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
trustField,
],
),
actions: [ actions: [
dialogButton('Cancel', dialogButton('Cancel',
onPressed: cancel, onPressed: cancel,
@@ -2124,3 +2210,280 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
); );
}); });
} }
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
VoidCallback onConfirm) {
dialogManager.show((setState, close, context) {
submit() {
close();
onConfirm.call();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: Text(content,
style: const TextStyle(fontSize: 15),
textAlign: TextAlign.start),
),
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void changeUnlockPinDialog(String oldPin, Function() callback) {
final pinController = TextEditingController(text: oldPin);
final confirmController = TextEditingController(text: oldPin);
String? pinErrorText;
String? confirmationErrorText;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
pinErrorText = null;
confirmationErrorText = null;
final pin = pinController.text.trim();
final confirm = confirmController.text.trim();
if (pin != confirm) {
setState(() {
confirmationErrorText =
translate('The confirmation is not identical.');
});
return;
}
final errorMsg = bind.mainSetUnlockPin(pin: pin);
if (errorMsg != '') {
setState(() {
pinErrorText = translate(errorMsg);
});
return;
}
callback.call();
close();
}
return CustomAlertDialog(
title: Text(translate("Set PIN")),
content: Column(
children: [
DialogTextField(
title: 'PIN',
controller: pinController,
obscureText: true,
errorText: pinErrorText,
),
DialogTextField(
title: translate('Confirmation'),
controller: confirmController,
obscureText: true,
errorText: confirmationErrorText,
)
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
final controller = TextEditingController();
String? errorText;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
final pin = controller.text.trim();
if (correctPin != pin) {
setState(() {
errorText = translate('Wrong PIN');
});
return;
}
passCallback.call();
close();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: PasswordWidget(
title: 'PIN',
controller: controller,
errorText: errorText,
hintText: '',
))
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void confrimDeleteTrustedDevicesDialog(
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
() async {
if (selectedDevices.isEmpty) return;
if (selectedDevices.length == trustedDevices.length) {
await bind.mainClearTrustedDevices();
trustedDevices.clear();
selectedDevices.clear();
} else {
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
await bind.mainRemoveTrustedDevices(json: json);
trustedDevices.removeWhere((element) {
return selectedDevices.contains(element.hwid);
});
selectedDevices.clear();
}
});
}
void manageTrustedDeviceDialog() async {
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
RxList<Uint8List> selectedDevices = RxList.empty();
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("Manage trusted devices")),
content: trustedDevicesTable(trustedDevices, selectedDevices),
actions: [
Obx(() => dialogButton(translate("Delete"),
onPressed: selectedDevices.isEmpty
? null
: () {
confrimDeleteTrustedDevicesDialog(
trustedDevices,
selectedDevices,
);
},
isOutline: false)
.marginOnly(top: 12)),
dialogButton(translate("Close"), onPressed: close, isOutline: true)
.marginOnly(top: 12),
],
onCancel: close,
);
});
}
class TrustedDevice {
late final Uint8List hwid;
late final int time;
late final String id;
late final String name;
late final String platform;
TrustedDevice.fromJson(Map<String, dynamic> json) {
final hwidList = json['hwid'] as List<dynamic>;
hwid = Uint8List.fromList(hwidList.cast<int>());
time = json['time'];
id = json['id'];
name = json['name'];
platform = json['platform'];
}
String daysRemaining() {
final expiry = time + 90 * 24 * 60 * 60 * 1000;
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
if (remaining < 0) {
return '0';
}
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
}
static Future<List<TrustedDevice>> get() async {
final List<TrustedDevice> devices = List.empty(growable: true);
try {
final devicesJson = await bind.mainGetTrustedDevices();
if (devicesJson.isNotEmpty) {
final devicesList = json.decode(devicesJson);
if (devicesList is List) {
for (var device in devicesList) {
devices.add(TrustedDevice.fromJson(device));
}
}
}
} catch (e) {
print(e.toString());
}
devices.sort((a, b) => b.time.compareTo(a.time));
return devices;
}
}
Widget trustedDevicesTable(
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
RxBool selectAll = false.obs;
setSelectAll() {
if (selectedDevices.isNotEmpty &&
selectedDevices.length == devices.length) {
selectAll.value = true;
} else {
selectAll.value = false;
}
}
devices.listen((_) {
setSelectAll();
});
selectedDevices.listen((_) {
setSelectAll();
});
return FittedBox(
child: Obx(() => DataTable(
columns: [
DataColumn(
label: Checkbox(
value: selectAll.value,
onChanged: (value) {
if (value == true) {
selectedDevices.clear();
selectedDevices.addAll(devices.map((e) => e.hwid));
} else {
selectedDevices.clear();
}
},
)),
DataColumn(label: Text(translate('Platform'))),
DataColumn(label: Text(translate('ID'))),
DataColumn(label: Text(translate('Username'))),
DataColumn(label: Text(translate('Days remaining'))),
],
rows: devices.map((device) {
return DataRow(cells: [
DataCell(Checkbox(
value: selectedDevices.contains(device.hwid),
onChanged: (value) {
if (value == null) return;
if (value) {
selectedDevices.remove(device.hwid);
selectedDevices.add(device.hwid);
} else {
selectedDevices.remove(device.hwid);
}
},
)),
DataCell(Text(device.platform)),
DataCell(Text(device.id)),
DataCell(Text(device.name)),
DataCell(Text(device.daysRemaining())),
]);
}).toList(),
)),
);
}

View File

@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
String _failedMsg = ''; String _failedMsg = '';
String _url = ''; String _url = '';
@override
void initState() {
super.initState();
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();

View File

@@ -23,11 +23,6 @@ class _MyGroupState extends State<MyGroup> {
RxString get searchUserText => gFFI.groupModel.searchUserText; RxString get searchUserText => gFFI.groupModel.searchUserText;
static TextEditingController searchUserController = TextEditingController(); static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
@@ -35,6 +30,8 @@ class _MyGroupState extends State<MyGroup> {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),

View File

@@ -353,7 +353,7 @@ class Draggable extends StatefulWidget {
final Widget Function(BuildContext, GestureDragUpdateCallback) builder; final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override @override
State<StatefulWidget> createState() => _DraggableState(); State<StatefulWidget> createState() => _DraggableState(chatModel);
} }
class _DraggableState extends State<Draggable> { class _DraggableState extends State<Draggable> {
@@ -362,10 +362,8 @@ class _DraggableState extends State<Draggable> {
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@override _DraggableState(ChatModel? chatModel) {
void initState() { _chatModel = chatModel;
super.initState();
_chatModel = widget.chatModel;
} }
get position => widget.position.pos; get position => widget.position.pos;
@@ -467,7 +465,8 @@ class IOSDraggable extends StatefulWidget {
final Widget Function(BuildContext) builder; final Widget Function(BuildContext) builder;
@override @override
IOSDraggableState createState() => IOSDraggableState(); IOSDraggableState createState() =>
IOSDraggableState(chatModel, width, height);
} }
class IOSDraggableState extends State<IOSDraggable> { class IOSDraggableState extends State<IOSDraggable> {
@@ -478,15 +477,13 @@ class IOSDraggableState extends State<IOSDraggable> {
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@override IOSDraggableState(ChatModel? chatModel, double w, double h) {
void initState() { _chatModel = chatModel;
super.initState(); _width = w;
_chatModel = widget.chatModel; _height = h;
_width = widget.width;
_height = widget.height;
} }
get position => widget.position; DraggableKeyPosition get position => widget.position;
checkKeyboard() { checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
@@ -494,13 +491,13 @@ class IOSDraggableState extends State<IOSDraggable> {
// save // save
if (!_keyboardVisible && currentVisible) { if (!_keyboardVisible && currentVisible) {
_saveHeight = position.value.dy; _saveHeight = position.pos.dy;
} }
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
position.value = Offset(position.value.dx, _saveHeight); position.update(Offset(position.pos.dx, _saveHeight));
}); });
} }
@@ -508,10 +505,10 @@ class IOSDraggableState extends State<IOSDraggable> {
if (_keyboardVisible && currentVisible) { if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height; final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height; final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + position.value.dy > contextHeight) { if (sumHeight + position.pos.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
position.value = Offset(position.value.dx, y); position.update(Offset(position.pos.dx, y));
}); });
} }
} }
@@ -526,14 +523,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
left: position.value.dx, left: position.pos.dx,
top: position.value.dy, top: position.pos.dy,
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
setState(() { setState(() {
position.value += details.delta; position.update(position.pos + details.delta);
}); });
_chatModel?.setChatWindowPosition(position.value); _chatModel?.setChatWindowPosition(position.pos);
}, },
child: Material( child: Material(
child: Container( child: Container(

View File

@@ -22,6 +22,8 @@ enum PeerUiType { grid, tile, list }
final peerCardUiType = PeerUiType.grid.obs; final peerCardUiType = PeerUiType.grid.obs;
bool? hideUsernameOnCard;
class _PeerCard extends StatefulWidget { class _PeerCard extends StatefulWidget {
final Peer peer; final Peer peer;
final PeerTabIndex tab; final PeerTabIndex tab;
@@ -130,8 +132,11 @@ 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 = hideUsernameOnCard ??=
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? 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));
@@ -239,8 +244,11 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerCard( Widget _buildPeerCard(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final name = hideUsernameOnCard ??=
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final child = Card( final child = Card(
color: Colors.transparent, color: Colors.transparent,
elevation: 0, elevation: 0,
@@ -628,8 +636,8 @@ abstract class BasePeerCard extends StatelessWidget {
@protected @protected
Future<bool> _isForceAlwaysRelay(String id) async { Future<bool> _isForceAlwaysRelay(String id) async {
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)) return option2bool(kOptionForceAlwaysRelay,
.isNotEmpty; (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
} }
@protected @protected

View File

@@ -76,8 +76,11 @@ class _PeerTabPageState extends State<PeerTabPage>
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible); final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
@override _PeerTabPageState() {
void initState() { _loadLocalOptions();
}
void _loadLocalOptions() {
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType); final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
if (uiType != '') { if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == 0 peerCardUiType.value = int.parse(uiType) == 0
@@ -87,8 +90,7 @@ class _PeerTabPageState extends State<PeerTabPage>
: PeerUiType.list; : PeerUiType.list;
} }
hideAbTagsPanel.value = hideAbTagsPanel.value =
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty; bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
super.initState();
} }
Future<void> handleTabSelection(int tabIndex) async { Future<void> handleTabSelection(int tabIndex) async {
@@ -399,9 +401,9 @@ class _PeerTabPageState extends State<PeerTabPage>
final peers = model.selectedPeers; final peers = model.selectedPeers;
switch (model.currentTab) { switch (model.currentTab) {
case 0: case 0:
peers.map((p) async { for (var p in peers) {
await bind.mainRemovePeer(id: p.id); await bind.mainRemovePeer(id: p.id);
}).toList(); }
await bind.mainLoadRecentPeers(); await bind.mainLoadRecentPeers();
break; break;
case 1: case 1:
@@ -413,9 +415,9 @@ class _PeerTabPageState extends State<PeerTabPage>
await bind.mainLoadFavPeers(); await bind.mainLoadFavPeers();
break; break;
case 2: case 2:
peers.map((p) async { for (var p in peers) {
await bind.mainRemoveDiscovered(id: p.id); await bind.mainRemoveDiscovered(id: p.id);
}).toList(); }
await bind.mainLoadLanPeers(); await bind.mainLoadLanPeers();
break; break;
case 3: case 3:
@@ -872,16 +874,18 @@ class PeerSortDropdown extends StatefulWidget {
} }
class _PeerSortDropdownState extends State<PeerSortDropdown> { class _PeerSortDropdownState extends State<PeerSortDropdown> {
@override _PeerSortDropdownState() {
void initState() {
if (!PeerSortType.values.contains(peerSort.value)) { if (!PeerSortType.values.contains(peerSort.value)) {
peerSort.value = PeerSortType.remoteId; _loadLocalOptions();
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
} }
super.initState(); }
void _loadLocalOptions() {
peerSort.value = PeerSortType.remoteId;
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
} }
@override @override

View File

@@ -45,10 +45,14 @@ class LoadEvent {
final peerSearchText = "".obs; final peerSearchText = "".obs;
/// for peer sort, global obs value /// for peer sort, global obs value
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs; RxString? _peerSort;
RxString get peerSort {
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
return _peerSort!;
}
// list for listener // list for listener
final obslist = [peerSearchText, peerSort].obs; RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
final peerSearchTextController = final peerSearchTextController =
TextEditingController(text: peerSearchText.value); TextEditingController(text: peerSearchText.value);
@@ -70,7 +74,8 @@ class _PeersView extends StatefulWidget {
} }
/// State for the peer widget. /// State for the peer widget.
class _PeersViewState extends State<_PeersView> with WindowListener { class _PeersViewState extends State<_PeersView>
with WindowListener, WidgetsBindingObserver {
static const int _maxQueryCount = 3; static const int _maxQueryCount = 3;
final HashMap<String, String> _emptyMessages = HashMap.from({ final HashMap<String, String> _emptyMessages = HashMap.from({
LoadEvent.recent: 'empty_recent_tip', LoadEvent.recent: 'empty_recent_tip',
@@ -82,9 +87,10 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
final _curPeers = <String>{}; final _curPeers = <String>{};
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30)); var _lastQueryTime = DateTime.now();
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
bool _isActive = true;
final _scrollController = ScrollController(); final _scrollController = ScrollController();
@@ -95,12 +101,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
windowManager.removeListener(this); windowManager.removeListener(this);
WidgetsBinding.instance.removeObserver(this);
_exit = true; _exit = true;
super.dispose(); super.dispose();
} }
@@ -115,6 +123,20 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryCount = _maxQueryCount; _queryCount = _maxQueryCount;
} }
// This function is required for mobile.
// `onWindowFocus` works fine for desktop.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (isDesktop) return;
if (state == AppLifecycleState.resumed) {
_isActive = true;
_queryCount = 0;
} else if (state == AppLifecycleState.inactive) {
_isActive = false;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<Peers>( return ChangeNotifierProvider<Peers>(
@@ -253,10 +275,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return body; return body;
} }
final _queryInterval = const Duration(seconds: 20); var _queryInterval = const Duration(seconds: 20);
void _startCheckOnlines() { void _startCheckOnlines() {
() async { () async {
final p = await bind.mainIsUsingPublicServer();
if (!p) {
_queryInterval = const Duration(seconds: 6);
}
while (!_exit) { while (!_exit) {
final now = DateTime.now(); final now = DateTime.now();
if (!setEquals(_curPeers, _lastQueryPeers)) { if (!setEquals(_curPeers, _lastQueryPeers)) {
@@ -264,7 +290,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(false); _queryOnlines(false);
} }
} else { } else {
if (_queryCount < _maxQueryCount) { if (_isActive && (_queryCount < _maxQueryCount || !p)) {
if (now.difference(_lastQueryTime) >= _queryInterval) { if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
@@ -282,14 +308,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(bool isLoadEvent) { _queryOnlines(bool isLoadEvent) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
_queryCount = 0; _queryCount = 0;
} }
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
} }
Future<List<Peer>>? matchPeers( Future<List<Peer>>? matchPeers(

View File

@@ -6,6 +6,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.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/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/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';
@@ -22,6 +23,20 @@ class TTextMenu {
required this.onPressed, required this.onPressed,
this.trailingIcon, this.trailingIcon,
this.divider = false}); this.divider = false});
Widget getChild() {
if (trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
child,
trailingIcon!,
],
);
} else {
return child;
}
}
} }
class TRadioMenu<T> { class TRadioMenu<T> {
@@ -115,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// paste // paste
if (isMobile && if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
pi.platform != kPeerPlatformAndroid &&
perms['keyboard'] != false &&
perms['clipboard'] != false) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Paste')), child: Text(translate('Send clipboard keystrokes')),
onPressed: () async { onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) { if (data != null && data.text != null) {
@@ -636,6 +648,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
v.addAll(toolbarKeyboardToggles(ffi)); v.addAll(toolbarKeyboardToggles(ffi));
} }
// view mode (mobile only, desktop is in keyboard menu)
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu(
value: ffiModel.viewOnly,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value);
},
child: Text(translate('View Mode'))));
}
return v; return v;
} }
@@ -776,3 +800,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
} }
return v; return v;
} }
bool showVirtualDisplayMenu(FFI ffi) {
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return false;
}
if (!ffi.ffiModel.pi.isInstalled) {
return false;
}
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
return true;
}
return false;
}
List<Widget> getVirtualDisplayMenuChildren(
FFI ffi, String id, VoidCallback? clickCallBack) {
if (!showVirtualDisplayMenu(ffi)) {
return [];
}
final pi = ffi.ffiModel.pi;
final privacyModeState = PrivacyModeState.find(id);
if (pi.isRustDeskIdd) {
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: i + 1, on: value);
clickCallBack?.call();
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)));
return children;
}
if (pi.isAmyuniIdd) {
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: false);
clickCallBack?.call();
},
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: true);
clickCallBack?.call();
},
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)),
];
return children;
}
return [];
}

View File

@@ -135,6 +135,18 @@ const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper"; const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service"; const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture"; const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
// buildin opitons
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
const String kOptionRemovePresetPasswordWarning =
"remove-preset-password-warning";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only"; const String kOptionToggleViewOnly = "view-only";
@@ -155,6 +167,8 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch"; const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse"; const String kPointerEventKindMouse = "mouse";
const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows = const String kKeyShowDisplaysAsIndividualWindows =
'displays_as_individual_windows'; 'displays_as_individual_windows';
const String kKeyUseAllMyDisplaysForTheRemoteSession = const String kKeyUseAllMyDisplaysForTheRemoteSession =
@@ -227,9 +241,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0; const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0;
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead. // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
final kWindowEdgeSize = isWindows ? 1.0 : 5.0; const kWindowResizeEdgeSize = 5.0;
final kWindowBorderWidth = 1.0; const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0; const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0; const kFrameClipRRectBorderRadius = 12.0;

View File

@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
final status = final status =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>; jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
final statusNum = status['status_num'] as int; final statusNum = status['status_num'] as int;
final preStatus = stateGlobal.svcStatus.value;
if (statusNum == 0) { if (statusNum == 0) {
stateGlobal.svcStatus.value = SvcStatus.connecting; stateGlobal.svcStatus.value = SvcStatus.connecting;
} else if (statusNum == -1) { } else if (statusNum == -1) {
stateGlobal.svcStatus.value = SvcStatus.notReady; stateGlobal.svcStatus.value = SvcStatus.notReady;
} else if (statusNum == 1) { } else if (statusNum == 1) {
stateGlobal.svcStatus.value = SvcStatus.ready; stateGlobal.svcStatus.value = SvcStatus.ready;
if (preStatus != SvcStatus.ready) {
gFFI.userModel.refreshCurrentUser();
}
} else { } else {
stateGlobal.svcStatus.value = SvcStatus.notReady; stateGlobal.svcStatus.value = SvcStatus.notReady;
} }
@@ -212,14 +208,14 @@ class _ConnectionPageState extends State<ConnectionPage>
void initState() { void initState() {
super.initState(); super.initState();
if (_idController.text.isEmpty) { if (_idController.text.isEmpty) {
() async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId(); final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) { if (lastRemoteId != _idController.id) {
setState(() { setState(() {
_idController.id = lastRemoteId; _idController.id = lastRemoteId;
}); });
} }
}(); });
} }
Get.put<IDTextEditingController>(_idController); Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this); windowManager.addListener(this);
@@ -261,8 +257,9 @@ 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 = stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize; ? kMaximizeEdgeSize
: windowResizeEdgeSize;
} }
@override @override
@@ -340,7 +337,7 @@ class _ConnectionPageState extends State<ConnectionPage>
?.merge(TextStyle(height: 1)), ?.merge(TextStyle(height: 1)),
).marginOnly(right: 4), ).marginOnly(right: 4),
Tooltip( Tooltip(
waitDuration: Duration(milliseconds: 0), waitDuration: Duration(milliseconds: 300),
message: translate("id_input_tip"), message: translate("id_input_tip"),
child: Icon( child: Icon(
Icons.help_outline_outlined, Icons.help_outline_outlined,

View File

@@ -443,14 +443,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}); });
} }
} else if (isMacOS) { } else if (isMacOS) {
if (!(bind.isOutgoingOnly() || final isOutgoingOnly = bind.isOutgoingOnly();
bind.mainIsCanScreenRecording(prompt: false))) { if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
return buildInstallCard("Permissions", "config_screen", "Configure", return buildInstallCard("Permissions", "config_screen", "Configure",
() async { () async {
bind.mainIsCanScreenRecording(prompt: true); bind.mainIsCanScreenRecording(prompt: true);
watchIsCanScreenRecording = true; watchIsCanScreenRecording = true;
}, help: 'Help', link: translate("doc_mac_permission")); }, help: 'Help', link: translate("doc_mac_permission"));
} else if (!bind.mainIsProcessTrusted(prompt: false)) { } else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
return buildInstallCard("Permissions", "config_acc", "Configure", return buildInstallCard("Permissions", "config_acc", "Configure",
() async { () async {
bind.mainIsProcessTrusted(prompt: true); bind.mainIsProcessTrusted(prompt: true);
@@ -462,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsCanInputMonitoring(prompt: true); bind.mainIsCanInputMonitoring(prompt: true);
watchIsInputMonitoring = true; watchIsInputMonitoring = true;
}, help: 'Help', link: translate("doc_mac_permission")); }, help: 'Help', link: translate("doc_mac_permission"));
} else if (!svcStopped.value && } else if (!isOutgoingOnly &&
!svcStopped.value &&
bind.mainIsInstalled() && bind.mainIsInstalled() &&
!bind.mainIsInstalledDaemon(prompt: false)) { !bind.mainIsInstalledDaemon(prompt: false)) {
return buildInstallCard("", "install_daemon_tip", "Install", () async { return buildInstallCard("", "install_daemon_tip", "Install", () async {
@@ -545,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
String? link, String? link,
bool? closeButton, bool? closeButton,
String? closeOption}) { String? closeOption}) {
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
content != 'install_daemon_tip') {
return const SizedBox();
}
void closeCard() async { void closeCard() async {
if (closeOption != null) { if (closeOption != null) {
await bind.mainSetLocalOption(key: closeOption, value: 'N'); await bind.mainSetLocalOption(key: closeOption, value: 'N');
@@ -838,7 +843,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
} }
void setPasswordDialog() async { void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
final pw = await bind.mainGetPermanentPassword(); final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw); final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw);
@@ -878,6 +883,9 @@ void setPasswordDialog() async {
return; return;
} }
bind.mainSetPermanentPassword(password: pass); bind.mainSetPermanentPassword(password: pass);
if (pass.isNotEmpty) {
notEmptyCallback?.call();
}
close(); close();
} }

View File

@@ -61,9 +61,13 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey; final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [ static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general, SettingsTabKey.general,
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) if (!bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety, SettingsTabKey.safety,
if (!bind.isDisableSettings()) SettingsTabKey.network, if (!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display, if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin, SettingsTabKey.plugin,
@@ -74,7 +78,8 @@ class DesktopSettingPage extends StatefulWidget {
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key); DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
@override @override
State<DesktopSettingPage> createState() => _DesktopSettingPageState(); State<DesktopSettingPage> createState() =>
_DesktopSettingPageState(initialTabkey);
static void switch2page(SettingsTabKey page) { static void switch2page(SettingsTabKey page) {
try { try {
@@ -84,8 +89,10 @@ class DesktopSettingPage extends StatefulWidget {
} }
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) { if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page); DesktopTabPage.onAddSetting(initialPage: page);
PageController controller = Get.find(tag: _kSettingPageControllerTag); PageController controller =
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag); Get.find<PageController>(tag: _kSettingPageControllerTag);
Rx<SettingsTabKey> selected =
Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
selected.value = page; selected.value = page;
controller.jumpToPage(index); controller.jumpToPage(index);
} else { } else {
@@ -105,10 +112,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override _DesktopSettingPageState(SettingsTabKey initialTabkey) {
void initState() { var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
super.initState();
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
if (initialIndex == -1) { if (initialIndex == -1) {
initialIndex = 0; initialIndex = 0;
} }
@@ -171,16 +176,32 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
} }
List<Widget> _children() { List<Widget> _children() {
final children = [ final children = List<Widget>.empty(growable: true);
_General(), for (final tab in DesktopSettingPage.tabKeys) {
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(), switch (tab) {
if (!bind.isDisableSettings()) _Network(), case SettingsTabKey.general:
if (!bind.isIncomingOnly()) _Display(), children.add(const _General());
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) break;
_Plugin(), case SettingsTabKey.safety:
if (!bind.isDisableAccount()) _Account(), children.add(const _Safety());
_About(), break;
]; case SettingsTabKey.network:
children.add(const _Network());
break;
case SettingsTabKey.display:
children.add(const _Display());
break;
case SettingsTabKey.plugin:
children.add(const _Plugin());
break;
case SettingsTabKey.account:
children.add(const _Account());
break;
case SettingsTabKey.about:
children.add(const _About());
break;
}
}
return children; return children;
} }
@@ -493,18 +514,20 @@ class _GeneralState extends State<_General> {
return const Offstage(); return const Offstage();
} }
return AudioInput(builder: (devices, currentDevice, setDevice) { builder(devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [ final child = ComboBox(
...devices.map((device) => _Radio<String>(context, keys: devices,
value: device, values: devices,
groupValue: currentDevice, initialKey: currentDevice,
autoNewLine: false, onChanged: (key) async {
label: device, onChanged: (value) { setDevice(key);
setDevice(value); setState(() {});
setState(() {}); },
})) ).marginOnly(left: _kContentHMargin);
]); return _Card(title: 'Audio Input Device', children: [child]);
}); }
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
} }
Widget record(BuildContext context) { Widget record(BuildContext context) {
@@ -679,15 +702,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
// Simple temp wrapper for PR check // Simple temp wrapper for PR check
tmpWrapper() { tmpWrapper() {
RxBool has2fa = bind.mainHasValid2FaSync().obs; RxBool has2fa = bind.mainHasValid2FaSync().obs;
RxBool hasBot = bind.mainHasValidBotSync().obs;
update() async { update() async {
has2fa.value = bind.mainHasValid2FaSync(); has2fa.value = bind.mainHasValid2FaSync();
setState(() {});
} }
onChanged(bool? checked) async { onChanged(bool? checked) async {
change2fa(callback: update); if (checked == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
change2fa(callback: update);
});
} else {
change2fa(callback: update);
}
} }
return GestureDetector( final tfa = GestureDetector(
child: InkWell( child: InkWell(
child: Obx(() => Row( child: Obx(() => Row(
children: [ children: [
@@ -708,6 +740,77 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
onChanged(!has2fa.value); onChanged(!has2fa.value);
}, },
).marginOnly(left: _kCheckBoxLeftMargin); ).marginOnly(left: _kCheckBoxLeftMargin);
if (!has2fa.value) {
return tfa;
}
updateBot() async {
hasBot.value = bind.mainHasValidBotSync();
setState(() {});
}
onChangedBot(bool? checked) async {
if (checked == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
changeBot(callback: updateBot);
});
} else {
changeBot(callback: updateBot);
}
}
final bot = GestureDetector(
child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("enable-bot-tip"),
child: InkWell(
child: Obx(() => Row(
children: [
Checkbox(
value: hasBot.value,
onChanged: enabled ? onChangedBot : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Telegram bot'),
style: TextStyle(
color: disabledTextColor(context, enabled)),
))
],
))),
),
onTap: () {
onChangedBot(!hasBot.value);
},
).marginOnly(left: _kCheckBoxLeftMargin + 30);
final trust = Row(
children: [
Flexible(
child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("enable-trusted-devices-tip"),
child: _OptionCheckBox(context, "Enable trusted devices",
kOptionEnableTrustedDevices,
enabled: !locked, update: (v) {
setState(() {});
}),
),
),
if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
ElevatedButton(
onPressed: locked
? null
: () {
manageTrustedDeviceDialog();
},
child: Text(translate('Manage trusted devices')))
],
).marginOnly(left: 30);
return Column(
children: [tfa, bot, trust],
);
} }
return tmpWrapper(); return tmpWrapper();
@@ -832,12 +935,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
label: value, label: value,
onChanged: locked onChanged: locked
? null ? null
: ((value) { : ((value) async {
() async { callback() async {
await model.setVerificationMethod( await model.setVerificationMethod(
passwordKeys[passwordValues.indexOf(value)]); passwordKeys[passwordValues.indexOf(value)]);
await model.updatePasswordModel(); await model.updatePasswordModel();
}(); }
if (value ==
passwordValues[passwordKeys
.indexOf(kUsePermanentPassword)] &&
(await bind.mainGetPermanentPassword())
.isEmpty) {
setPasswordDialog(notEmptyCallback: callback);
} else {
await callback();
}
}), }),
)) ))
.toList(); .toList();
@@ -930,6 +1043,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(context, 'allow-only-conn-window-open-tip', _OptionCheckBox(context, 'allow-only-conn-window-open-tip',
'allow-only-conn-window-open', 'allow-only-conn-window-open',
reverse: false, enabled: enabled), reverse: false, enabled: enabled),
if (bind.mainIsInstalled()) unlockPin()
]); ]);
} }
@@ -1030,12 +1144,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
bool enabled = !locked; bool enabled = !locked;
// Simple temp wrapper for PR check // Simple temp wrapper for PR check
tmpWrapper() { tmpWrapper() {
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) != RxBool hasWhitelist = whitelistNotEmpty().obs;
defaultOptionWhitelist)
.obs;
update() async { update() async {
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) != hasWhitelist.value = whitelistNotEmpty();
defaultOptionWhitelist;
} }
onChanged(bool? checked) async { onChanged(bool? checked) async {
@@ -1146,7 +1257,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
width: 95, width: 95,
child: TextField( child: TextField(
controller: controller, controller: controller,
enabled: enabled && !locked && isOptFixed, enabled: enabled && !locked && !isOptFixed,
onChanged: (_) => applyEnabled.value = true, onChanged: (_) => applyEnabled.value = true,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow(RegExp( FilteringTextInputFormatter.allow(RegExp(
@@ -1180,6 +1291,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
}(), }(),
]; ];
} }
Widget unlockPin() {
bool enabled = !locked;
RxString unlockPin = bind.mainGetUnlockPin().obs;
update() async {
unlockPin.value = bind.mainGetUnlockPin();
}
onChanged(bool? checked) async {
changeUnlockPinDialog(unlockPin.value, update);
}
final isOptFixed = isOptionFixed(kOptionWhitelist);
return GestureDetector(
child: Obx(() => Row(
children: [
Checkbox(
value: unlockPin.isNotEmpty,
onChanged: enabled && !isOptFixed ? onChanged : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Unlock with PIN'),
style: TextStyle(color: disabledTextColor(context, enabled)),
))
],
)),
onTap: enabled
? () {
onChanged(!unlockPin.isNotEmpty);
}
: null,
).marginOnly(left: _kCheckBoxLeftMargin);
}
} }
class _Network extends StatefulWidget { class _Network extends StatefulWidget {
@@ -1199,6 +1344,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
super.build(context); super.build(context);
bool enabled = !locked; bool enabled = !locked;
final scrollController = ScrollController(); final scrollController = ScrollController();
final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper( return DesktopScrollWrapper(
scrollController: scrollController, scrollController: scrollController,
child: ListView( child: ListView(
@@ -1212,11 +1361,12 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
AbsorbPointer( AbsorbPointer(
absorbing: locked, absorbing: locked,
child: Column(children: [ child: Column(children: [
server(enabled), if (!hideServer) server(enabled),
_Card(title: 'Proxy', children: [ if (!hideProxy)
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy, _Card(title: 'Proxy', children: [
enabled: enabled), _Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
]), enabled: enabled),
]),
]), ]),
), ),
]).marginOnly(bottom: _kListViewBottomMargin)); ]).marginOnly(bottom: _kListViewBottomMargin));
@@ -1715,7 +1865,7 @@ class _AboutState extends State<_About> {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: scrollController, controller: scrollController,
physics: DraggableNeverScrollableScrollPhysics(), physics: DraggableNeverScrollableScrollPhysics(),
child: _Card(title: '${translate('About')} RustDesk', children: [ child: _Card(title: translate('About RustDesk'), children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -2070,9 +2220,14 @@ Widget _lock(
Text(translate(label)).marginOnly(left: 5), Text(translate(label)).marginOnly(left: 5),
]).marginSymmetric(vertical: 2)), ]).marginSymmetric(vertical: 2)),
onPressed: () async { onPressed: () async {
bool checked = await callMainCheckSuperUserPermission(); final unlockPin = bind.mainGetUnlockPin();
if (checked) { if (unlockPin.isEmpty) {
onUnlock(); bool checked = await callMainCheckSuperUserPermission();
if (checked) {
onUnlock();
}
} else {
checkUnlockPinDialog(unlockPin, onUnlock);
} }
}, },
).marginSymmetric(horizontal: 2, vertical: 4), ).marginSymmetric(horizontal: 2, vertical: 4),
@@ -2249,35 +2404,40 @@ void changeSocks5Proxy() async {
children: [ children: [
Row( Row(
children: [ children: [
ConstrainedBox( if (!isMobile)
constraints: const BoxConstraints(minWidth: 140), ConstrainedBox(
child: Align( constraints: const BoxConstraints(minWidth: 140),
alignment: Alignment.centerRight, child: Align(
child: Row( alignment: Alignment.centerRight,
children: [ child: Row(
Text( children: [
translate('Server'), Text(
).marginOnly(right: 4), translate('Server'),
Tooltip( ).marginOnly(right: 4),
waitDuration: Duration(milliseconds: 0), Tooltip(
message: translate("default_proxy_tip"), waitDuration: Duration(milliseconds: 0),
child: Icon( message: translate("default_proxy_tip"),
Icons.help_outline_outlined, child: Icon(
size: 16, Icons.help_outline_outlined,
color: Theme.of(context) size: 16,
.textTheme color: Theme.of(context)
.titleLarge .textTheme
?.color .titleLarge
?.withOpacity(0.5), ?.color
?.withOpacity(0.5),
),
), ),
), ],
], )).marginOnly(right: 10),
)).marginOnly(right: 10), ),
),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
errorText: proxyMsg.isNotEmpty ? proxyMsg : null, errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
labelText: isMobile ? translate('Server') : null,
helperText:
isMobile ? translate("default_proxy_tip") : null,
helperMaxLines: isMobile ? 3 : null,
), ),
controller: proxyController, controller: proxyController,
autofocus: true, autofocus: true,
@@ -2288,15 +2448,19 @@ void changeSocks5Proxy() async {
).marginOnly(bottom: 8), ).marginOnly(bottom: 8),
Row( Row(
children: [ children: [
ConstrainedBox( if (!isMobile)
constraints: const BoxConstraints(minWidth: 140), ConstrainedBox(
child: Text( constraints: const BoxConstraints(minWidth: 140),
'${translate("Username")}:', child: Text(
textAlign: TextAlign.right, '${translate("Username")}:',
).marginOnly(right: 10)), textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded( Expanded(
child: TextField( child: TextField(
controller: userController, controller: userController,
decoration: InputDecoration(
labelText: isMobile ? translate('Username') : null,
),
enabled: !isOptFixed, enabled: !isOptFixed,
), ),
), ),
@@ -2304,16 +2468,18 @@ void changeSocks5Proxy() async {
).marginOnly(bottom: 8), ).marginOnly(bottom: 8),
Row( Row(
children: [ children: [
ConstrainedBox( if (!isMobile)
constraints: const BoxConstraints(minWidth: 140), ConstrainedBox(
child: Text( constraints: const BoxConstraints(minWidth: 140),
'${translate("Password")}:', child: Text(
textAlign: TextAlign.right, '${translate("Password")}:',
).marginOnly(right: 10)), textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded( Expanded(
child: Obx(() => TextField( child: Obx(() => TextField(
obscureText: obscure.value, obscureText: obscure.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: isMobile ? translate('Password') : null,
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () => obscure.value = !obscure.value, onPressed: () => obscure.value = !obscure.value,
icon: Icon(obscure.value icon: Icon(obscure.value

View File

@@ -8,6 +8,7 @@ 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.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
// import 'package:flutter/services.dart';
import '../../common/shared_state.dart'; import '../../common/shared_state.dart';
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
static void onAddSetting( static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) { {SettingsTabKey initialPage = SettingsTabKey.general}) {
try { try {
DesktopTabController tabController = Get.find(); DesktopTabController tabController = Get.find<DesktopTabController>();
tabController.add(TabInfo( tabController.add(TabInfo(
key: kTabLabelSettingPage, key: kTabLabelSettingPage,
label: kTabLabelSettingPage, label: kTabLabelSettingPage,
@@ -41,21 +42,11 @@ class _DesktopTabPageState extends State<DesktopTabPage>
final tabController = DesktopTabController(tabType: DesktopTabType.main); final tabController = DesktopTabController(tabType: DesktopTabType.main);
final RxBool _block = false.obs; final RxBool _block = false.obs;
// bool mouseIn = false;
@override _DesktopTabPageState() {
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
Get.put<DesktopTabController>(tabController);
RemoteCountState.init(); RemoteCountState.init();
Get.put<DesktopTabController>(tabController);
tabController.add(TabInfo( tabController.add(TabInfo(
key: kTabLabelHomePage, key: kTabLabelHomePage,
label: kTabLabelHomePage, label: kTabLabelHomePage,
@@ -78,8 +69,34 @@ class _DesktopTabPageState extends State<DesktopTabPage>
} }
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
WidgetsBinding.instance.addObserver(this);
}
/*
bool _handleKeyEvent(KeyEvent event) {
if (!mouseIn && event is KeyDownEvent) {
print('key down: ${event.logicalKey}');
shouldBeBlocked(_block, canBeBlocked);
}
return false; // allow it to propagate
}
*/
@override @override
void dispose() { void dispose() {
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>(); Get.delete<DesktopTabController>();
@@ -102,18 +119,15 @@ class _DesktopTabPageState extends State<DesktopTabPage>
isClose: false, isClose: false,
), ),
), ),
blockTab: _block,
))); )));
widget() => MouseRegion(
onEnter: (_) async {
await shouldBeBlocked(_block, canBeBlocked);
},
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
return isMacOS || kUseCompatibleUiMode return isMacOS || kUseCompatibleUiMode
? Obx(() => widget()) ? tabWidget
: Obx( : Obx(
() => DragToResizeArea( () => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
child: widget(), enableResizeEdges: windowManagerEnableResizeEdges,
child: tabWidget,
), ),
); );
} }

View File

@@ -92,13 +92,16 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.dialogManager _ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection); .showLoading(translate('Connecting...'), onCancel: closeConnection);
}); });
Get.put(_ffi, tag: 'ft_${widget.id}'); Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
if (!isLinux) { if (!isLinux) {
WakelockPlus.enable(); WakelockPlus.enable();
} }
debugPrint("File manager page init success with id ${widget.id}"); debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState); _ffi.dialogManager.setOverlayState(_overlayKeyState);
widget.tabController.onSelected?.call(widget.id); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
});
} }
@override @override
@@ -259,6 +262,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Offstage( Offstage(
offstage: item.state != JobState.paused, offstage: item.state != JobState.paused,
child: MenuButton( child: MenuButton(
tooltip: translate("Resume"),
onPressed: () { onPressed: () {
jobController.resumeJob(item.id); jobController.resumeJob(item.id);
}, },
@@ -271,6 +275,7 @@ class _FileManagerPageState extends State<FileManagerPage>
), ),
), ),
MenuButton( MenuButton(
tooltip: translate("Delete"),
padding: EdgeInsets.only(right: 15), padding: EdgeInsets.only(right: 15),
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/close.svg", "assets/close.svg",
@@ -518,6 +523,7 @@ class _FileManagerViewState extends State<FileManagerView> {
Row( Row(
children: [ children: [
MenuButton( MenuButton(
tooltip: translate('Back'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 3, right: 3,
), ),
@@ -537,6 +543,7 @@ class _FileManagerViewState extends State<FileManagerView> {
}, },
), ),
MenuButton( MenuButton(
tooltip: translate('Parent directory'),
child: RotatedBox( child: RotatedBox(
quarterTurns: 3, quarterTurns: 3,
child: SvgPicture.asset( child: SvgPicture.asset(
@@ -601,6 +608,7 @@ class _FileManagerViewState extends State<FileManagerView> {
switch (_locationStatus.value) { switch (_locationStatus.value) {
case LocationStatus.bread: case LocationStatus.bread:
return MenuButton( return MenuButton(
tooltip: translate('Search'),
onPressed: () { onPressed: () {
_locationStatus.value = LocationStatus.fileSearchBar; _locationStatus.value = LocationStatus.fileSearchBar;
Future.delayed( Future.delayed(
@@ -627,6 +635,7 @@ class _FileManagerViewState extends State<FileManagerView> {
); );
case LocationStatus.fileSearchBar: case LocationStatus.fileSearchBar:
return MenuButton( return MenuButton(
tooltip: translate('Clear'),
onPressed: () { onPressed: () {
onSearchText("", isLocal); onSearchText("", isLocal);
_locationStatus.value = LocationStatus.bread; _locationStatus.value = LocationStatus.bread;
@@ -642,6 +651,7 @@ class _FileManagerViewState extends State<FileManagerView> {
} }
}), }),
MenuButton( MenuButton(
tooltip: translate('Refresh File'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 3, left: 3,
), ),
@@ -667,6 +677,7 @@ class _FileManagerViewState extends State<FileManagerView> {
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [ children: [
MenuButton( MenuButton(
tooltip: translate('Home'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 3, right: 3,
), ),
@@ -682,11 +693,27 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor, hoverColor: Theme.of(context).hoverColor,
), ),
MenuButton( MenuButton(
tooltip: translate('Create Folder'),
onPressed: () { onPressed: () {
final name = TextEditingController(); final name = TextEditingController();
String? errorText;
_ffi.dialogManager.show((setState, close, context) { _ffi.dialogManager.show((setState, close, context) {
name.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() { submit() {
if (name.value.text.isNotEmpty) { if (name.value.text.isNotEmpty) {
if (!PathUtil.validName(name.value.text,
controller.options.value.isWindows)) {
setState(() {
errorText = translate("Invalid folder name");
});
return;
}
controller.createDir(PathUtil.join( controller.createDir(PathUtil.join(
controller.directory.value.path, controller.directory.value.path,
name.value.text, name.value.text,
@@ -718,6 +745,7 @@ class _FileManagerViewState extends State<FileManagerView> {
labelText: translate( labelText: translate(
"Please enter the folder name", "Please enter the folder name",
), ),
errorText: errorText,
), ),
controller: name, controller: name,
autofocus: true, autofocus: true,
@@ -751,6 +779,7 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor, hoverColor: Theme.of(context).hoverColor,
), ),
Obx(() => MenuButton( Obx(() => MenuButton(
tooltip: translate('Delete'),
onPressed: SelectedItems.valid(selectedItems.items) onPressed: SelectedItems.valid(selectedItems.items)
? () async { ? () async {
await (controller await (controller
@@ -882,6 +911,7 @@ class _FileManagerViewState extends State<FileManagerView> {
menuPos = RelativeRect.fromLTRB(x, y, x, y); menuPos = RelativeRect.fromLTRB(x, y, x, y);
}, },
child: MenuButton( child: MenuButton(
tooltip: translate('More'),
onPressed: () => mod_menu.showMenu( onPressed: () => mod_menu.showMenu(
context: context, context: context,
position: menuPos, position: menuPos,
@@ -971,6 +1001,7 @@ class _FileManagerViewState extends State<FileManagerView> {
final lastModifiedStr = entry.isDrive final lastModifiedStr = entry.isDrive
? " " ? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} "; : "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container( child: Obx(() => Container(
@@ -1035,6 +1066,35 @@ class _FileManagerViewState extends State<FileManagerView> {
_onSelectedChanged( _onSelectedChanged(
items, filteredEntries, entry, isLocal); items, filteredEntries, entry, isLocal);
}, },
onSecondaryTap: () {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version,
"1.3.0") >=
0)
mod_menu.PopupMenuItem(
child: Text("Rename"),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
}
},
onSecondaryTapDown: (details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
},
), ),
SizedBox( SizedBox(
width: 2.0, width: 2.0,

View File

@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
}; };
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( print(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
@@ -97,6 +96,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
controller: tabController, controller: tabController,
onWindowCloseButton: handleWindowCloseButton, onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(), tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter, labelGetter: DesktopTab.tablabelGetter,
)); ));
final tabWidget = isLinux final tabWidget = isLinux
@@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
: SubWindowDragToResizeArea( : SubWindowDragToResizeArea(
child: tabWidget, child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
); );
} }

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
class _InstallPageState extends State<InstallPage> { class _InstallPageState extends State<InstallPage> {
final tabController = DesktopTabController(tabType: DesktopTabType.main); final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override _InstallPageState() {
void initState() {
super.initState();
Get.put<DesktopTabController>(tabController); Get.put<DesktopTabController>(tabController);
const label = "install"; const label = "install";
tabController.add(TabInfo( tabController.add(TabInfo(
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragToResizeArea( return DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: windowManagerEnableResizeEdges,
child: Container( child: Container(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12), padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
); );
_InstallPageBodyState() {
controller = TextEditingController(text: bind.installInstallPath());
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
}
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
controller = TextEditingController(text: bind.installInstallPath());
super.initState(); super.initState();
} }
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (desktopicon.value) args += ' desktopicon'; if (desktopicon.value) args += ' desktopicon';
bind.installInstallMe(options: args, path: controller.text); bind.installInstallMe(options: args, path: controller.text);
} }
do_install(); do_install();
} }

View File

@@ -63,9 +63,12 @@ class _PortForwardPageState extends State<PortForwardPage>
isSharedPassword: widget.isSharedPassword, isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
isRdp: widget.isRDP); isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}'); Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}"); debugPrint("Port forward page init success with id ${widget.id}");
widget.tabController.onSelected?.call(widget.id); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
});
} }
@override @override

View File

@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
}; };
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@@ -54,8 +55,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint( debugPrint(
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
@@ -106,6 +105,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
return true; return true;
}, },
tail: AddButton(), tail: AddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter, labelGetter: DesktopTab.tablabelGetter,
), ),
); );
@@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
() => SubWindowDragToResizeArea( () => SubWindowDragToResizeArea(
child: tabWidget, child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
), ),
); );

View File

@@ -45,7 +45,9 @@ class RemotePage extends StatefulWidget {
this.switchUuid, this.switchUuid,
this.forceRelay, this.forceRelay,
this.isSharedPassword, this.isSharedPassword,
}) : super(key: key); }) : super(key: key) {
initSharedStates(id);
}
final String id; final String id;
final SessionID? sessionId; final SessionID? sessionId;
@@ -64,7 +66,7 @@ class RemotePage extends StatefulWidget {
@override @override
State<RemotePage> createState() { State<RemotePage> createState() {
final state = _RemotePageState(); final state = _RemotePageState(id);
_lastState.value = state; _lastState.value = state;
return state; return state;
} }
@@ -94,8 +96,11 @@ class _RemotePageState extends State<RemotePage>
SessionID get sessionId => _ffi.sessionId; SessionID get sessionId => _ffi.sessionId;
_RemotePageState(String id) {
_initStates(id);
}
void _initStates(String id) { void _initStates(String id) {
initSharedStates(id);
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor); _zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
_showRemoteCursor = ShowRemoteCursorState.find(id); _showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id);
@@ -105,9 +110,8 @@ class _RemotePageState extends State<RemotePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initStates(widget.id);
_ffi = FFI(widget.sessionId); _ffi = FFI(widget.sessionId);
Get.put(_ffi, tag: widget.id); Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) { _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded( showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.ffiModel.pi.platform, _ffi.dialogManager);
@@ -135,11 +139,13 @@ class _RemotePageState extends State<RemotePage>
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible(); _ffi.dialogManager.loadMobileActionsOverlayVisible();
// Session option should be set after models.dart/FFI.start WidgetsBinding.instance.addPostFrameCallback((_) {
_showRemoteCursor.value = bind.sessionGetToggleOptionSync( // Session option should be set after models.dart/FFI.start
sessionId: sessionId, arg: 'show-remote-cursor'); _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
_zoomCursor.value = bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: 'show-remote-cursor');
sessionId: sessionId, arg: kOptionZoomCursor); _zoomCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionZoomCursor);
});
DesktopMultiWindow.addListener(this); DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) { // if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback( // customCursorController.registerNeedUpdateCursorCallback(
@@ -154,7 +160,10 @@ class _RemotePageState extends State<RemotePage>
// } // }
_blockableOverlayState.applyFfi(_ffi); _blockableOverlayState.applyFfi(_ffi);
widget.tabController?.onSelected?.call(widget.id); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController?.onSelected?.call(widget.id);
});
} }
@override @override
@@ -506,12 +515,13 @@ class _RemotePageState extends State<RemotePage>
]; ];
if (!_ffi.canvasModel.cursorEmbedded) { if (!_ffi.canvasModel.cursorEmbedded) {
paints.add(Obx(() => Offstage( paints
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse, .add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
child: CursorPaint( ? Offstage()
id: widget.id, : CursorPaint(
zoomCursor: _zoomCursor, id: widget.id,
)))); zoomCursor: _zoomCursor,
)));
} }
paints.add( paints.add(
Positioned( Positioned(
@@ -564,11 +574,6 @@ class _ImagePaintState extends State<ImagePaint> {
RxBool get remoteCursorMoved => widget.remoteCursorMoved; RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context); final m = Provider.of<ImageModel>(context);

View File

@@ -71,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final ffi = remotePage.ffi; final ffi = remotePage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId); bind.setCurSessionId(sessionId: ffi.sessionId);
} }
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(params['windowId'])
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
UnreadChatCountState.find(id).value = 0; UnreadChatCountState.find(id).value = 0;
}; };
@@ -98,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
)); ));
_update_remote_count(); _update_remote_count();
} }
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
if (!_isScreenRectSet) { if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
restoreWindowPosition( restoreWindowPosition(
@@ -121,11 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
} }
} }
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = Scaffold( final child = Scaffold(
@@ -134,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
controller: tabController, controller: tabController,
onWindowCloseButton: handleWindowCloseButton, onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(), tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
pageViewBuilder: (pageView) => pageView, pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter, labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() { tabBuilder: (key, icon, label, themeConf) => Obx(() {
@@ -233,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
// Specially configured for a better resize area and remote control. // Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding, childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
)); ));
} }
@@ -413,12 +409,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final display = args['display']; final display = args['display'];
final displays = args['displays']; final displays = args['displays'];
final screenRect = parseParamScreenRect(args); final screenRect = parseParamScreenRect(args);
final prePeerCount = tabController.length;
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) { if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false); await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false); stateGlobal.setFullscreen(false, procWnd: false);
} }
await setNewConnectWindowFrame(windowId(), id!, display, screenRect); await setNewConnectWindowFrame(
windowId(), id!, prePeerCount, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId()); await windowOnTop(windowId());
}); });

View File

@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
class _DesktopServerPageState extends State<DesktopServerPage> class _DesktopServerPageState extends State<DesktopServerPage>
with WindowListener, AutomaticKeepAliveClientMixin { with WindowListener, AutomaticKeepAliveClientMixin {
final tabController = gFFI.serverModel.tabController; final tabController = gFFI.serverModel.tabController;
@override
void initState() { _DesktopServerPageState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, ""); gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this); Get.put<DesktopTabController>(tabController);
Get.put(tabController);
tabController.onRemoved = (_, id) { tabController.onRemoved = (_, id) {
onRemoveId(id); onRemoveId(id);
}; };
}
@override
void initState() {
windowManager.addListener(this);
super.initState(); super.initState();
} }
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
child: Consumer<ServerModel>( child: Consumer<ServerModel>(
builder: (context, serverModel, child) { builder: (context, serverModel, child) {
final body = Scaffold( final body = Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).colorScheme.background,
body: ConnectionManager(), body: ConnectionManager(),
); );
return isLinux return isLinux
@@ -104,10 +108,11 @@ class ConnectionManager extends StatefulWidget {
State<StatefulWidget> createState() => ConnectionManagerState(); State<StatefulWidget> createState() => ConnectionManagerState();
} }
class ConnectionManagerState extends State<ConnectionManager> { class ConnectionManagerState extends State<ConnectionManager>
@override with WidgetsBindingObserver {
void initState() { final RxBool _block = false.obs;
gFFI.serverModel.updateClientState();
ConnectionManagerState() {
gFFI.serverModel.tabController.onSelected = (client_id_str) { gFFI.serverModel.tabController.onSelected = (client_id_str) {
final client_id = int.tryParse(client_id_str); final client_id = int.tryParse(client_id_str);
if (client_id != null) { if (client_id != null) {
@@ -116,7 +121,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
if (client != null) { if (client != null) {
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id)); gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
if (client.unreadChatMessageCount.value > 0) { if (client.unreadChatMessageCount.value > 0) {
Future.delayed(Duration.zero, () { WidgetsBinding.instance.addPostFrameCallback((_) {
client.unreadChatMessageCount.value = 0; client.unreadChatMessageCount.value = 0;
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id)); gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
}); });
@@ -127,9 +132,31 @@ class ConnectionManagerState extends State<ConnectionManager> {
} }
}; };
gFFI.chatModel.isConnManager = true; gFFI.chatModel.isConnManager = true;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (!allowRemoteCMModification()) {
shouldBeBlocked(_block, null);
}
}
}
@override
void initState() {
gFFI.serverModel.updateClientState();
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context); final serverModel = Provider.of<ServerModel>(context);
@@ -165,8 +192,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
selectedBorderColor: MyTheme.accent, selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100, maxLabelWidth: 100,
tail: null, //buildScrollJumper(), tail: null, //buildScrollJumper(),
selectedTabBackgroundColor: blockTab: allowRemoteCMModification() ? null : _block,
Theme.of(context).hintColor.withOpacity(0),
tabBuilder: (key, icon, label, themeConf) { tabBuilder: (key, icon, label, themeConf) {
final client = serverModel.clients final client = serverModel.clients
.firstWhereOrNull((client) => client.id.toString() == key); .firstWhereOrNull((client) => client.id.toString() == key);
@@ -201,27 +227,28 @@ class ConnectionManagerState extends State<ConnectionManager> {
borderWidth; borderWidth;
final realChatPageWidth = final realChatPageWidth =
constrains.maxWidth - realClosedWidth; constrains.maxWidth - realClosedWidth;
return Row(children: [ final row = Row(children: [
if (constrains.maxWidth > if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width) kConnectionManagerWindowSizeClosedChat.width)
Consumer<ChatModel>( Consumer<ChatModel>(
builder: (_, model, child) => SizedBox( builder: (_, model, child) => SizedBox(
width: realChatPageWidth, width: realChatPageWidth,
child: buildRemoteBlock( child: allowRemoteCMModification()
child: Container( ? buildSidePage()
decoration: BoxDecoration( : buildRemoteBlock(
border: Border( child: buildSidePage(),
right: BorderSide( block: _block,
color: Theme.of(context) mask: true),
.dividerColor))),
child: buildSidePage()),
),
)), )),
SizedBox( SizedBox(
width: realClosedWidth, width: realClosedWidth,
child: child:
SizedBox(width: realClosedWidth, child: pageView)), SizedBox(width: realClosedWidth, child: pageView)),
]); ]);
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: row,
);
}, },
), ),
), ),
@@ -381,7 +408,10 @@ class _CmHeaderState extends State<_CmHeader>
_time.value = _time.value + 1; _time.value = _time.value + 1;
} }
}); });
gFFI.serverModel.tabController.onSelected?.call(client.id.toString()); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
});
} }
@override @override
@@ -714,7 +744,8 @@ class _CmControlPanel extends StatelessWidget {
child: buildButton(context, child: buildButton(context,
color: MyTheme.accent, color: MyTheme.accent,
onClick: null, onTapDown: (details) async { onClick: null, onTapDown: (details) async {
final devicesInfo = await AudioInput.getDevicesInfo(); final devicesInfo =
await AudioInput.getDevicesInfo(true, true);
List<String> devices = devicesInfo['devices'] as List<String>; List<String> devices = devicesInfo['devices'] as List<String>;
if (devices.isEmpty) { if (devices.isEmpty) {
msgBox( msgBox(
@@ -740,13 +771,14 @@ class _CmControlPanel extends StatelessWidget {
value: d, value: d,
height: 18, height: 18,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onTap: () => AudioInput.setDevice(d), onTap: () => AudioInput.setDevice(d, true, true),
child: IgnorePointer( child: IgnorePointer(
child: RadioMenuButton( child: RadioMenuButton(
value: d, value: d,
groupValue: currentDevice, groupValue: currentDevice,
onChanged: (v) { onChanged: (v) {
if (v != null) AudioInput.setDevice(v); if (v != null)
AudioInput.setDevice(v, true, true);
}, },
child: Container( child: Container(
child: Text( child: Text(
@@ -1043,6 +1075,10 @@ class _CmControlPanel extends StatelessWidget {
} }
void checkClickTime(int id, Function() callback) async { void checkClickTime(int id, Function() callback) async {
if (allowRemoteCMModification()) {
callback();
return;
}
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch; var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
await bind.cmCheckClickTime(connId: id); await bind.cmCheckClickTime(connId: id);
Timer(const Duration(milliseconds: 120), () async { Timer(const Duration(milliseconds: 120), () async {
@@ -1051,6 +1087,11 @@ void checkClickTime(int id, Function() callback) async {
}); });
} }
bool allowRemoteCMModification() {
return option2bool(kOptionAllowRemoteCmModification,
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
}
class _FileTransferLogPage extends StatefulWidget { class _FileTransferLogPage extends StatefulWidget {
_FileTransferLogPage({Key? key}) : super(key: key); _FileTransferLogPage({Key? key}) : super(key: key);
@@ -1116,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
Text(translate('Create Folder')) Text(translate('Create Folder'))
], ],
); );
case CmFileAction.rename:
return Column(
children: [
Icon(
Icons.drive_file_move_outlined,
color: Theme.of(context).tabBarTheme.labelColor,
),
Text(translate('Rename'))
],
);
} }
} }

View File

@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
return Padding( return Padding(
padding: widget.padding, padding: widget.padding,
child: Tooltip( child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: widget.tooltip, message: widget.tooltip,
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,

View File

@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
@override @override
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() => MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(); MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
} }
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>> class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
extends State<W> { extends State<W> {
RxBool enabled = true.obs; RxBool enabled = true.obs;
@override MyPopupMenuItemState(bool? e) {
void initState() { if (e != null) {
super.initState(); enabled.value = e;
if (widget.enabled != null) {
enabled.value = widget.enabled!.value;
} }
} }

View File

@@ -372,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
initState() { initState() {
super.initState(); super.initState();
Future.delayed(Duration.zero, () async { WidgetsBinding.instance.addPostFrameCallback((_) async {
_fractionX.value = double.tryParse(await bind.sessionGetOption( _fractionX.value = double.tryParse(await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, sessionId: widget.ffi.sessionId,
arg: 'remote-menubar-drag-x') ?? arg: 'remote-menubar-drag-x') ??
@@ -1032,11 +1032,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
String get id => widget.id; String get id => widget.id;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_screenAdjustor.updateScreen(); _screenAdjustor.updateScreen();
@@ -1052,15 +1047,11 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi, ffi: widget.ffi,
screenAdjustor: _screenAdjustor, screenAdjustor: _screenAdjustor,
), ),
if (pi.isRustDeskIdd) if (showVirtualDisplayMenu(ffi))
_RustDeskVirtualDisplayMenu( _SubmenuButton(
id: widget.id,
ffi: widget.ffi,
),
if (pi.isAmyuniIdd)
_AmyuniVirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi, ffi: widget.ffi,
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
child: Text(translate("Virtual display")),
), ),
cursorToggles(), cursorToggles(),
Divider(), Divider(),
@@ -1282,7 +1273,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_getLocalResolutionWayland(); WidgetsBinding.instance.addPostFrameCallback((_) {
_getLocalResolutionWayland();
});
} }
Rect? scaledRect() { Rect? scaledRect() {
@@ -1559,155 +1552,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
} }
} }
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_RustDeskVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_RustDeskVirtualDisplayMenu> createState() =>
_RustDeskVirtualDisplayMenuState();
}
class _RustDeskVirtualDisplayMenuState
extends State<_RustDeskVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: i + 1,
on: value);
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: widget.ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)));
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_AmyuniVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_AmyuniVirtualDisplayMenu> createState() =>
_AmiyuniVirtualDisplayMenuState();
}
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: false),
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: true),
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)),
];
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _KeyboardMenu extends StatelessWidget { class _KeyboardMenu extends StatelessWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
@@ -1741,6 +1585,7 @@ class _KeyboardMenu extends StatelessWidget {
viewMode(), viewMode(),
Divider(), Divider(),
...toolbarToggles(), ...toolbarToggles(),
...mobileActions(),
]); ]);
} }
@@ -1877,6 +1722,39 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi, ffi: ffi,
child: Text(translate('View Mode'))); child: Text(translate('View Mode')));
} }
mobileActions() {
if (pi.platform != kPeerPlatformAndroid) return [];
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
Divider(),
MenuButton(
child: Text(translate('Back')),
onPressed: () => ffi.inputModel.onMobileBack(),
ffi: ffi),
MenuButton(
child: Text(translate('Home')),
onPressed: () => ffi.inputModel.onMobileHome(),
ffi: ffi),
MenuButton(
child: Text(translate('Apps')),
onPressed: () => ffi.inputModel.onMobileApps(),
ffi: ffi),
MenuButton(
child: Text(translate('Volume up')),
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
ffi: ffi),
MenuButton(
child: Text(translate('Volume down')),
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
ffi: ffi),
MenuButton(
child: Text(translate('Power')),
onPressed: () => ffi.inputModel.onMobilePower(),
ffi: ffi),
];
}
} }
class _ChatMenu extends StatefulWidget { class _ChatMenu extends StatefulWidget {
@@ -1950,28 +1828,31 @@ class _VoiceCallMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
menuChildrenGetter() { menuChildrenGetter() {
final audioInput = final audioInput = AudioInput(
AudioInput(builder: (devices, currentDevice, setDevice) { builder: (devices, currentDevice, setDevice) {
return Column( return Column(
children: devices children: devices
.map((d) => RdoMenuButton<String>( .map((d) => RdoMenuButton<String>(
child: Container( child: Container(
child: Text( child: Text(
d, d,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
),
constraints: BoxConstraints(maxWidth: 250),
), ),
constraints: BoxConstraints(maxWidth: 250), value: d,
), groupValue: currentDevice,
value: d, onChanged: (v) {
groupValue: currentDevice, if (v != null) setDevice(v);
onChanged: (v) { },
if (v != null) setDevice(v); ffi: ffi,
}, ))
ffi: ffi, .toList(),
)) );
.toList(), },
); isCm: false,
}); isVoiceCall: true,
);
return [ return [
audioInput, audioInput,
Divider(), Divider(),

View File

@@ -227,11 +227,9 @@ typedef TabMenuBuilder = Widget Function(String key);
typedef LabelGetter = Rx<String> Function(String key); typedef LabelGetter = Rx<String> Function(String key);
/// [_lastClickTime], help to handle double click /// [_lastClickTime], help to handle double click
int _lastClickTime = int _lastClickTime = 0;
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
// ignore: must_be_immutable class DesktopTab extends StatefulWidget {
class DesktopTab extends StatelessWidget {
final bool showLogo; final bool showLogo;
final bool showTitle; final bool showTitle;
final bool showMinimize; final bool showMinimize;
@@ -248,15 +246,12 @@ class DesktopTab extends StatelessWidget {
final Color? selectedTabBackgroundColor; final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor; final Color? unSelectedTabBackgroundColor;
final Color? selectedBorderColor; final Color? selectedBorderColor;
final RxBool? blockTab;
final DesktopTabController controller; final DesktopTabController controller;
Rx<DesktopTabState> get state => controller.state;
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
late final DesktopTabType tabType;
late final bool isMainWindow;
final RxList<String> invisibleTabKeys = RxList.empty(); final RxList<String> invisibleTabKeys = RxList.empty();
DesktopTab({ DesktopTab({
@@ -277,38 +272,260 @@ class DesktopTab extends StatelessWidget {
this.selectedTabBackgroundColor, this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor, this.unSelectedTabBackgroundColor,
this.selectedBorderColor, this.selectedBorderColor,
}) : super(key: key) { this.blockTab,
tabType = controller.tabType; }) : super(key: key);
isMainWindow = tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
}
static RxString tablabelGetter(String peerId) { static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias'); final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias)); return RxString(getDesktopTabLabel(peerId, alias));
} }
@override
State<DesktopTab> createState() {
return _DesktopTabState();
}
}
// ignore: must_be_immutable
class _DesktopTabState extends State<DesktopTab>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
bool get showLogo => widget.showLogo;
bool get showTitle => widget.showTitle;
bool get showMinimize => widget.showMinimize;
bool get showMaximize => widget.showMaximize;
bool get showClose => widget.showClose;
Widget Function(Widget pageView)? get pageViewBuilder =>
widget.pageViewBuilder;
TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
Widget? get tail => widget.tail;
Future<bool> Function()? get onWindowCloseButton =>
widget.onWindowCloseButton;
TabBuilder? get tabBuilder => widget.tabBuilder;
LabelGetter? get labelGetter => widget.labelGetter;
double? get maxLabelWidth => widget.maxLabelWidth;
Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
Color? get unSelectedTabBackgroundColor =>
widget.unSelectedTabBackgroundColor;
Color? get selectedBorderColor => widget.selectedBorderColor;
RxBool? get blockTab => widget.blockTab;
DesktopTabController get controller => widget.controller;
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
Debouncer get _scrollDebounce => widget._scrollDebounce;
Rx<DesktopTabState> get state => controller.state;
DesktopTabType get tabType => controller.tabType;
bool get isMainWindow =>
tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
_DesktopTabState() : super();
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
});
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
super.onWindowMinimize();
}
@override
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
}
_saveFrame() async {
if (tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController windowController) async {
if (controller.length != 0) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await windowController.show();
await windowController.focus();
final res = await onWindowCloseButton?.call() ?? true;
if (!res) return;
}
controller.clear();
}
await windowController.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
}
macOSWindowClose(
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_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
if (isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await onWindowCloseButton?.call() ?? true) {
if (await controller.isFullScreen()) {
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
await notMainWindowClose(controller);
}
}
} else {
await notMainWindowClose(controller);
}
}
super.onWindowClose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(children: [ return Column(children: [
Obx(() => Offstage( Obx(() {
offstage: !stateGlobal.showTabBar.isTrue || if (stateGlobal.showTabBar.isTrue &&
(kUseCompatibleUiMode && isHideSingleItem()), !(kUseCompatibleUiMode && isHideSingleItem())) {
child: SizedBox( final showBottomDivider = _showTabBarBottomDivider(tabType);
return SizedBox(
height: _kTabBarHeight, height: _kTabBarHeight,
child: Column( child: Column(
children: [ children: [
SizedBox( SizedBox(
height: _kTabBarHeight - 1, height:
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
child: _buildBar(), child: _buildBar(),
), ),
const Divider( if (showBottomDivider)
height: 1, const Divider(
), height: 1,
),
], ],
), ),
))), );
} else {
return Offstage();
}
}),
Expanded( Expanded(
child: pageViewBuilder != null child: pageViewBuilder != null
? pageViewBuilder!(_buildPageView()) ? pageViewBuilder!(_buildPageView())
@@ -317,10 +534,15 @@ class DesktopTab extends StatelessWidget {
} }
Widget _buildBlock({required Widget child}) { Widget _buildBlock({required Widget child}) {
if (tabType != DesktopTabType.main) { if (blockTab != null) {
return buildRemoteBlock(
child: child,
block: blockTab!,
use: canBeBlocked,
mask: tabType == DesktopTabType.main);
} else {
return child; return child;
} }
return buildRemoteBlock(child: child, use: canBeBlocked);
} }
List<Widget> _tabWidgets = []; List<Widget> _tabWidgets = [];
@@ -457,7 +679,6 @@ class DesktopTab extends StatelessWidget {
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar. // hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
WindowActionPanel( WindowActionPanel(
isMainWindow: isMainWindow, isMainWindow: isMainWindow,
tabType: tabType,
state: state, state: state,
tabController: controller, tabController: controller,
invisibleTabKeys: invisibleTabKeys, invisibleTabKeys: invisibleTabKeys,
@@ -475,7 +696,6 @@ class DesktopTab extends StatelessWidget {
class WindowActionPanel extends StatefulWidget { class WindowActionPanel extends StatefulWidget {
final bool isMainWindow; final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state; final Rx<DesktopTabState> state;
final DesktopTabController tabController; final DesktopTabController tabController;
@@ -491,7 +711,6 @@ class WindowActionPanel extends StatefulWidget {
const WindowActionPanel( const WindowActionPanel(
{Key? key, {Key? key,
required this.isMainWindow, required this.isMainWindow,
required this.tabType,
required this.state, required this.state,
required this.tabController, required this.tabController,
required this.invisibleTabKeys, required this.invisibleTabKeys,
@@ -509,180 +728,7 @@ class WindowActionPanel extends StatefulWidget {
} }
} }
class WindowActionPanelState extends State<WindowActionPanel> class WindowActionPanelState extends State<WindowActionPanel> {
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
});
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
super.onWindowMinimize();
}
@override
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
}
_saveFrame() async {
if (widget.tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
if (widget.tabController.length != 0) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await controller.show();
await controller.focus();
final res = await widget.onClose?.call() ?? true;
if (!res) return;
}
widget.tabController.clear();
}
await controller.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
}
macOSWindowClose(
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_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
if (widget.isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await widget.onClose?.call() ?? true) {
if (await controller.isFullScreen()) {
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
await notMainWindowClose(controller);
}
}
} else {
await notMainWindowClose(controller);
}
}
super.onWindowClose();
}
bool showTabDowndown() { bool showTabDowndown() {
return widget.tabController.state.value.tabs.length > 1 && return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen || (widget.tabController.tabType == DesktopTabType.remoteScreen ||
@@ -703,72 +749,69 @@ class WindowActionPanelState extends State<WindowActionPanel>
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Obx(() => Offstage( Obx(() {
offstage: if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
!(showTabDowndown() && existingInvisibleTab().isNotEmpty), return _TabDropDownButton(
child: _TabDropDownButton( controller: widget.tabController,
controller: widget.tabController, labelGetter: widget.labelGetter,
labelGetter: widget.labelGetter, tabkeys: existingInvisibleTab());
tabkeys: existingInvisibleTab()), } else {
)), return Offstage();
Offstage(offstage: widget.tail == null, child: widget.tail), }
Offstage( }),
offstage: kUseCompatibleUiMode, if (widget.tail != null) widget.tail!,
child: Row( if (!kUseCompatibleUiMode)
Row(
children: [ children: [
Offstage( if (widget.showMinimize && !isMacOS)
offstage: !widget.showMinimize || isMacOS, ActionIcon(
child: ActionIcon( message: 'Minimize',
message: 'Minimize', icon: IconFont.min,
icon: IconFont.min, onTap: () {
onTap: () { if (widget.isMainWindow) {
if (widget.isMainWindow) { windowManager.minimize();
windowManager.minimize(); } else {
} else { WindowController.fromWindowId(kWindowId!).minimize();
WindowController.fromWindowId(kWindowId!).minimize(); }
} },
}, isClose: false,
isClose: false, ),
)), if (widget.showMaximize && !isMacOS)
Offstage( Obx(() => ActionIcon(
offstage: !widget.showMaximize || isMacOS, message: stateGlobal.isMaximized.isTrue
child: Obx(() => ActionIcon( ? 'Restore'
message: stateGlobal.isMaximized.isTrue : 'Maximize',
? 'Restore' icon: stateGlobal.isMaximized.isTrue
: 'Maximize', ? IconFont.restore
icon: stateGlobal.isMaximized.isTrue : IconFont.max,
? IconFont.restore onTap: bind.isIncomingOnly() && isInHomePage()
: IconFont.max, ? null
onTap: bind.isIncomingOnly() && isInHomePage() : _toggleMaximize,
? null isClose: false,
: _toggleMaximize, )),
isClose: false, if (widget.showClose && !isMacOS)
))), ActionIcon(
Offstage( message: 'Close',
offstage: !widget.showClose || isMacOS, icon: IconFont.close,
child: ActionIcon( onTap: () async {
message: 'Close', final res = await widget.onClose?.call() ?? true;
icon: IconFont.close, if (res) {
onTap: () async { // hide for all window
final res = await widget.onClose?.call() ?? true; // note: the main window can be restored by tray icon
if (res) { Future.delayed(Duration.zero, () async {
// hide for all window if (widget.isMainWindow) {
// note: the main window can be restored by tray icon await windowManager.close();
Future.delayed(Duration.zero, () async { } else {
if (widget.isMainWindow) { await WindowController.fromWindowId(kWindowId!)
await windowManager.close(); .close();
} else { }
await WindowController.fromWindowId(kWindowId!) });
.close(); }
} },
}); isClose: true,
} )
},
isClose: true,
))
], ],
), ),
),
], ],
); );
} }
@@ -1121,7 +1164,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
height: _kTabBarHeight, // _kTabBarHeight also displays normally
height: _showTabBarBottomDivider(widget.tabType)
? _kTabBarHeight - 1
: _kTabBarHeight,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@@ -1172,22 +1218,26 @@ class _CloseButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: _kIconSize, width: _kIconSize,
child: Offstage( child: () {
offstage: !visible, if (visible) {
child: InkWell( return InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor, hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: () => onClose(), onTap: () => onClose(),
child: Icon( child: Icon(
Icons.close, Icons.close,
size: _kIconSize, size: _kIconSize,
color: tabSelected color: tabSelected
? MyTheme.tabbar(context).selectedIconColor ? MyTheme.tabbar(context).selectedIconColor
: MyTheme.tabbar(context).unSelectedIconColor, : MyTheme.tabbar(context).unSelectedIconColor,
), ),
), );
)).paddingOnly(left: 10); } else {
return Offstage();
}
}())
.paddingOnly(left: 10);
} }
} }
@@ -1216,13 +1266,7 @@ class ActionIcon extends StatefulWidget {
} }
class _ActionIconState extends State<ActionIcon> { class _ActionIconState extends State<ActionIcon> {
var hover = false.obs; final hover = false.obs;
@override
void initState() {
super.initState();
hover.value = false;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -1341,27 +1385,30 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
child: InkWell(child: Text(label)), child: InkWell(child: Text(label)),
), ),
Obx( Obx(
() => Offstage( () {
offstage: !(tabInfo?.onTabCloseButton != null && if (tabInfo?.onTabCloseButton != null &&
menuHover.value), menuHover.value) {
child: InkWell( return InkWell(
onTap: () { onTap: () {
tabInfo?.onTabCloseButton?.call(); tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) { if (Navigator.of(context).canPop()) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
onHover: (event) => onHover: (event) =>
setState(() => btnHover.value = true), setState(() => btnHover.value = true),
onExit: (event) => onExit: (event) =>
setState(() => btnHover.value = false), setState(() => btnHover.value = false),
child: Icon(Icons.close, child: Icon(Icons.close,
color: color:
btnHover.value ? Colors.red : null))), btnHover.value ? Colors.red : null)));
), } else {
) return Offstage();
}
},
),
], ],
), ),
), ),
@@ -1373,6 +1420,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
} }
} }
bool _showTabBarBottomDivider(DesktopTabType tabType) {
return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
}
class TabbarTheme extends ThemeExtension<TabbarTheme> { class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color? selectedTabIconColor; final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor; final Color? unSelectedTabIconColor;

View File

@@ -260,7 +260,7 @@ showCmWindow({bool isStartup = false}) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions( WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true); size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
await windowManager.waitUntilReadyToShow(windowOptions, null); await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker(); bind.mainHideDock();
await Future.wait([ await Future.wait([
windowManager.show(), windowManager.show(),
windowManager.focus(), windowManager.focus(),
@@ -288,14 +288,14 @@ hideCmWindow({bool isStartup = false}) async {
size: kConnectionManagerWindowSizeClosedChat); size: kConnectionManagerWindowSizeClosedChat);
windowManager.setOpacity(0); windowManager.setOpacity(0);
await windowManager.waitUntilReadyToShow(windowOptions, null); await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker(); bind.mainHideDock();
await windowManager.minimize(); await windowManager.minimize();
await windowManager.hide(); await windowManager.hide();
_isCmReadyToShow = true; _isCmReadyToShow = true;
} else if (_isCmReadyToShow) { } else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 0) { if (await windowManager.getOpacity() != 0) {
await windowManager.setOpacity(0); await windowManager.setOpacity(0);
bind.mainHideDocker(); bind.mainHideDock();
await windowManager.minimize(); await windowManager.minimize();
await windowManager.hide(); await windowManager.hide();
} }

View File

@@ -50,19 +50,26 @@ class _ConnectionPageState extends State<ConnectionPage> {
bool isPeersLoaded = false; bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription; StreamSubscription? _uniLinksSubscription;
_ConnectionPageState() {
if (!isWeb) _uniLinksSubscription = listenUniLinks();
_idController.addListener(() {
_idEmpty.value = _idController.text.isEmpty;
});
Get.put<IDTextEditingController>(_idController);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!isWeb) _uniLinksSubscription = listenUniLinks();
if (_idController.text.isEmpty) { if (_idController.text.isEmpty) {
() async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId(); final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) { if (lastRemoteId != _idController.id) {
setState(() { setState(() {
_idController.id = lastRemoteId; _idController.id = lastRemoteId;
}); });
} }
}(); });
} }
if (isAndroid) { if (isAndroid) {
if (!bind.isCustomClient()) { if (!bind.isCustomClient()) {
@@ -72,11 +79,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
}); });
} }
} }
_idController.addListener(() {
_idEmpty.value = _idController.text.isEmpty;
});
Get.put<IDTextEditingController>(_idController);
} }
@override @override
@@ -395,7 +397,7 @@ class _WebMenuState extends State<WebMenu> {
[ [
PopupMenuItem( PopupMenuItem(
value: "about", value: "about",
child: Text('${translate('About')} RustDesk'), child: Text(translate('About RustDesk')),
) )
]; ];
}, },

View File

@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
setState(() {}); setState(() {});
} else if (v == "folder") { } else if (v == "folder") {
final name = TextEditingController(); final name = TextEditingController();
gFFI.dialogManager String? errorText;
.show((setState, close, context) => CustomAlertDialog( gFFI.dialogManager.show((setState, close, context) {
title: Text(translate("Create Folder")), name.addListener(() {
content: Column( if (errorText != null) {
mainAxisSize: MainAxisSize.min, setState(() {
children: [ errorText = null;
TextFormField( });
decoration: InputDecoration( }
labelText: translate( });
"Please enter the folder name"), return CustomAlertDialog(
), title: Text(translate("Create Folder")),
controller: name, content: Column(
), mainAxisSize: MainAxisSize.min,
], children: [
TextFormField(
decoration: InputDecoration(
labelText:
translate("Please enter the folder name"),
errorText: errorText,
), ),
actions: [ controller: name,
dialogButton("Cancel", ),
onPressed: () => close(false), ],
isOutline: true), ),
dialogButton("OK", onPressed: () { actions: [
if (name.value.text.isNotEmpty) { dialogButton("Cancel",
currentFileController.createDir( onPressed: () => close(false), isOutline: true),
PathUtil.join( dialogButton("OK", onPressed: () {
currentDir.path, if (name.value.text.isNotEmpty) {
name.value.text, if (!PathUtil.validName(
currentOptions.isWindows)); name.value.text,
close(); currentFileController
} .options.value.isWindows)) {
}) setState(() {
])); errorText =
translate("Invalid folder name");
});
return;
}
currentFileController.createDir(PathUtil.join(
currentDir.path,
name.value.text,
currentOptions.isWindows));
close();
}
})
]);
});
} else if (v == "hidden") { } else if (v == "hidden") {
currentFileController.toggleShowHidden(); currentFileController.toggleShowHidden();
} }
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
child: Text(translate("Properties")), child: Text(translate("Properties")),
value: "properties", value: "properties",
enabled: false, enabled: false,
) ),
if (!entries[index].isDrive &&
versionCmp(gFFI.ffiModel.pi.version,
"1.3.0") >=
0)
PopupMenuItem(
child: Text(translate("Rename")),
value: "rename",
)
]; ];
}, },
onSelected: (v) { onSelected: (v) {
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
_selectedItems.clear(); _selectedItems.clear();
widget.selectMode.toggle(isLocal); widget.selectMode.toggle(isLocal);
setState(() {}); setState(() {});
} else if (v == "rename") {
controller.renameAction(
entries[index], isLocal);
} }
}), }),
onTap: () { onTap: () {

View File

@@ -34,7 +34,7 @@ class RemotePage extends StatefulWidget {
final bool? isSharedPassword; final bool? isSharedPassword;
@override @override
State<RemotePage> createState() => _RemotePageState(); State<RemotePage> createState() => _RemotePageState(id);
} }
class _RemotePageState extends State<RemotePage> { class _RemotePageState extends State<RemotePage> {
@@ -55,6 +55,15 @@ class _RemotePageState extends State<RemotePage> {
InputModel get inputModel => gFFI.inputModel; InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId; SessionID get sessionId => gFFI.sessionId;
final TextEditingController _textController =
TextEditingController(text: initText);
_RemotePageState(String id) {
initSharedStates(id);
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -77,12 +86,9 @@ class _RemotePageState extends State<RemotePage> {
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
keyboardSubscription = keyboardSubscription =
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
initSharedStates(widget.id);
gFFI.chatModel gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_blockableOverlayState.applyFfi(gFFI); _blockableOverlayState.applyFfi(gFFI);
gFFI.dialogManager.loadMobileActionsOverlayVisible();
} }
@override @override
@@ -145,37 +151,59 @@ class _RemotePageState extends State<RemotePage> {
setState(() {}); setState(() {});
} }
// handle mobile virtual keyboard void _handleIOSSoftKeyboardInput(String newValue) {
void handleSoftKeyboardInput(String newValue) {
var oldValue = _value; var oldValue = _value;
_value = newValue; _value = newValue;
if (isIOS) { var i = newValue.length - 1;
var i = newValue.length - 1; for (; i >= 0 && newValue[i] != '\1'; --i) {}
for (; i >= 0 && newValue[i] != '\1'; --i) {} var j = oldValue.length - 1;
var j = oldValue.length - 1; for (; j >= 0 && oldValue[j] != '\1'; --j) {}
for (; j >= 0 && oldValue[j] != '\1'; --j) {} if (i < j) j = i;
if (i < j) j = i; var subNewValue = newValue.substring(j + 1);
newValue = newValue.substring(j + 1); var subOldValue = oldValue.substring(j + 1);
oldValue = oldValue.substring(j + 1);
var common = 0; // get common prefix of subNewValue and subOldValue
for (; var common = 0;
common < oldValue.length && for (;
common < newValue.length && common < subOldValue.length &&
newValue[common] == oldValue[common]; common < subNewValue.length &&
++common) {} subNewValue[common] == subOldValue[common];
for (i = 0; i < oldValue.length - common; ++i) { ++common) {}
inputModel.inputKey('VK_BACK');
} // get newStr from subNewValue
if (newValue.length > common) { var newStr = "";
var s = newValue.substring(common); if (subNewValue.length > common) {
if (s.length > 1) { newStr = subNewValue.substring(common);
bind.sessionInputString(sessionId: sessionId, value: s);
} else {
inputChar(s);
}
}
return;
} }
// Set the value to the old value and early return if is still composing. (1 && 2)
// 1. The composing range is valid
// 2. The new string is shorter than the composing range.
if (_textController.value.isComposingRangeValid) {
final composingLength = _textController.value.composing.end -
_textController.value.composing.start;
if (composingLength > newStr.length) {
_value = oldValue;
return;
}
}
// Delete the different part in the old value.
for (i = 0; i < subOldValue.length - common; ++i) {
inputModel.inputKey('VK_BACK');
}
// Input the new string.
if (newStr.length > 1) {
bind.sessionInputString(sessionId: sessionId, value: newStr);
} else {
inputChar(newStr);
}
}
void _handleNonIOSSoftKeyboardInput(String newValue) {
var oldValue = _value;
_value = newValue;
if (oldValue.isNotEmpty && if (oldValue.isNotEmpty &&
newValue.isNotEmpty && newValue.isNotEmpty &&
oldValue[0] == '\1' && oldValue[0] == '\1' &&
@@ -214,6 +242,15 @@ class _RemotePageState extends State<RemotePage> {
} }
} }
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (isIOS) {
_handleIOSSoftKeyboardInput(newValue);
} else {
_handleNonIOSSoftKeyboardInput(newValue);
}
}
void inputChar(String char) { void inputChar(String char) {
if (char == '\n') { if (char == '\n') {
char = 'VK_RETURN'; char = 'VK_RETURN';
@@ -227,6 +264,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.invokeMethod("enable_soft_keyboard", true); gFFI.invokeMethod("enable_soft_keyboard", true);
// destroy first, so that our _value trick can work // destroy first, so that our _value trick can work
_value = initText; _value = initText;
_textController.text = _value;
setState(() => _showEdit = false); setState(() => _showEdit = false);
_timer?.cancel(); _timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboard, () { _timer = Timer(kMobileDelaySoftKeyboard, () {
@@ -242,12 +280,10 @@ class _RemotePageState extends State<RemotePage> {
}); });
} }
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
Widget _bottomWidget() => _showGestureHelp Widget _bottomWidget() => _showGestureHelp
? getGestureHelp() ? getGestureHelp()
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty : (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar(keyboard) ? getBottomAppBar()
: Offstage()); : Offstage());
@override @override
@@ -314,7 +350,7 @@ class _RemotePageState extends State<RemotePage> {
return Container( return Container(
color: kColorCanvas, color: kColorCanvas,
child: isWebDesktop child: isWebDesktop
? getBodyForDesktopWithListener(keyboard) ? getBodyForDesktopWithListener()
: SafeArea( : SafeArea(
child: child:
OrientationBuilder(builder: (ctx, orientation) { OrientationBuilder(builder: (ctx, orientation) {
@@ -346,9 +382,9 @@ class _RemotePageState extends State<RemotePage> {
} }
Widget getRawPointerAndKeyBody(Widget child) { Widget getRawPointerAndKeyBody(Widget child) {
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; final ffiModel = Provider.of<FfiModel>(context);
return RawPointerMouseRegion( return RawPointerMouseRegion(
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
inputModel: inputModel, inputModel: inputModel,
// Disable RawKeyFocusScope before the connecting is established. // Disable RawKeyFocusScope before the connecting is established.
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog. // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
@@ -361,7 +397,8 @@ class _RemotePageState extends State<RemotePage> {
); );
} }
Widget getBottomAppBar(bool keyboard) { Widget getBottomAppBar() {
final ffiModel = Provider.of<FfiModel>(context);
return BottomAppBar( return BottomAppBar(
elevation: 10, elevation: 10,
color: MyTheme.accent, color: MyTheme.accent,
@@ -387,7 +424,7 @@ class _RemotePageState extends State<RemotePage> {
}, },
) )
] + ] +
(isWebDesktop (isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
? [] ? []
: gFFI.ffiModel.isPeerAndroid : gFFI.ffiModel.isPeerAndroid
? [ ? [
@@ -491,7 +528,7 @@ class _RemotePageState extends State<RemotePage> {
autofocus: true, autofocus: true,
focusNode: _mobileFocusNode, focusNode: _mobileFocusNode,
maxLines: null, maxLines: null,
initialValue: _value, controller: _textController,
// trick way to make backspace work always // trick way to make backspace work always
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
onChanged: handleSoftKeyboardInput, onChanged: handleSoftKeyboardInput,
@@ -499,48 +536,84 @@ class _RemotePageState extends State<RemotePage> {
), ),
]; ];
if (showCursorPaint) { if (showCursorPaint) {
paints.add(CursorPaint()); paints.add(CursorPaint(widget.id));
} }
return paints; return paints;
}())); }()));
} }
Widget getBodyForDesktopWithListener(bool keyboard) { Widget getBodyForDesktopWithListener() {
final ffiModel = Provider.of<FfiModel>(context);
var paints = <Widget>[ImagePaint()]; var paints = <Widget>[ImagePaint()];
if (showCursorPaint) { 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 (ffiModel.keyboard || cursor) {
paints.add(CursorPaint()); paints.add(CursorPaint(widget.id));
} }
} }
return Container( return Container(
color: MyTheme.canvasColor, child: Stack(children: paints)); color: MyTheme.canvasColor, child: Stack(children: paints));
} }
List<TTextMenu> _getMobileActionMenus() {
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
!gFFI.ffiModel.keyboard) {
return [];
}
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
TTextMenu(
child: Text(translate('Back')),
onPressed: () => gFFI.inputModel.onMobileBack(),
),
TTextMenu(
child: Text(translate('Home')),
onPressed: () => gFFI.inputModel.onMobileHome(),
),
TTextMenu(
child: Text(translate('Apps')),
onPressed: () => gFFI.inputModel.onMobileApps(),
),
TTextMenu(
child: Text(translate('Volume up')),
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
),
TTextMenu(
child: Text(translate('Volume down')),
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
),
TTextMenu(
child: Text(translate('Power')),
onPressed: () => gFFI.inputModel.onMobilePower(),
),
];
}
void showActions(String id) async { void showActions(String id) async {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final x = 120.0; final x = 120.0;
final y = size.height; final y = size.height;
final mobileActionMenus = _getMobileActionMenus();
final menus = toolbarControls(context, id, gFFI); final menus = toolbarControls(context, id, gFFI);
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final more = menus final List<PopupMenuEntry<int>> more = [
.asMap() ...mobileActionMenus
.entries .asMap()
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key)) .entries
.toList(); .map((e) =>
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList(),
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
...menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(
child: e.value.getChild(),
value: e.key + mobileActionMenus.length))
.toList(),
];
() async { () async {
var index = await showMenu( var index = await showMenu(
context: context, context: context,
@@ -548,8 +621,12 @@ class _RemotePageState extends State<RemotePage> {
items: more, items: more,
elevation: 8, elevation: 8,
); );
if (index != null && index < menus.length) { if (index != null) {
menus[index].onPressed.call(); if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
}
} }
}(); }();
} }
@@ -569,9 +646,11 @@ class _RemotePageState extends State<RemotePage> {
child: Text(translate(label), style: labelStyle), child: Text(translate(label), style: labelStyle),
trailingIcon: Transform.scale( trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1, scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton( child: IgnorePointer(
onPressed: onPressed, child: IconButton(
icon: icon, onPressed: null,
icon: icon,
),
), ),
), ),
onPressed: onPressed, onPressed: onPressed,
@@ -602,23 +681,11 @@ class _RemotePageState extends State<RemotePage> {
), ),
onPressVoiceCall), onPressVoiceCall),
]; ];
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final menuItems = menus final menuItems = menus
.asMap() .asMap()
.entries .entries
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key)) .map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList(); .toList();
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
@@ -712,11 +779,6 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
onPressed: onPressed); onPressed: onPressed);
} }
@override
void initState() {
super.initState();
}
_updateRect() { _updateRect() {
RenderObject? renderObject = _key.currentContext?.findRenderObject(); RenderObject? renderObject = _key.currentContext?.findRenderObject();
if (renderObject == null) { if (renderObject == null) {
@@ -885,26 +947,52 @@ class ImagePaint extends StatelessWidget {
} }
class CursorPaint extends StatelessWidget { class CursorPaint extends StatelessWidget {
late final String id;
CursorPaint(this.id);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final m = Provider.of<CursorModel>(context); final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context); final c = Provider.of<CanvasModel>(context);
final ffiModel = Provider.of<FfiModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard(); final adjust = gFFI.cursorModel.adjustForKeyboard();
var s = c.scale; final s = c.scale;
double hotx = m.hotx; double hotx = m.hotx;
double hoty = m.hoty; double hoty = m.hoty;
if (m.image == null) { var image = m.image;
if (image == null) {
if (preDefaultCursor.image != null) { if (preDefaultCursor.image != null) {
image = preDefaultCursor.image;
hotx = preDefaultCursor.image!.width / 2; hotx = preDefaultCursor.image!.width / 2;
hoty = preDefaultCursor.image!.height / 2; hoty = preDefaultCursor.image!.height / 2;
} }
} }
if (preForbiddenCursor.image != null &&
!ffiModel.viewOnly &&
!ffiModel.keyboard &&
!ShowRemoteCursorState.find(id).value) {
image = preForbiddenCursor.image;
hotx = preForbiddenCursor.image!.width / 2;
hoty = preForbiddenCursor.image!.height / 2;
}
if (image == null) {
return Offstage();
}
final minSize = 12.0;
double mins =
minSize / (image.width > image.height ? image.width : image.height);
double factor = 1.0;
if (s < mins) {
factor = s / mins;
}
final s2 = s < mins ? mins : s;
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: m.image ?? preDefaultCursor.image, image: image,
x: m.x * s - hotx + c.x, x: (m.x - hotx) * factor + c.x / s2,
y: m.y * s - hoty + c.y - adjust, y: (m.y - hoty) * factor + (c.y - adjust) / s2,
scale: 1), scale: s2),
); );
} }
} }
@@ -982,22 +1070,40 @@ void showOptions(
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs; var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
final radios = [ final radios = [
for (var e in viewStyleRadios) for (var e in viewStyleRadios)
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) { Obx(() => getRadio<String>(
e.onChanged?.call(v); e.child,
if (v != null) viewStyle.value = v; e.value,
})), viewStyle.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
}
: null)),
const Divider(color: MyTheme.border), const Divider(color: MyTheme.border),
for (var e in imageQualityRadios) for (var e in imageQualityRadios)
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) { Obx(() => getRadio<String>(
e.onChanged?.call(v); e.child,
if (v != null) imageQuality.value = v; e.value,
})), imageQuality.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
}
: null)),
const Divider(color: MyTheme.border), const Divider(color: MyTheme.border),
for (var e in codecRadios) for (var e in codecRadios)
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) { Obx(() => getRadio<String>(
e.onChanged?.call(v); e.child,
if (v != null) codec.value = v; e.value,
})), codec.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
}
: null)),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
]; ];
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList(); final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
@@ -1008,10 +1114,12 @@ void showOptions(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
value: rxCursorToggleValues[e.key].value, value: rxCursorToggleValues[e.key].value,
onChanged: (v) { onChanged: e.value.onChanged != null
e.value.onChanged?.call(v); ? (v) {
if (v != null) rxCursorToggleValues[e.key].value = v; e.value.onChanged?.call(v);
}, if (v != null) rxCursorToggleValues[e.key].value = v;
}
: null,
title: e.value.child))) title: e.value.child)))
.toList(); .toList();
@@ -1023,10 +1131,12 @@ void showOptions(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
value: rxToggleValues[e.key].value, value: rxToggleValues[e.key].value,
onChanged: (v) { onChanged: e.value.onChanged != null
e.value.onChanged?.call(v); ? (v) {
if (v != null) rxToggleValues[e.key].value = v; e.value.onChanged?.call(v);
}, if (v != null) rxToggleValues[e.key].value = v;
}
: null,
title: e.value.child))) title: e.value.child)))
.toList(); .toList();
final toggles = [ final toggles = [
@@ -1046,14 +1156,110 @@ void showOptions(
); );
} }
var popupDialogMenus = List<Widget>.empty(growable: true);
final resolution = getResolutionMenu(gFFI, id);
if (resolution != null) {
popupDialogMenus.add(ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: resolution.child,
onTap: () {
close();
resolution.onPressed();
},
));
}
final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
if (virtualDisplayMenu != null) {
popupDialogMenus.add(ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: virtualDisplayMenu.child,
onTap: () {
close();
virtualDisplayMenu.onPressed();
},
));
}
if (popupDialogMenus.isNotEmpty) {
popupDialogMenus.add(const Divider(color: MyTheme.border));
}
return CustomAlertDialog( return CustomAlertDialog(
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: displays + radios + toggles + [privacyModeWidget]), children: displays +
radios +
popupDialogMenus +
toggles +
[privacyModeWidget]),
); );
}, clickMaskDismiss: true, backDismiss: true); }, clickMaskDismiss: true, backDismiss: true);
} }
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
if (!showVirtualDisplayMenu(ffi)) {
return null;
}
return TTextMenu(
child: Text(translate("Virtual display")),
onPressed: () {
ffi.dialogManager.show((setState, close, context) {
final children = getVirtualDisplayMenuChildren(ffi, id, close);
return CustomAlertDialog(
title: Text(translate('Virtual display')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
},
);
}
TTextMenu? getResolutionMenu(FFI ffi, String id) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final resolutions = pi.resolutions;
final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
final visible =
ffiModel.keyboard && (resolutions.length > 1) && display != null;
if (!visible) return null;
return TTextMenu(
child: Text(translate("Resolution")),
onPressed: () {
ffi.dialogManager.show((setState, close, context) {
final children = resolutions
.map((e) => getRadio<String>(
Text('${e.width}x${e.height}'),
'${e.width}x${e.height}',
'${display.width}x${display.height}',
(value) {
close();
bind.sessionChangeResolution(
sessionId: ffi.sessionId,
display: pi.currentDisplay,
width: e.width,
height: e.height,
);
},
))
.toList();
return CustomAlertDialog(
title: Text(translate('Resolution')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
},
);
}
void sendPrompt(bool isMac, String key) { void sendPrompt(bool isMac, String key) {
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl; final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
if (isMac) { if (isMac) {

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
final icon = const Icon(Icons.mobile_screen_share); final icon = const Icon(Icons.mobile_screen_share);
@override @override
final appBarActions = [ final appBarActions = (!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
? [_DropDownAction()]
: [];
ServerPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ServerPageState();
}
class _DropDownAction extends StatelessWidget {
_DropDownAction();
// should only have one action
final actions = [
PopupMenuButton<String>( PopupMenuButton<String>(
tooltip: "", tooltip: "",
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
@@ -101,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
), ),
]; ];
}, },
onSelected: (value) { onSelected: (value) async {
if (value == "changeID") { if (value == "changeID") {
changeIdDialog(); changeIdDialog();
} else if (value == "setPermanentPassword") { } else if (value == "setPermanentPassword") {
setPermanentPasswordDialog(gFFI.dialogManager); setPasswordDialog();
} else if (value == "setTemporaryPasswordLength") { } else if (value == "setTemporaryPasswordLength") {
setTemporaryPasswordLengthDialog(gFFI.dialogManager); setTemporaryPasswordLengthDialog(gFFI.dialogManager);
} else if (value == kUsePermanentPassword || } else if (value == kUsePermanentPassword ||
value == kUseTemporaryPassword || value == kUseTemporaryPassword ||
value == kUseBothPasswords) { value == kUseBothPasswords) {
bind.mainSetOption(key: kOptionVerificationMethod, value: value); callback() {
gFFI.serverModel.updatePasswordModel(); bind.mainSetOption(key: kOptionVerificationMethod, value: value);
gFFI.serverModel.updatePasswordModel();
}
if (value == kUsePermanentPassword &&
(await bind.mainGetPermanentPassword()).isEmpty) {
setPasswordDialog(notEmptyCallback: callback);
} else {
callback();
}
} else if (value.startsWith("AcceptSessionsVia")) { } else if (value.startsWith("AcceptSessionsVia")) {
value = value.substring("AcceptSessionsVia".length); value = value.substring("AcceptSessionsVia".length);
if (value == "Password") { if (value == "Password") {
@@ -126,10 +151,10 @@ class ServerPage extends StatefulWidget implements PageShape {
}) })
]; ];
ServerPage({Key? key}) : super(key: key);
@override @override
State<StatefulWidget> createState() => _ServerPageState(); Widget build(BuildContext context) {
return actions[0];
}
} }
class _ServerPageState extends State<ServerPage> { class _ServerPageState extends State<ServerPage> {
@@ -162,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
buildPresetPasswordWarning(), buildPresetPasswordWarningMobile(),
gFFI.serverModel.isStart gFFI.serverModel.isStart
? ServerInfo() ? ServerInfo()
: ServiceNotRunningNotification(), : ServiceNotRunningNotification(),

View File

@@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.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';
@@ -83,18 +85,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _fingerprint = ""; var _fingerprint = "";
var _buildDate = ""; var _buildDate = "";
var _autoDisconnectTimeout = ""; var _autoDisconnectTimeout = "";
var _hideServer = false;
var _hideProxy = false;
var _hideNetwork = false;
var _enableTrustedDevices = false;
@override _SettingsState() {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_enableAbr = option2bool( _enableAbr = option2bool(
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr)); kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery, _denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery)); bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) != _onlyWhiteList = whitelistNotEmpty();
defaultOptionWhitelist;
_enableDirectIPAccess = option2bool( _enableDirectIPAccess = option2bool(
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer)); kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
_enableRecordSession = option2bool(kOptionEnableRecordSession, _enableRecordSession = option2bool(kOptionEnableRecordSession,
@@ -109,8 +110,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect)); bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
_autoDisconnectTimeout = _autoDisconnectTimeout =
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout); bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
_hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
_hideNetwork =
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
}
() async { @override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) async {
var update = false; var update = false;
if (_hasIgnoreBattery) { if (_hasIgnoreBattery) {
@@ -169,7 +182,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
if (update) { if (update) {
setState(() {}); setState(() {});
} }
}(); });
} }
@override @override
@@ -233,18 +246,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
], ],
)); ));
final List<AbstractSettingsTile> enhancementsTiles = []; final List<AbstractSettingsTile> enhancementsTiles = [];
final List<AbstractSettingsTile> shareScreenTiles = [ final enable2fa = bind.mainHasValid2FaSync();
final List<AbstractSettingsTile> tfaTiles = [
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('enable-2fa-title')), title: Text(translate('enable-2fa-title')),
initialValue: bind.mainHasValid2FaSync(), initialValue: enable2fa,
onToggle: (_) async { onToggle: (v) async {
update() async { update() async {
setState(() {}); setState(() {});
} }
change2fa(callback: update); if (v == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
change2fa(callback: update);
});
} else {
change2fa(callback: update);
}
}, },
), ),
if (enable2fa)
SettingsTile.switchTile(
title: Text(translate('Telegram bot')),
initialValue: bind.mainHasValidBotSync(),
onToggle: (v) async {
update() async {
setState(() {});
}
if (v == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
changeBot(callback: update);
});
} else {
changeBot(callback: update);
}
},
),
if (enable2fa)
SettingsTile.switchTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate('Enable trusted devices')),
Text('* ${translate('enable-trusted-devices-tip')}',
style: Theme.of(context).textTheme.bodySmall),
],
),
initialValue: _enableTrustedDevices,
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
? null
: (v) async {
mainSetBoolOption(kOptionEnableTrustedDevices, v);
setState(() {
_enableTrustedDevices = v;
});
},
),
if (enable2fa && _enableTrustedDevices)
SettingsTile(
title: Text(translate('Manage trusted devices')),
trailing: Icon(Icons.arrow_forward_ios),
onPressed: (context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return _ManageTrustedDevices();
}));
})
];
final List<AbstractSettingsTile> shareScreenTiles = [
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('Deny LAN discovery')), title: Text(translate('Deny LAN discovery')),
initialValue: _denyLANDiscovery, initialValue: _denyLANDiscovery,
@@ -273,9 +344,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
initialValue: _onlyWhiteList, initialValue: _onlyWhiteList,
onToggle: (_) async { onToggle: (_) async {
update() async { update() async {
final onlyWhiteList = final onlyWhiteList = whitelistNotEmpty();
(await bind.mainGetOption(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
if (onlyWhiteList != _onlyWhiteList) { if (onlyWhiteList != _onlyWhiteList) {
setState(() { setState(() {
_onlyWhiteList = onlyWhiteList; _onlyWhiteList = onlyWhiteList;
@@ -530,6 +599,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
)); ));
final disabledSettings = bind.isDisableSettings(); final disabledSettings = bind.isDisableSettings();
final hideSecuritySettings =
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
final settings = SettingsList( final settings = SettingsList(
sections: [ sections: [
customClientSection, customClientSection,
@@ -553,13 +624,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
], ],
), ),
SettingsSection(title: Text(translate("Settings")), tiles: [ SettingsSection(title: Text(translate("Settings")), tiles: [
if (!disabledSettings) if (!disabledSettings && !_hideNetwork && !_hideServer)
SettingsTile( 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);
}), }),
if (!isIOS && !_hideNetwork && !_hideProxy)
SettingsTile(
title: Text(translate('Socks5/Http(s) Proxy')),
leading: Icon(Icons.network_ping),
onPressed: (context) {
changeSocks5Proxy();
}),
SettingsTile( SettingsTile(
title: Text(translate('Language')), title: Text(translate('Language')),
leading: Icon(Icons.translate), leading: Icon(Icons.translate),
@@ -625,13 +703,24 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
), ),
], ],
), ),
if (isAndroid && !disabledSettings && !outgoingOnly) if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection( SettingsSection(
title: Text(translate("Share Screen")), title: Text(translate("Share Screen")),
tiles: shareScreenTiles, tiles: shareScreenTiles,
), ),
if (!bind.isIncomingOnly()) defaultDisplaySection(), if (!bind.isIncomingOnly()) defaultDisplaySection(),
if (isAndroid && !disabledSettings && !outgoingOnly) if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection( SettingsSection(
title: Text(translate("Enhancements")), title: Text(translate("Enhancements")),
tiles: enhancementsTiles, tiles: enhancementsTiles,
@@ -786,7 +875,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
void showAbout(OverlayDialogManager dialogManager) { void showAbout(OverlayDialogManager dialogManager) {
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
return CustomAlertDialog( return CustomAlertDialog(
title: Text('${translate('About')} RustDesk'), title: Text(translate('About RustDesk')),
content: Wrap(direction: Axis.vertical, spacing: 12, children: [ content: Wrap(direction: Axis.vertical, spacing: 12, children: [
Text('Version: $version'), Text('Version: $version'),
InkWell( InkWell(
@@ -940,6 +1029,51 @@ class __DisplayPageState extends State<_DisplayPage> {
} }
} }
class _ManageTrustedDevices extends StatefulWidget {
const _ManageTrustedDevices();
@override
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
}
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
RxList<Uint8List> selectedDevices = RxList.empty();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(translate('Manage trusted devices')),
centerTitle: true,
actions: [
Obx(() => IconButton(
icon: Icon(Icons.delete, color: Colors.white),
onPressed: selectedDevices.isEmpty
? null
: () {
confrimDeleteTrustedDevicesDialog(
trustedDevices, selectedDevices);
}))
],
),
body: FutureBuilder(
future: TrustedDevice.get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final devices = snapshot.data as List<TrustedDevice>;
trustedDevices = devices.obs;
return trustedDevicesTable(trustedDevices, selectedDevices);
}),
);
}
}
class _RadioEntry { class _RadioEntry {
final String label; final String label;
final String value; final String value;

View File

@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
final OnTouchModeChange onTouchModeChange; final OnTouchModeChange onTouchModeChange;
@override @override
State<StatefulWidget> createState() => _GestureHelpState(); State<StatefulWidget> createState() => _GestureHelpState(touchMode);
} }
class _GestureHelpState extends State<GestureHelp> { class _GestureHelpState extends State<GestureHelp> {
var _selectedIndex; late int _selectedIndex;
var _touchMode; late bool _touchMode;
@override _GestureHelpState(bool touchMode) {
void initState() { _touchMode = touchMode;
_touchMode = widget.touchMode;
_selectedIndex = _touchMode ? 1 : 0; _selectedIndex = _touchMode ? 1 : 0;
super.initState();
} }
@override @override

View File

@@ -17,17 +17,17 @@ import '../common.dart';
final syncAbOption = 'sync-ab-with-recent-sessions'; final syncAbOption = 'sync-ab-with-recent-sessions';
bool shouldSyncAb() { bool shouldSyncAb() {
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty; return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
} }
final sortAbTagsOption = 'sync-ab-tags'; final sortAbTagsOption = 'sync-ab-tags';
bool shouldSortTags() { bool shouldSortTags() {
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty; return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
} }
final filterAbTagOption = 'filter-ab-by-intersection'; final filterAbTagOption = 'filter-ab-by-intersection';
bool filterAbTagByIntersection() { bool filterAbTagByIntersection() {
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty; return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
} }
const _personalAddressBookName = "My address book"; const _personalAddressBookName = "My address book";
@@ -111,9 +111,10 @@ class AbModel {
Future<void> _pullAb( Future<void> _pullAb(
{required ForcePullAb? force, required bool quiet}) async { {required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return; if (bind.isDisableAb()) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!gFFI.userModel.isLogin) return; if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (force == null && listInitialized && current.initialized) return; if (force == null && listInitialized && current.initialized) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!listInitialized || force == ForcePullAb.listAndCurrent) { if (!listInitialized || force == ForcePullAb.listAndCurrent) {
try { try {
// Read personal guid every time to avoid upgrading the server without closing the main window // Read personal guid every time to avoid upgrading the server without closing the main window
@@ -815,8 +816,6 @@ abstract class BaseAb {
} }
class LegacyAb extends BaseAb { class LegacyAb extends BaseAb {
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
bool get emtpy => peers.isEmpty && tags.isEmpty; bool get emtpy => peers.isEmpty && tags.isEmpty;
// licensedDevices is obtained from personal ab, shared ab restrict it in server // licensedDevices is obtained from personal ab, shared ab restrict it in server
var licensedDevices = 0; var licensedDevices = 0;
@@ -1209,8 +1208,6 @@ class LegacyAb extends BaseAb {
class Ab extends BaseAb { class Ab extends BaseAb {
AbProfile profile; AbProfile profile;
late final bool personal; late final bool personal;
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
bool get emtpy => peers.isEmpty && tags.isEmpty; bool get emtpy => peers.isEmpty && tags.isEmpty;
Ab(this.profile, this.personal); Ab(this.profile, this.personal);

View File

@@ -33,6 +33,8 @@ class CmFileModel {
_onFileRemove(evt['remove']); _onFileRemove(evt['remove']);
} else if (evt['create_dir'] != null) { } else if (evt['create_dir'] != null) {
_onDirCreate(evt['create_dir']); _onDirCreate(evt['create_dir']);
} else if (evt['rename'] != null) {
_onRename(evt['rename']);
} }
} }
@@ -59,8 +61,6 @@ class CmFileModel {
_dealOneJob(dynamic l, bool calcSpeed) { _dealOneJob(dynamic l, bool calcSpeed) {
final data = TransferJobSerdeData.fromJson(l); final data = TransferJobSerdeData.fromJson(l);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId]; var jobTable = _jobTables[data.connId];
if (jobTable == null) { if (jobTable == null) {
debugPrint("jobTable should not be null"); debugPrint("jobTable should not be null");
@@ -70,12 +70,7 @@ class CmFileModel {
if (job == null) { if (job == null) {
job = CmFileLog(); job = CmFileLog();
jobTable.add(job); jobTable.add(job);
final currentSelectedTab = _addUnread(data.connId);
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
} }
job.id = data.id; job.id = data.id;
job.action = job.action =
@@ -167,8 +162,6 @@ class CmFileModel {
try { try {
dynamic d = jsonDecode(log); dynamic d = jsonDecode(log);
FileActionLog data = FileActionLog.fromJson(d); FileActionLog data = FileActionLog.fromJson(d);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId]; var jobTable = _jobTables[data.connId];
if (jobTable == null) { if (jobTable == null) {
debugPrint("jobTable should not be null"); debugPrint("jobTable should not be null");
@@ -179,17 +172,45 @@ class CmFileModel {
..fileName = data.path ..fileName = data.path
..action = CmFileAction.createDir ..action = CmFileAction.createDir
..state = JobState.done); ..state = JobState.done);
final currentSelectedTab = _addUnread(data.connId);
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
jobTable.refresh(); jobTable.refresh();
} catch (e) { } catch (e) {
debugPrint('$e'); debugPrint('$e');
} }
} }
_onRename(dynamic log) {
try {
dynamic d = jsonDecode(log);
FileRenamenLog data = FileRenamenLog.fromJson(d);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
return;
}
final fileName = '${data.path} -> ${data.newName}';
jobTable.add(CmFileLog()
..id = 0
..fileName = fileName
..action = CmFileAction.rename
..state = JobState.done);
_addUnread(data.connId);
jobTable.refresh();
} catch (e) {
debugPrint('$e');
}
}
_addUnread(int connId) {
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
}
} }
enum CmFileAction { enum CmFileAction {
@@ -198,6 +219,7 @@ enum CmFileAction {
localToRemote, localToRemote,
remove, remove,
createDir, createDir,
rename,
} }
class CmFileLog { class CmFileLog {
@@ -285,3 +307,22 @@ class FileActionLog {
dir: d['dir'] ?? false, dir: d['dir'] ?? false,
); );
} }
class FileRenamenLog {
int connId = 0;
String path = '';
String newName = '';
FileRenamenLog({
required this.connId,
required this.path,
required this.newName,
});
FileRenamenLog.fromJson(dynamic d)
: this(
connId: d['connId'] ?? 0,
path: d['path'] ?? '',
newName: d['newName'] ?? '',
);
}

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:flutter_hbb/utils/event_loop.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@@ -642,6 +643,77 @@ class FileController {
path: path, path: path,
isRemote: !isLocal); isRemote: !isLocal);
} }
Future<void> renameAction(Entry item, bool isLocal) async {
final textEditingController = TextEditingController(text: item.name);
String? errorText;
dialogManager?.show((setState, close, context) {
textEditingController.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() async {
final newName = textEditingController.text;
if (newName.isEmpty || newName == item.name) {
close();
return;
}
if (directory.value.entries.any((e) => e.name == newName)) {
setState(() {
errorText = translate("Already exists");
});
return;
}
if (!PathUtil.validName(newName, options.value.isWindows)) {
setState(() {
if (item.isDirectory) {
errorText = translate("Invalid folder name");
} else {
errorText = translate("Invalid file name");
}
});
return;
}
await bind.sessionRenameFile(
sessionId: sessionId,
actId: JobController.jobID.next(),
path: item.path,
newName: newName,
isRemote: !isLocal);
close();
}
return CustomAlertDialog(
content: Column(
children: [
DialogTextField(
title: '${translate('Rename')} ${item.name}',
controller: textEditingController,
errorText: errorText,
),
],
),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
],
onSubmit: submit,
onCancel: close,
);
});
}
} }
class JobController { class JobController {
@@ -1083,6 +1155,13 @@ class PathUtil {
final pathUtil = isWindows ? windowsContext : posixContext; final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.dirname(path); return pathUtil.dirname(path);
} }
static bool validName(String name, bool isWindows) {
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
return reg.hasMatch(name);
}
} }
class DirectoryOptions { class DirectoryOptions {

View File

@@ -28,6 +28,7 @@ class GroupModel {
Future<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
if (bind.isDisableGroupPanel()) return; if (bind.isDisableGroupPanel()) return;
if (!gFFI.userModel.isLogin || groupLoading.value) return; if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (!force && initialized) return; if (!force && initialized) return;
if (!quiet) { if (!quiet) {
groupLoading.value = true; groupLoading.value = true;

View File

@@ -1152,4 +1152,27 @@ class InputModel {
platformFFI.stopDesktopWebListener(); platformFFI.stopDesktopWebListener();
} }
} }
void onMobileBack() => tap(MouseButtons.right);
void onMobileHome() => tap(MouseButtons.wheel);
Future<void> onMobileApps() async {
sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
sendMouse('up', MouseButtons.wheel);
}
// Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async {
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
await Future.delayed(Duration(milliseconds: 100));
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
}
Future<void> onMobileVolumeUp() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
Future<void> onMobileVolumeDown() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
Future<void> onMobilePower() async =>
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
} }

View File

@@ -192,10 +192,10 @@ class FfiModel with ChangeNotifier {
_permissions[k] = v == 'true'; _permissions[k] = v == 'true';
}); });
// Only inited at remote page // Only inited at remote page
if (desktopType == DesktopType.remote) { if (parent.target?.connType == ConnType.defaultConn) {
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false; KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
} }
debugPrint('$_permissions'); debugPrint('updatePermission: $_permissions');
notifyListeners(); notifyListeners();
} }
@@ -384,7 +384,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'use_texture_render') { } else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId); _handleUseTextureRender(evt, sessionId, peerId);
} else { } else {
debugPrint('Unknown event name: $name'); debugPrint('Event is not handled in the fixed branch: $name');
} }
}; };
} }
@@ -438,20 +438,6 @@ class FfiModel with ChangeNotifier {
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) { _handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
final running = evt['running'] == 'true'; final running = evt['running'] == 'true';
parent.target?.elevationModel.onPortableServiceRunning(running); parent.target?.elevationModel.onPortableServiceRunning(running);
if (running) {
if (pi.primaryDisplay != kInvalidDisplayIndex) {
if (pi.currentDisplay != pi.primaryDisplay) {
// Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'elevated_switch_display_msg', '', parent.target!.dialogManager);
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([pi.primaryDisplay]),
);
}
}
}
} }
handleAliasChanged(Map<String, dynamic> evt) { handleAliasChanged(Map<String, dynamic> evt) {
@@ -737,6 +723,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, bool isCache) async { handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
// This call is to ensuer the keyboard mode is updated depending on the peer version. // This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode(); parent.target?.inputModel.updateKeyboardMode();
@@ -1015,14 +1003,15 @@ class FfiModel with ChangeNotifier {
// Notify to switch display // Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt', msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'display_is_plugged_out_msg', '', parent.target!.dialogManager); 'display_is_plugged_out_msg', '', parent.target!.dialogManager);
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex final isPeerPrimaryDisplayValid =
? 0 pi.primaryDisplay == kInvalidDisplayIndex ||
: pi.primaryDisplay; pi.primaryDisplay >= pi.displays.length;
final displays = newDisplay; final newDisplay =
isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay;
bind.sessionSwitchDisplay( bind.sessionSwitchDisplay(
isDesktop: isDesktop, isDesktop: isDesktop,
sessionId: sessionId, sessionId: sessionId,
value: Int32List.fromList([displays]), value: Int32List.fromList([newDisplay]),
); );
if (_pi.isSupportMultiUiSession) { if (_pi.isSupportMultiUiSession) {
@@ -1187,26 +1176,28 @@ class ImageModel with ChangeNotifier {
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(int display, Uint8List rgba) { clearImage() => _image = null;
onRgba(int display, Uint8List rgba) async {
try {
await decodeAndUpdate(display, rgba);
} catch (e) {
debugPrint('onRgba error: $e');
}
platformFFI.nextRgba(sessionId, display);
}
decodeAndUpdate(int display, Uint8List rgba) async {
final pid = parent.target?.id; final pid = parent.target?.id;
final rect = parent.target?.ffiModel.pi.getDisplayRect(display); final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
img.decodeImageFromPixels( final image = await img.decodeImageFromPixels(
rgba, rgba,
rect?.width.toInt() ?? 0, rect?.width.toInt() ?? 0,
rect?.height.toInt() ?? 0, rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () { );
// Unlock the rgba memory from rust codes. if (parent.target?.id != pid) return;
platformFFI.nextRgba(sessionId, display); await update(image);
}).then((image) {
if (parent.target?.id != pid) return;
try {
// my throw exception, because the listener maybe already dispose
update(image);
} catch (e) {
debugPrint('update image: $e');
}
});
} }
update(ui.Image? image) async { update(ui.Image? image) async {
@@ -1740,7 +1731,7 @@ class PredefinedCursor {
_image2 = img2.decodePng(base64Decode(png)); _image2 = img2.decodePng(base64Decode(png));
if (_image2 != null) { if (_image2 != null) {
// The png type of forbidden cursor image is `PngColorType.indexed`. // The png type of forbidden cursor image is `PngColorType.indexed`.
if (isWindows && id == kPreForbiddenCursorId) { if (id == kPreForbiddenCursorId) {
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4); _image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
} }
@@ -2561,32 +2552,30 @@ class FFI {
} }
} else if (message is EventToUI_Rgba) { } else if (message is EventToUI_Rgba) {
final display = message.field0; final display = message.field0;
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) { // Fetch the image buffer from rust codes.
//debugPrint("EventToUI_Rgba display:$display"); final sz = platformFFI.getRgbaSize(sessionId, display);
textureModel.setTextureType(display: display, gpuTexture: false); if (sz == 0) {
platformFFI.nextRgba(sessionId, display);
return;
}
final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) {
onEvent2UIRgba(); onEvent2UIRgba();
await imageModel.onRgba(display, rgba);
} else { } else {
// Fetch the image buffer from rust codes. platformFFI.nextRgba(sessionId, display);
final sz = platformFFI.getRgbaSize(sessionId, display);
if (sz == 0) {
platformFFI.nextRgba(sessionId, display);
return;
}
final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) {
onEvent2UIRgba();
imageModel.onRgba(display, rgba);
} else {
platformFFI.nextRgba(sessionId, display);
}
} }
} else if (message is EventToUI_Texture) { } else if (message is EventToUI_Texture) {
final display = message.field0; final display = message.field0;
debugPrint("EventToUI_Texture display:$display"); final gpuTexture = message.field1;
if (hasGpuTextureRender) { debugPrint(
textureModel.setTextureType(display: display, gpuTexture: true); "EventToUI_Texture display:$display, gpuTexture:$gpuTexture");
onEvent2UIRgba(); if (gpuTexture && !hasGpuTextureRender) {
debugPrint('the gpuTexture is not supported.');
return;
} }
textureModel.setTextureType(display: display, gpuTexture: gpuTexture);
onEvent2UIRgba();
} }
}(); }();
}); });
@@ -2622,8 +2611,9 @@ class FFI {
remember: remember); remember: remember);
} }
void send2FA(SessionID sessionId, String code) { void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
bind.sessionSend2Fa(sessionId: sessionId, code: code); bind.sessionSend2Fa(
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
} }
/// Close the remote session. /// Close the remote session.
@@ -2640,7 +2630,7 @@ class FFI {
canvasModel.scale, canvasModel.scale,
ffiModel.pi.currentDisplay); ffiModel.pi.currentDisplay);
} }
imageModel.update(null); await imageModel.update(null);
cursorModel.clear(); cursorModel.clear();
ffiModel.clear(); ffiModel.clear();
canvasModel.clear(); canvasModel.clear();

View File

@@ -117,9 +117,13 @@ class PlatformFFI {
? DynamicLibrary.open('librustdesk.so') ? DynamicLibrary.open('librustdesk.so')
: isWindows : isWindows
? DynamicLibrary.open('librustdesk.dll') ? DynamicLibrary.open('librustdesk.dll')
: isMacOS :
? DynamicLibrary.open("liblibrustdesk.dylib") // Use executable itself as the dynamic library for MacOS.
: DynamicLibrary.process(); // Multiple dylib instances will cause some global instances to be invalid.
// eg. `lazy_static` objects in rust side, will be created more than once, which is not expected.
//
// isMacOS? DynamicLibrary.open("liblibrustdesk.dylib") :
DynamicLibrary.process();
debugPrint('initializing FFI $_appType'); debugPrint('initializing FFI $_appType');
try { try {
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba"); _session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");

View File

@@ -177,6 +177,11 @@ class ServerModel with ChangeNotifier {
await timerCallback(); await timerCallback();
}); });
} }
// Initial keyboard status is off on mobile
if (isMobile) {
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
}
} }
/// 1. check android permission /// 1. check android permission
@@ -191,7 +196,7 @@ class ServerModel with ChangeNotifier {
bind.mainSetOption(key: kOptionEnableAudio, value: "N"); bind.mainSetOption(key: kOptionEnableAudio, value: "N");
} else { } else {
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio); final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
_audioOk = audioOption.isEmpty; _audioOk = audioOption != 'N';
} }
// file // file
@@ -201,7 +206,7 @@ class ServerModel with ChangeNotifier {
} else { } else {
final fileOption = final fileOption =
await bind.mainGetOption(key: kOptionEnableFileTransfer); await bind.mainGetOption(key: kOptionEnableFileTransfer);
_fileOk = fileOption.isEmpty; _fileOk = fileOption != 'N';
} }
notifyListeners(); notifyListeners();

View File

@@ -14,7 +14,7 @@ class StateGlobal {
bool _isMinimized = false; bool _isMinimized = false;
final RxBool isMaximized = false.obs; final RxBool isMaximized = false.obs;
final RxBool _showTabBar = true.obs; final RxBool _showTabBar = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize); final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteToolBar = false.obs; final RxBool showRemoteToolBar = false.obs;
final svcStatus = SvcStatus.notReady.obs; final svcStatus = SvcStatus.notReady.obs;
@@ -93,7 +93,7 @@ class StateGlobal {
? kFullScreenEdgeSize ? kFullScreenEdgeSize
: isMaximized.isTrue : isMaximized.isTrue
? kMaximizeEdgeSize ? kMaximizeEdgeSize
: windowEdgeSize; : windowResizeEdgeSize;
String getInputSource({bool force = false}) { String getInputSource({bool force = false}) {
if (force || _inputSource.isEmpty) { if (force || _inputSource.isEmpty) {

View File

@@ -17,13 +17,23 @@ bool refreshingUser = false;
class UserModel { class UserModel {
final RxString userName = ''.obs; final RxString userName = ''.obs;
final RxBool isAdmin = false.obs; final RxBool isAdmin = false.obs;
final RxString networkError = ''.obs;
bool get isLogin => userName.isNotEmpty; bool get isLogin => userName.isNotEmpty;
WeakReference<FFI> parent; WeakReference<FFI> parent;
UserModel(this.parent); UserModel(this.parent) {
userName.listen((p0) {
// When user name becomes empty, show login button
// When user name becomes non-empty:
// For _updateLocalUserInfo, network error will be set later
// For login success, should clear network error
networkError.value = '';
});
}
void refreshCurrentUser() async { void refreshCurrentUser() async {
if (bind.isDisableAccount()) return; if (bind.isDisableAccount()) return;
networkError.value = '';
final token = bind.mainGetLocalOption(key: 'access_token'); final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') { if (token == '') {
await updateOtherModels(); await updateOtherModels();
@@ -38,12 +48,18 @@ class UserModel {
if (refreshingUser) return; if (refreshingUser) return;
try { try {
refreshingUser = true; refreshingUser = true;
final response = await http.post(Uri.parse('$url/api/currentUser'), final http.Response response;
headers: { try {
'Content-Type': 'application/json', response = await http.post(Uri.parse('$url/api/currentUser'),
'Authorization': 'Bearer $token' headers: {
}, 'Content-Type': 'application/json',
body: json.encode(body)); 'Authorization': 'Bearer $token'
},
body: json.encode(body));
} catch (e) {
networkError.value = e.toString();
rethrow;
}
refreshingUser = false; refreshingUser = false;
final status = response.statusCode; final status = response.statusCode;
if (status == 401 || status == 400) { if (status == 401 || status == 400) {

View File

@@ -13,14 +13,12 @@ Future<ui.Image?> decodeImageFromPixels(
int? rowBytes, int? rowBytes,
int? targetWidth, int? targetWidth,
int? targetHeight, int? targetHeight,
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
bool allowUpscaling = true, bool allowUpscaling = true,
}) async { }) async {
if (targetWidth != null) { if (targetWidth != null) {
assert(allowUpscaling || targetWidth <= width); assert(allowUpscaling || targetWidth <= width);
if (!(allowUpscaling || targetWidth <= width)) { if (!(allowUpscaling || targetWidth <= width)) {
print("not allow upscaling but targetWidth > width"); print("not allow upscaling but targetWidth > width");
onPixelsCopied?.call();
return null; return null;
} }
} }
@@ -28,7 +26,6 @@ Future<ui.Image?> decodeImageFromPixels(
assert(allowUpscaling || targetHeight <= height); assert(allowUpscaling || targetHeight <= height);
if (!(allowUpscaling || targetHeight <= height)) { if (!(allowUpscaling || targetHeight <= height)) {
print("not allow upscaling but targetHeight > height"); print("not allow upscaling but targetHeight > height");
onPixelsCopied?.call();
return null; return null;
} }
} }
@@ -36,9 +33,7 @@ Future<ui.Image?> decodeImageFromPixels(
final ui.ImmutableBuffer buffer; final ui.ImmutableBuffer buffer;
try { try {
buffer = await ui.ImmutableBuffer.fromUint8List(pixels); buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
onPixelsCopied?.call();
} catch (e) { } catch (e) {
onPixelsCopied?.call();
return null; return null;
} }

View File

@@ -142,7 +142,10 @@ class RustdeskImpl {
} }
Future<void> sessionSend2Fa( Future<void> sessionSend2Fa(
{required UuidValue sessionId, required String code, dynamic hint}) { {required UuidValue sessionId,
required String code,
required bool trustThisDevice,
dynamic hint}) {
return Future(() => js.context.callMethod('setByName', ['send_2fa', code])); return Future(() => js.context.callMethod('setByName', ['send_2fa', code]));
} }
@@ -1412,7 +1415,7 @@ class RustdeskImpl {
return false; return false;
} }
bool mainHideDocker({dynamic hint}) { bool mainHideDock({dynamic hint}) {
throw UnimplementedError(); throw UnimplementedError();
} }
@@ -1614,5 +1617,38 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
bool mainHasValidBotSync({dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainVerifyBot({required String token, dynamic hint}) {
throw UnimplementedError();
}
String mainGetUnlockPin({dynamic hint}) {
throw UnimplementedError();
}
String mainSetUnlockPin({required String pin, dynamic hint}) {
throw UnimplementedError();
}
bool sessionGetEnableTrustedDevices(
{required UuidValue sessionId, dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainGetTrustedDevices({dynamic hint}) {
throw UnimplementedError();
}
Future<void> mainRemoveTrustedDevices({required String json, dynamic hint}) {
throw UnimplementedError();
}
Future<void> mainClearTrustedDevices({dynamic hint}) {
throw UnimplementedError();
}
void dispose() {} void dispose() {}
} }

View File

@@ -45,5 +45,7 @@
<string>Record the sound from microphone for the purpose of the remote desktop.</string> <string>Record the sound from microphone for the purpose of the remote desktop.</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
</dict> </dict>
</plist> </plist>

2
flutter/ndk_x86.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter

View File

@@ -335,7 +335,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3" resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
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"
@@ -377,7 +377,7 @@ packages:
path: "." path: "."
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
url: "https://github.com/21pages/dynamic_layouts.git" url: "https://github.com/rustdesk-org/dynamic_layouts.git"
source: git source: git
version: "0.0.1+1" version: "0.0.1+1"
external_path: external_path:
@@ -511,7 +511,7 @@ packages:
path: "." path: "."
ref: "38951317afe79d953ab25733667bd96e172a80d3" ref: "38951317afe79d953ab25733667bd96e172a80d3"
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
url: "https://github.com/21pages/flutter_gpu_texture_renderer" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git source: git
version: "0.0.1" version: "0.0.1"
flutter_improved_scrolling: flutter_improved_scrolling:

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.6+44 version: 1.3.0+46
environment: environment:
sdk: '^3.1.0' sdk: '^3.1.0'
@@ -79,7 +79,7 @@ dependencies:
git: git:
url: https://github.com/rustdesk-org/flutter_improved_scrolling url: https://github.com/rustdesk-org/flutter_improved_scrolling
uni_links: ^0.5.1 uni_links: ^0.5.1
uni_links_desktop: ^0.1.7 uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
path: ^1.8.1 path: ^1.8.1
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
bot_toast: ^4.0.3 bot_toast: ^4.0.3
@@ -92,14 +92,14 @@ dependencies:
dropdown_button2: ^2.0.0 dropdown_button2: ^2.0.0
flutter_gpu_texture_renderer: flutter_gpu_texture_renderer:
git: git:
url: https://github.com/21pages/flutter_gpu_texture_renderer url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 38951317afe79d953ab25733667bd96e172a80d3 ref: 38951317afe79d953ab25733667bd96e172a80d3
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 flex_color_picker: ^3.3.0
dynamic_layouts: dynamic_layouts:
git: git:
url: https://github.com/21pages/dynamic_layouts.git url: https://github.com/rustdesk-org/dynamic_layouts.git
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9 ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3 pull_down_button: ^0.9.3
device_info_plus: ^9.1.0 device_info_plus: ^9.1.0

View File

@@ -5,6 +5,9 @@
#include "resource.h" #include "resource.h"
#include <cstdlib> // for getenv and _putenv
#include <cstring> // for strcmp
namespace { namespace {
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
@@ -143,6 +146,25 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
return OnCreate(); return OnCreate();
} }
static void trySetWindowForeground(HWND window) {
char* value = nullptr;
size_t size = 0;
// Use _dupenv_s to safely get the environment variable
_dupenv_s(&value, &size, "SET_FOREGROUND_WINDOW");
if (value != nullptr) {
// Correctly compare the value with "1"
if (strcmp(value, "1") == 0) {
// Clear the environment variable
_putenv("SET_FOREGROUND_WINDOW=");
// Set the window to foreground
SetForegroundWindow(window);
}
// Free the duplicated string
free(value);
}
}
// static // static
LRESULT CALLBACK Win32Window::WndProc(HWND const window, LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message, UINT const message,
@@ -156,6 +178,7 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams); auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window); EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window; that->window_handle_ = window;
trySetWindowForeground(window);
} else if (Win32Window* that = GetThisFromHandle(window)) { } else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam); return that->MessageHandler(window, message, wparam, lparam);
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 481 KiB

View File

@@ -795,7 +795,11 @@ impl FuseNode {
conn_id: desc.conn_id, conn_id: desc.conn_id,
stream_id: rand::random(), stream_id: rand::random(),
index: inode as usize - 2, index: inode as usize - 2,
name: desc.name.to_str().unwrap().to_owned(), name: desc
.name
.to_str()
.map(|s| s.to_string())
.unwrap_or_default(),
parent: None, parent: None,
attributes: InodeAttributes::from_description(inode, desc), attributes: InodeAttributes::from_description(inode, desc),
children: Vec::new(), children: Vec::new(),
@@ -1140,7 +1144,7 @@ mod fuse_test {
} }
fn build_single_file(prefix: &str) { fn build_single_file(prefix: &str) {
let raw_name = "衬衫的价格为 9 镑 15 便士.txt"; let raw_name = "simple_test_file.txt";
let f_name = if prefix == "" { let f_name = if prefix == "" {
raw_name.to_string() raw_name.to_string()
} else { } else {

View File

@@ -1,3 +1,4 @@
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::{CliprdrError, CliprdrServiceContext}; use crate::{CliprdrError, CliprdrServiceContext};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -52,9 +53,9 @@ pub fn create_cliprdr_context(
log::warn!("umount {:?} may fail: {:?}", mnt_path, e); log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
} }
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse().unwrap())?; let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?;
log::debug!("start cliprdr FUSE"); log::debug!("start cliprdr FUSE");
unix_ctx.run().expect("failed to start cliprdr FUSE"); unix_ctx.run()?;
Ok(Box::new(unix_ctx) as Box<_>) Ok(Box::new(unix_ctx) as Box<_>)
} }
@@ -63,8 +64,10 @@ pub fn create_cliprdr_context(
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
} }
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct DummyCliprdrContext {} struct DummyCliprdrContext {}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl CliprdrServiceContext for DummyCliprdrContext { impl CliprdrServiceContext for DummyCliprdrContext {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
Ok(()) Ok(())

View File

@@ -113,7 +113,7 @@ impl LocalFile {
let win32_time = self let win32_time = self
.last_write_time .last_write_time
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap_or_default()
.as_nanos() as u64 .as_nanos() as u64
/ 100 / 100
+ LDAP_EPOCH_DELTA; + LDAP_EPOCH_DELTA;
@@ -188,7 +188,7 @@ impl LocalFile {
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> { pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
self.load_handle()?; self.load_handle()?;
let handle = self.handle.as_mut().unwrap(); let handle = self.handle.as_mut()?;
if offset != self.offset.load(Ordering::Relaxed) { if offset != self.offset.load(Ordering::Relaxed) {
handle handle
@@ -238,9 +238,9 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
})?; })?;
if mt.is_dir() { if mt.is_dir() {
let dir = std::fs::read_dir(path).unwrap(); let dir = std::fs::read_dir(path)?;
for entry in dir { for entry in dir {
let entry = entry.unwrap(); let entry = entry?;
let path = entry.path(); let path = entry.path();
constr_file_lst(&path, file_list, visited)?; constr_file_lst(&path, file_list, visited)?;
} }

View File

@@ -383,13 +383,11 @@ impl ClipboardContext {
let file_contents_id = fmt_lst let file_contents_id = fmt_lst
.iter() .iter()
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
.map(|(id, _)| *id) .map(|(id, _)| *id)?;
.unwrap();
let file_descriptor_id = fmt_lst let file_descriptor_id = fmt_lst
.iter() .iter()
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
.map(|(id, _)| *id) .map(|(id, _)| *id)?;
.unwrap();
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id); add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id); add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);

View File

@@ -7,9 +7,9 @@ use crate::CliprdrError;
// url encode and decode is needed // url encode and decode is needed
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/'); const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
pub(super) fn encode_path_to_uri(path: &PathBuf) -> String { pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result<String> {
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET) let encoded =
.to_string(); percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
format!("file://{}", encoded) format!("file://{}", encoded)
} }
@@ -54,7 +54,7 @@ mod uri_test {
#[test] #[test]
fn test_conversion() { fn test_conversion() {
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png"); let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
let uri = super::encode_path_to_uri(&path); let uri = super::encode_path_to_uri(&path).unwrap();
assert_eq!( assert_eq!(
uri, uri,
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"

View File

@@ -89,7 +89,13 @@ impl SysClipboard for X11Clipboard {
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
*self.former_file_list.lock() = paths.to_vec(); *self.former_file_list.lock() = paths.to_vec();
let uri_list: Vec<String> = paths.iter().map(encode_path_to_uri).collect(); let uri_list: Vec<String> = {
let mut v = Vec::new();
for path in paths {
v.push(encode_path_to_uri(path)?);
}
v
};
let uri_list = uri_list.join("\n"); let uri_list = uri_list.join("\n");
let text_uri_list_data = uri_list.as_bytes().to_vec(); let text_uri_list_data = uri_list.as_bytes().to_vec();
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();

View File

@@ -5,16 +5,16 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![allow(deref_nullptr)] #![allow(deref_nullptr)]
use std::{
boxed::Box,
ffi::{CStr, CString},
result::Result,
};
use crate::{ use crate::{
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
}; };
use hbb_common::log; use hbb_common::log;
use std::{
boxed::Box,
ffi::{CStr, CString},
result::Result,
};
// only used error code will be recorded here // only used error code will be recorded here
/// success /// success
@@ -779,7 +779,7 @@ pub fn server_format_list(
} else { } else {
let n = match CString::new(format.1) { let n = match CString::new(format.1) {
Ok(n) => n, Ok(n) => n,
Err(_) => CString::new("").unwrap(), Err(_) => CString::new("").unwrap_or_default(),
}; };
CLIPRDR_FORMAT { CLIPRDR_FORMAT {
formatId: format.0 as UINT32, formatId: format.0 as UINT32,

View File

@@ -22,8 +22,8 @@ appveyor = { repository = "pythoneer/enigo-85xiy" }
serde = { version = "1.0", optional = true } serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true }
log = "0.4" log = "0.4"
rdev = { git = "https://github.com/fufesou/rdev" } rdev = { git = "https://github.com/rustdesk-org/rdev" }
tfc = { git = "https://github.com/fufesou/The-Fat-Controller" } tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller", branch = "history/rebase_upstream_20240722" }
hbb_common = { path = "../hbb_common" } hbb_common = { path = "../hbb_common" }
[features] [features]

View File

@@ -154,8 +154,8 @@ impl MouseControllable for Enigo {
} }
}, },
match button { match button {
MouseButton::Back => XBUTTON1 as u32 * WHEEL_DELTA as u32, MouseButton::Back => XBUTTON1 as u32,
MouseButton::Forward => XBUTTON2 as u32 * WHEEL_DELTA as u32, MouseButton::Forward => XBUTTON2 as u32,
_ => 0, _ => 0,
}, },
0, 0,

View File

@@ -37,8 +37,8 @@ libc = "0.2"
dlopen = "0.1" dlopen = "0.1"
toml = "0.7" toml = "0.7"
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052 # new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" } sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" }
thiserror = "1.0" thiserror = "1.0"
httparse = "1.5" httparse = "1.5"
base64 = "0.22" base64 = "0.22"
@@ -46,7 +46,7 @@ url = "2.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1" mac_address = "1.1"
machine-uid = { git = "https://github.com/21pages/machine-uid" } machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false } tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
rustls-platform-verifier = "0.3.1" rustls-platform-verifier = "0.3.1"
@@ -58,7 +58,7 @@ tokio-native-tls ="0.3"
protobuf-codegen = { version = "3.4" } protobuf-codegen = { version = "3.4" }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi"] } winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
osascript = "0.3" osascript = "0.3"

View File

@@ -81,10 +81,13 @@ message LoginRequest {
uint64 session_id = 10; uint64 session_id = 10;
string version = 11; string version = 11;
OSLogin os_login = 12; OSLogin os_login = 12;
string my_platform = 13;
bytes hwid = 14;
} }
message Auth2FA { message Auth2FA {
string code = 1; string code = 1;
bytes hwid = 2;
} }
message ChatMessage { string text = 1; } message ChatMessage { string text = 1; }
@@ -136,6 +139,7 @@ message LoginResponse {
string error = 1; string error = 1;
PeerInfo peer_info = 2; PeerInfo peer_info = 2;
} }
bool enable_trusted_devices = 3;
} }
message TouchScaleUpdate { message TouchScaleUpdate {
@@ -271,6 +275,10 @@ enum ControlKey {
RShift = 73; RShift = 73;
RControl = 74; RControl = 74;
RAlt = 75; RAlt = 75;
VolumeMute = 76; // mainly used on mobile devices as controlled side
VolumeUp = 77;
VolumeDown = 78;
Power = 79; // mainly used on mobile devices as controlled side
CtrlAltDel = 100; CtrlAltDel = 100;
LockScreen = 101; LockScreen = 101;
} }
@@ -311,11 +319,25 @@ message Hash {
string challenge = 2; string challenge = 2;
} }
enum ClipboardFormat {
Text = 0;
Rtf = 1;
Html = 2;
ImageRgba = 21;
ImagePng = 22;
ImageSvg = 23;
}
message Clipboard { message Clipboard {
bool compress = 1; bool compress = 1;
bytes content = 2; bytes content = 2;
int32 width = 3;
int32 height = 4;
ClipboardFormat format = 5;
} }
message MultiClipboards { repeated Clipboard clipboards = 1; }
enum FileType { enum FileType {
Dir = 0; Dir = 0;
DirLink = 2; DirLink = 2;
@@ -349,6 +371,12 @@ message ReadAllFiles {
bool include_hidden = 3; bool include_hidden = 3;
} }
message FileRename {
int32 id = 1;
string path = 2;
string new_name = 3;
}
message FileAction { message FileAction {
oneof union { oneof union {
ReadDir read_dir = 1; ReadDir read_dir = 1;
@@ -360,6 +388,7 @@ message FileAction {
ReadAllFiles all_files = 7; ReadAllFiles all_files = 7;
FileTransferCancel cancel = 8; FileTransferCancel cancel = 8;
FileTransferSendConfirmRequest send_confirm = 9; FileTransferSendConfirmRequest send_confirm = 9;
FileRename rename = 10;
} }
} }
@@ -810,5 +839,6 @@ message Message {
PeerInfo peer_info = 25; PeerInfo peer_info = 25;
PointerDeviceEvent pointer_device_event = 26; PointerDeviceEvent pointer_device_event = 26;
Auth2FA auth_2fa = 27; Auth2FA auth_2fa = 27;
MultiClipboards multi_clipboards = 28;
} }
} }

View File

@@ -21,13 +21,13 @@ message PunchHoleRequest {
string licence_key = 3; string licence_key = 3;
ConnType conn_type = 4; ConnType conn_type = 4;
string token = 5; string token = 5;
string version = 6;
} }
message PunchHole { message PunchHole {
bytes socket_addr = 1; bytes socket_addr = 1;
string relay_server = 2; string relay_server = 2;
NatType nat_type = 3; NatType nat_type = 3;
string request_region = 4;
} }
message TestNatRequest { message TestNatRequest {
@@ -52,7 +52,6 @@ message PunchHoleSent {
string relay_server = 3; string relay_server = 3;
NatType nat_type = 4; NatType nat_type = 4;
string version = 5; string version = 5;
string request_region = 6;
} }
message RegisterPk { message RegisterPk {
@@ -92,6 +91,7 @@ message PunchHoleResponse {
bool is_local = 6; bool is_local = 6;
} }
string other_failure = 7; string other_failure = 7;
int32 feedback = 8;
} }
message ConfigUpdate { message ConfigUpdate {
@@ -108,7 +108,6 @@ message RequestRelay {
string licence_key = 6; string licence_key = 6;
ConnType conn_type = 7; ConnType conn_type = 7;
string token = 8; string token = 8;
string request_region = 9;
} }
message RelayResponse { message RelayResponse {
@@ -121,7 +120,7 @@ message RelayResponse {
} }
string refuse_reason = 6; string refuse_reason = 6;
string version = 7; string version = 7;
string request_region = 8; int32 feedback = 9;
} }
message SoftwareUpdate { string url = 1; } message SoftwareUpdate { string url = 1; }
@@ -133,7 +132,6 @@ message SoftwareUpdate { string url = 1; }
message FetchLocalAddr { message FetchLocalAddr {
bytes socket_addr = 1; bytes socket_addr = 1;
string relay_server = 2; string relay_server = 2;
string request_region = 3;
} }
message LocalAddr { message LocalAddr {
@@ -142,7 +140,6 @@ message LocalAddr {
string relay_server = 3; string relay_server = 3;
string id = 4; string id = 4;
string version = 5; string version = 5;
string request_region = 6;
} }
message PeerDiscovery { message PeerDiscovery {
@@ -168,6 +165,10 @@ message KeyExchange {
repeated bytes keys = 1; repeated bytes keys = 1;
} }
message HealthCheck {
string token = 1;
}
message RendezvousMessage { message RendezvousMessage {
oneof union { oneof union {
RegisterPeer register_peer = 6; RegisterPeer register_peer = 6;
@@ -190,5 +191,6 @@ message RendezvousMessage {
OnlineRequest online_request = 23; OnlineRequest online_request = 23;
OnlineResponse online_response = 24; OnlineResponse online_response = 24;
KeyExchange key_exchange = 25; KeyExchange key_exchange = 25;
HealthCheck hc = 26;
} }
} }

View File

@@ -1,5 +1,5 @@
use std::{cell::RefCell, io}; use std::{cell::RefCell, io};
use zstd::bulk::{Compressor, Decompressor}; use zstd::bulk::Compressor;
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), // The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
// which is currently 22. Levels >= 20 // which is currently 22. Levels >= 20
@@ -7,7 +7,6 @@ use zstd::bulk::{Compressor, Decompressor};
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT // value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
thread_local! { thread_local! {
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL)); static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
static DECOMPRESSOR: RefCell<io::Result<Decompressor<'static>>> = RefCell::new(Decompressor::new());
} }
pub fn compress(data: &[u8]) -> Vec<u8> { pub fn compress(data: &[u8]) -> Vec<u8> {
@@ -31,27 +30,5 @@ pub fn compress(data: &[u8]) -> Vec<u8> {
} }
pub fn decompress(data: &[u8]) -> Vec<u8> { pub fn decompress(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new(); zstd::decode_all(data).unwrap_or_default()
DECOMPRESSOR.with(|d| {
if let Ok(mut d) = d.try_borrow_mut() {
match &mut *d {
Ok(d) => {
const MAX: usize = 1024 * 1024 * 64;
const MIN: usize = 1024 * 1024;
let mut n = 30 * data.len();
n = n.clamp(MIN, MAX);
match d.decompress(data, n) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to decompress: {}", err);
}
}
}
Err(err) => {
crate::log::debug!("Failed to get decompressor: {}", err);
}
}
}
});
out
} }

View File

@@ -10,6 +10,7 @@ use std::{
}; };
use anyhow::Result; use anyhow::Result;
use bytes::Bytes;
use rand::Rng; use rand::Rng;
use regex::Regex; use regex::Regex;
use serde as de; use serde as de;
@@ -52,6 +53,7 @@ lazy_static::lazy_static! {
static ref CONFIG: RwLock<Config> = RwLock::new(Config::load()); static ref CONFIG: RwLock<Config> = RwLock::new(Config::load());
static ref CONFIG2: RwLock<Config2> = RwLock::new(Config2::load()); static ref CONFIG2: RwLock<Config2> = RwLock::new(Config2::load());
static ref LOCAL_CONFIG: RwLock<LocalConfig> = RwLock::new(LocalConfig::load()); static ref LOCAL_CONFIG: RwLock<LocalConfig> = RwLock::new(LocalConfig::load());
static ref TRUSTED_DEVICES: RwLock<(Vec<TrustedDevice>, bool)> = Default::default();
static ref ONLINE: Mutex<HashMap<String, i64>> = Default::default(); static ref ONLINE: Mutex<HashMap<String, i64>> = Default::default();
pub static ref PROD_RENDEZVOUS_SERVER: RwLock<String> = RwLock::new(match option_env!("RENDEZVOUS_SERVER") { pub static ref PROD_RENDEZVOUS_SERVER: RwLock<String> = RwLock::new(match option_env!("RENDEZVOUS_SERVER") {
Some(key) if !key.is_empty() => key, Some(key) if !key.is_empty() => key,
@@ -69,6 +71,7 @@ lazy_static::lazy_static! {
pub static ref DEFAULT_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default(); pub static ref DEFAULT_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default(); pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
pub static ref HARD_SETTINGS: RwLock<HashMap<String, String>> = Default::default(); pub static ref HARD_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
pub static ref BUILTIN_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
} }
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -207,6 +210,10 @@ pub struct Config2 {
nat_type: i32, nat_type: i32,
#[serde(default, deserialize_with = "deserialize_i32")] #[serde(default, deserialize_with = "deserialize_i32")]
serial: i32, serial: i32,
#[serde(default, deserialize_with = "deserialize_string")]
unlock_pin: String,
#[serde(default, deserialize_with = "deserialize_string")]
trusted_devices: String,
#[serde(default)] #[serde(default)]
socks: Option<Socks5Server>, socks: Option<Socks5Server>,
@@ -426,14 +433,20 @@ fn patch(path: PathBuf) -> PathBuf {
impl Config2 { impl Config2 {
fn load() -> Config2 { fn load() -> Config2 {
let mut config = Config::load_::<Config2>("2"); let mut config = Config::load_::<Config2>("2");
let mut store = false;
if let Some(mut socks) = config.socks { if let Some(mut socks) = config.socks {
let (password, _, store) = let (password, _, store2) =
decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
socks.password = password; socks.password = password;
config.socks = Some(socks); config.socks = Some(socks);
if store { store |= store2;
config.store(); }
} let (unlock_pin, _, store2) =
decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION);
config.unlock_pin = unlock_pin;
store |= store2;
if store {
config.store();
} }
config config
} }
@@ -449,6 +462,8 @@ impl Config2 {
encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
config.socks = Some(socks); config.socks = Some(socks);
} }
config.unlock_pin =
encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
Config::store_(&config, "2"); Config::store_(&config, "2");
} }
@@ -487,7 +502,19 @@ pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + s
#[inline] #[inline]
pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> { pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
Ok(confy::store_path(path, cfg)?) #[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
Ok(confy::store_path_perms(
path,
cfg,
fs::Permissions::from_mode(0o600),
)?)
}
#[cfg(windows)]
{
Ok(confy::store_path(path, cfg)?)
}
} }
impl Config { impl Config {
@@ -975,6 +1002,7 @@ impl Config {
} }
config.password = password.into(); config.password = password.into();
config.store(); config.store();
Self::clear_trusted_devices();
} }
pub fn get_permanent_password() -> String { pub fn get_permanent_password() -> String {
@@ -1068,6 +1096,77 @@ impl Config {
NetworkType::Direct NetworkType::Direct
} }
pub fn get_unlock_pin() -> String {
CONFIG2.read().unwrap().unlock_pin.clone()
}
pub fn set_unlock_pin(pin: &str) {
let mut config = CONFIG2.write().unwrap();
if pin == config.unlock_pin {
return;
}
config.unlock_pin = pin.to_string();
config.store();
}
pub fn get_trusted_devices_json() -> String {
serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default()
}
pub fn get_trusted_devices() -> Vec<TrustedDevice> {
let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone();
if synced {
return devices;
}
let devices = CONFIG2.read().unwrap().trusted_devices.clone();
let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION);
if succ {
let mut devices: Vec<TrustedDevice> =
serde_json::from_str(&devices).unwrap_or_default();
let len = devices.len();
devices.retain(|d| !d.outdate());
if store || devices.len() != len {
Self::set_trusted_devices(devices.clone());
}
*TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true);
devices
} else {
Default::default()
}
}
fn set_trusted_devices(mut trusted_devices: Vec<TrustedDevice>) {
trusted_devices.retain(|d| !d.outdate());
let devices = serde_json::to_string(&trusted_devices).unwrap_or_default();
let max_len = 1024 * 1024;
if devices.bytes().len() > max_len {
log::error!("Trusted devices too large: {}", devices.bytes().len());
return;
}
let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len);
let mut config = CONFIG2.write().unwrap();
config.trusted_devices = devices;
config.store();
*TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true);
}
pub fn add_trusted_device(device: TrustedDevice) {
let mut devices = Self::get_trusted_devices();
devices.retain(|d| d.hwid != device.hwid);
devices.push(device);
Self::set_trusted_devices(devices);
}
pub fn remove_trusted_devices(hwids: &Vec<Bytes>) {
let mut devices = Self::get_trusted_devices();
devices.retain(|d| !hwids.contains(&d.hwid));
Self::set_trusted_devices(devices);
}
pub fn clear_trusted_devices() {
Self::set_trusted_devices(Default::default());
}
pub fn get() -> Config { pub fn get() -> Config {
return CONFIG.read().unwrap().clone(); return CONFIG.read().unwrap().clone();
} }
@@ -1898,6 +1997,22 @@ impl Group {
} }
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct TrustedDevice {
pub hwid: Bytes,
pub time: i64,
pub id: String,
pub name: String,
pub platform: String,
}
impl TrustedDevice {
pub fn outdate(&self) -> bool {
const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000;
self.time + DAYS_90 < crate::get_time()
}
}
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);
@@ -2087,6 +2202,22 @@ pub mod keys {
pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture"; pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture";
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
"enable-android-software-encoding-half-scale"; "enable-android-software-encoding-half-scale";
pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices";
// buildin options
pub const OPTION_DISPLAY_NAME: &str = "display-name";
pub const OPTION_DISABLE_UDP: &str = "disable-udp";
pub const OPTION_PRESET_USERNAME: &str = "preset-user-name";
pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name";
pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning";
pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings";
pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings";
pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings";
pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings";
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
pub const OPTION_HIDE_TRAY: &str = "hide-tray";
// flutter local options // flutter local options
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
@@ -2096,6 +2227,7 @@ pub mod keys {
pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible"; pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible";
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type"; pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name"; pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification";
// android floating window options // android floating window options
pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window"; pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window";
@@ -2173,6 +2305,7 @@ pub mod keys {
OPTION_KEEP_SCREEN_ON, OPTION_KEEP_SCREEN_ON,
OPTION_DISABLE_GROUP_PANEL, OPTION_DISABLE_GROUP_PANEL,
OPTION_PRE_ELEVATE_SERVICE, OPTION_PRE_ELEVATE_SERVICE,
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
]; ];
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS // DEFAULT_SETTINGS, OVERWRITE_SETTINGS
pub const KEYS_SETTINGS: &[&str] = &[ pub const KEYS_SETTINGS: &[&str] = &[
@@ -2211,6 +2344,24 @@ pub mod keys {
OPTION_PRESET_ADDRESS_BOOK_TAG, OPTION_PRESET_ADDRESS_BOOK_TAG,
OPTION_ENABLE_DIRECTX_CAPTURE, OPTION_ENABLE_DIRECTX_CAPTURE,
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE, OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
OPTION_ENABLE_TRUSTED_DEVICES,
];
// BUILDIN_SETTINGS
pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[
OPTION_DISPLAY_NAME,
OPTION_DISABLE_UDP,
OPTION_PRESET_USERNAME,
OPTION_PRESET_STRATEGY_NAME,
OPTION_REMOVE_PRESET_PASSWORD_WARNING,
OPTION_HIDE_SECURITY_SETTINGS,
OPTION_HIDE_NETWORK_SETTINGS,
OPTION_HIDE_SERVER_SETTINGS,
OPTION_HIDE_PROXY_SETTINGS,
OPTION_HIDE_USERNAME_ON_CARD,
OPTION_HIDE_HELP_CARDS,
OPTION_DEFAULT_CONNECT_PASSWORD,
OPTION_HIDE_TRAY,
]; ];
} }
@@ -2471,4 +2622,26 @@ mod tests {
assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str"); assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str");
} }
} }
#[test]
fn test_store_load() {
let peerconfig_id = "123456789";
let cfg: PeerConfig = Default::default();
cfg.store(&peerconfig_id);
assert_eq!(PeerConfig::load(&peerconfig_id), cfg);
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
// ignore file type information by masking with 0o777 (see https://stackoverflow.com/a/50045872)
fs::metadata(PeerConfig::path(&peerconfig_id))
.expect("reading metadata failed")
.permissions()
.mode()
& 0o777,
0o600
);
}
}
} }

View File

@@ -838,6 +838,21 @@ pub fn create_dir(dir: &str) -> ResultType<()> {
Ok(()) Ok(())
} }
#[inline]
pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> {
let path = std::path::Path::new(&path);
if path.exists() {
let dir = path
.parent()
.ok_or(anyhow!("Parent directoy of {path:?} not exists"))?;
let new_path = dir.join(&new_name);
std::fs::rename(&path, &new_path)?;
Ok(())
} else {
bail!("{path:?} not exists");
}
}
#[inline] #[inline]
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) { pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
for entry in entries { for entry in entries {

View File

@@ -85,40 +85,19 @@ pub fn get_display_server_of_session(session: &str) -> String {
run_loginctl(Some(vec!["show-session", "-p", "Type", session])) run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
// Check session type of the session // Check session type of the session
{ {
let display_server = String::from_utf8_lossy(&output.stdout) String::from_utf8_lossy(&output.stdout)
.replace("Type=", "") .replace("Type=", "")
.trim_end() .trim_end()
.into(); .into()
if display_server == "tty" {
// If the type is tty...
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
// Get the tty number
{
let tty: String = String::from_utf8_lossy(&output.stdout)
.replace("TTY=", "")
.trim_end()
.into();
if let Ok(xorg_results) = run_cmds(&format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
// And check if Xorg is running on that tty
{
if xorg_results.trim_end() != "" {
// If it is, manually return "x11", otherwise return tty
return "x11".to_owned();
}
}
}
}
display_server
} else { } else {
"".to_owned() "".to_owned()
}; };
if display_server.is_empty() || display_server == "tty" { if display_server.is_empty() || display_server == "tty" {
// loginctl has not given the expected output. try something else.
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
display_server = sestype; if !sestype.is_empty() {
return sestype.to_lowercase();
}
} }
}
if display_server == "" {
display_server = "x11".to_owned(); display_server = "x11".to_owned();
} }
display_server.to_lowercase() display_server.to_lowercase()

View File

@@ -1,6 +1,5 @@
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
os::windows::raw::HANDLE,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::Instant,
}; };
@@ -17,7 +16,7 @@ use winapi::{
sysinfoapi::VerSetConditionMask, sysinfoapi::VerSetConditionMask,
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0}, winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
winnt::{ winnt::{
OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION, HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR, VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
}, },
}, },

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.2.6" version = "1.3.0"
edition = "2021" edition = "2021"
description = "RustDesk Remote Desktop" description = "RustDesk Remote Desktop"
@@ -14,6 +14,9 @@ dirs = "5.0"
md5 = "0.7" md5 = "0.7"
winapi = { version = "0.3", features = ["winbase"] } winapi = { version = "0.3", features = ["winbase"] }
[target.'cfg(target_os = "windows")'.dependencies]
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
[package.metadata.winres] [package.metadata.winres]
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved." LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
ProductName = "RustDesk" ProductName = "RustDesk"

View File

@@ -8,6 +8,8 @@ use std::{
use bin_reader::BinaryReader; use bin_reader::BinaryReader;
pub mod bin_reader; pub mod bin_reader;
#[cfg(windows)]
mod ui;
#[cfg(windows)] #[cfg(windows)]
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml"); const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
@@ -17,6 +19,8 @@ const APP_METADATA_CONFIG: &str = "meta.toml";
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = "; const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
const APP_PREFIX: &str = "rustdesk"; const APP_PREFIX: &str = "rustdesk";
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
#[cfg(windows)]
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool { fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else { let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
@@ -55,7 +59,13 @@ fn write_meta(dir: &PathBuf, ts: u64) {
} }
} }
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> { fn setup(
reader: BinaryReader,
dir: Option<PathBuf>,
clear: bool,
_args: &Vec<String>,
_ui: &mut bool,
) -> Option<PathBuf> {
let dir = if let Some(dir) = dir { let dir = if let Some(dir) = dir {
dir dir
} else { } else {
@@ -70,6 +80,11 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
let mut ts = 0; let mut ts = 0;
if clear || !is_timestamp_matches(&dir, &mut ts) { if clear || !is_timestamp_matches(&dir, &mut ts) {
#[cfg(windows)]
if _args.is_empty() {
*_ui = true;
ui::setup();
}
std::fs::remove_dir_all(&dir).ok(); std::fs::remove_dir_all(&dir).ok();
} }
for file in reader.files.iter() { for file in reader.files.iter() {
@@ -83,7 +98,7 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
Some(dir.join(&reader.exe)) Some(dir.join(&reader.exe))
} }
fn execute(path: PathBuf, args: Vec<String>) { fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
println!("executing {}", path.display()); println!("executing {}", path.display());
// setup env // setup env
let exe = std::env::current_exe().unwrap_or_default(); let exe = std::env::current_exe().unwrap_or_default();
@@ -95,13 +110,28 @@ fn execute(path: PathBuf, args: Vec<String>) {
{ {
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
if _ui {
cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1");
}
} }
cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name) let _child = cmd
.env(APPNAME_RUNTIME_ENV_KEY, exe_name)
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.spawn() .spawn();
.ok();
#[cfg(windows)]
if _ui {
match _child {
Ok(child) => unsafe {
winapi::um::winuser::AllowSetForegroundWindow(child.id() as u32);
},
Err(e) => {
eprintln!("{:?}", e);
}
}
}
} }
fn main() { fn main() {
@@ -119,18 +149,21 @@ fn main() {
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe"); let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe");
let mut ui = false;
let reader = BinaryReader::default(); let reader = BinaryReader::default();
if let Some(exe) = setup( if let Some(exe) = setup(
reader, reader,
None, None,
click_setup || args.contains(&"--silent-install".to_owned()), click_setup || args.contains(&"--silent-install".to_owned()),
&args,
&mut ui,
) { ) {
if click_setup { if click_setup {
args = vec!["--install".to_owned()]; args = vec!["--install".to_owned()];
} else if quick_support { } else if quick_support {
args = vec!["--quick_support".to_owned()]; args = vec!["--quick_support".to_owned()];
} }
execute(exe, args); execute(exe, args, ui);
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

232
libs/portable/src/ui.rs Normal file
View File

@@ -0,0 +1,232 @@
use native_windows_gui as nwg;
use nwg::NativeUi;
use std::cell::RefCell;
const GIF_DATA: &[u8] = include_bytes!("./res/spin.gif");
const LABEL_DATA: &[u8] = include_bytes!("./res/label.png");
const GIF_SIZE: i32 = 32;
const BG_COLOR: [u8; 3] = [90, 90, 120];
const BORDER_COLOR: [u8; 3] = [40, 40, 40];
const GIF_DELAY: u64 = 30;
#[derive(Default)]
pub struct BasicApp {
window: nwg::Window,
border_image: nwg::ImageFrame,
bg_image: nwg::ImageFrame,
gif_image: nwg::ImageFrame,
label_image: nwg::ImageFrame,
border_layout: nwg::GridLayout,
bg_layout: nwg::GridLayout,
inner_layout: nwg::GridLayout,
timer: nwg::AnimationTimer,
decoder: nwg::ImageDecoder,
gif_index: RefCell<usize>,
gif_images: RefCell<Vec<nwg::Bitmap>>,
}
impl BasicApp {
fn exit(&self) {
self.timer.stop();
nwg::stop_thread_dispatch();
}
fn load_gif(&self) -> Result<(), nwg::NwgError> {
let image_source = self.decoder.from_stream(GIF_DATA)?;
for frame_index in 0..image_source.frame_count() {
let image_data = image_source.frame(frame_index)?;
let image_data = self
.decoder
.resize_image(&image_data, [GIF_SIZE as u32, GIF_SIZE as u32])?;
let bmp = image_data.as_bitmap()?;
self.gif_images.borrow_mut().push(bmp);
}
Ok(())
}
fn update_gif(&self) -> Result<(), nwg::NwgError> {
let images = self.gif_images.borrow();
if images.len() == 0 {
return Err(nwg::NwgError::ImageDecoderError(
-1,
"no gif images".to_string(),
));
}
let image_index = *self.gif_index.borrow() % images.len();
self.gif_image.set_bitmap(Some(&images[image_index]));
*self.gif_index.borrow_mut() = (image_index + 1) % images.len();
Ok(())
}
fn start_timer(&self) {
self.timer.start();
}
}
mod basic_app_ui {
use super::*;
use native_windows_gui::{self as nwg, Bitmap};
use nwg::{Event, GridLayoutItem};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
pub struct BasicAppUi {
inner: Rc<BasicApp>,
default_handler: RefCell<Vec<nwg::EventHandler>>,
}
impl nwg::NativeUi<BasicAppUi> for BasicApp {
fn build_ui(mut data: BasicApp) -> Result<BasicAppUi, nwg::NwgError> {
data.decoder = nwg::ImageDecoder::new()?;
let col_cnt: i32 = 7;
let row_cnt: i32 = 3;
let border_width: i32 = 1;
let window_size = (
GIF_SIZE * col_cnt + 2 * border_width,
GIF_SIZE * row_cnt + 2 * border_width,
);
// Controls
nwg::Window::builder()
.flags(nwg::WindowFlags::POPUP | nwg::WindowFlags::VISIBLE)
.size(window_size)
.center(true)
.build(&mut data.window)?;
nwg::ImageFrame::builder()
.parent(&data.window)
.size(window_size)
.background_color(Some(BORDER_COLOR))
.build(&mut data.border_image)?;
nwg::ImageFrame::builder()
.parent(&data.border_image)
.size((row_cnt * GIF_SIZE, col_cnt * GIF_SIZE))
.background_color(Some(BG_COLOR))
.build(&mut data.bg_image)?;
nwg::ImageFrame::builder()
.parent(&data.bg_image)
.size((GIF_SIZE, GIF_SIZE))
.background_color(Some(BG_COLOR))
.build(&mut data.gif_image)?;
nwg::ImageFrame::builder()
.parent(&data.bg_image)
.background_color(Some(BG_COLOR))
.bitmap(Some(&Bitmap::from_bin(LABEL_DATA)?))
.build(&mut data.label_image)?;
nwg::AnimationTimer::builder()
.parent(&data.window)
.interval(std::time::Duration::from_millis(GIF_DELAY))
.build(&mut data.timer)?;
// Wrap-up
let ui = BasicAppUi {
inner: Rc::new(data),
default_handler: Default::default(),
};
// Layouts
nwg::GridLayout::builder()
.parent(&ui.window)
.spacing(0)
.margin([0, 0, 0, 0])
.max_column(Some(1))
.max_row(Some(1))
.child_item(GridLayoutItem::new(&ui.border_image, 0, 0, 1, 1))
.build(&ui.border_layout)?;
nwg::GridLayout::builder()
.parent(&ui.border_image)
.spacing(0)
.margin([
border_width as _,
border_width as _,
border_width as _,
border_width as _,
])
.max_column(Some(1))
.max_row(Some(1))
.child_item(GridLayoutItem::new(&ui.bg_image, 0, 0, 1, 1))
.build(&ui.bg_layout)?;
nwg::GridLayout::builder()
.parent(&ui.bg_image)
.spacing(0)
.margin([0, 0, 0, 0])
.max_column(Some(col_cnt as _))
.max_row(Some(row_cnt as _))
.child_item(GridLayoutItem::new(&ui.gif_image, 2, 1, 1, 1))
.child_item(GridLayoutItem::new(&ui.label_image, 3, 1, 3, 1))
.build(&ui.inner_layout)?;
// Events
let evt_ui = Rc::downgrade(&ui.inner);
let handle_events = move |evt, _evt_data, _handle| {
if let Some(evt_ui) = evt_ui.upgrade().as_mut() {
match evt {
Event::OnWindowClose => {
evt_ui.exit();
}
Event::OnTimerTick => {
if let Err(e) = evt_ui.update_gif() {
eprintln!("{:?}", e);
}
}
_ => {}
}
}
};
ui.default_handler
.borrow_mut()
.push(nwg::full_bind_event_handler(
&ui.window.handle,
handle_events,
));
return Ok(ui);
}
}
impl Drop for BasicAppUi {
/// To make sure that everything is freed without issues, the default handler must be unbound.
fn drop(&mut self) {
let mut handlers = self.default_handler.borrow_mut();
for handler in handlers.drain(0..) {
nwg::unbind_event_handler(&handler);
}
}
}
impl Deref for BasicAppUi {
type Target = BasicApp;
fn deref(&self) -> &BasicApp {
&self.inner
}
}
}
fn ui() -> Result<(), nwg::NwgError> {
nwg::init()?;
let app = BasicApp::build_ui(Default::default())?;
app.load_gif()?;
app.start_timer();
nwg::dispatch_thread_events();
Ok(())
}
pub fn setup() {
std::thread::spawn(move || {
if let Err(e) = ui() {
eprintln!("{:?}", e);
}
});
}

View File

@@ -21,7 +21,7 @@ cfg-if = "1.0"
num_cpus = "1.15" num_cpus = "1.15"
lazy_static = "1.4" lazy_static = "1.4"
hbb_common = { path = "../hbb_common" } hbb_common = { path = "../hbb_common" }
webm = { git = "https://github.com/21pages/rust-webm" } webm = { git = "https://github.com/rustdesk-org/rust-webm" }
serde = {version="1.0", features=["derive"]} serde = {version="1.0", features=["derive"]}
[dependencies.winapi] [dependencies.winapi]
@@ -59,7 +59,7 @@ gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", optional = true } gstreamer-video = { version = "0.16", optional = true }
[dependencies.hwcodec] [dependencies.hwcodec]
git = "https://github.com/21pages/hwcodec" git = "https://github.com/rustdesk-org/hwcodec"
optional = true optional = true

View File

@@ -188,6 +188,52 @@ fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &s
generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex); generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex);
} }
// If you have problems installing ffmpeg, you can download $VCPKG_ROOT/installed from ci
// Linux require link in hwcodec
/*
fn ffmpeg() {
// ffmpeg
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let static_libs = vec!["avcodec", "avutil", "avformat"];
static_libs.iter().for_each(|lib| {
find_package(lib);
});
if target_os == "windows" {
println!("cargo:rustc-link-lib=static=libmfx");
}
// os
let dyn_libs: Vec<&str> = if target_os == "windows" {
["User32", "bcrypt", "ole32", "advapi32"].to_vec()
} else if target_os == "linux" {
let mut v = ["va", "va-drm", "va-x11", "vdpau", "X11", "stdc++"].to_vec();
if target_arch == "x86_64" {
v.push("z");
}
v
} else if target_os == "macos" || target_os == "ios" {
["c++", "m"].to_vec()
} else if target_os == "android" {
["z", "m", "android", "atomic"].to_vec()
} else {
panic!("unsupported os");
};
dyn_libs
.iter()
.map(|lib| println!("cargo:rustc-link-lib={}", lib))
.count();
if target_os == "macos" || target_os == "ios" {
println!("cargo:rustc-link-lib=framework=CoreFoundation");
println!("cargo:rustc-link-lib=framework=CoreVideo");
println!("cargo:rustc-link-lib=framework=CoreMedia");
println!("cargo:rustc-link-lib=framework=VideoToolbox");
println!("cargo:rustc-link-lib=framework=AVFoundation");
}
}
*/
fn main() { fn main() {
// note: all link symbol names in x86 (32-bit) are prefixed wth "_". // note: all link symbol names in x86 (32-bit) are prefixed wth "_".
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc, // run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
@@ -204,6 +250,7 @@ fn main() {
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*"); gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*"); gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*"); gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
// ffmpeg();
// there is problem with cfg(target_os) in build.rs, so use our workaround // there is problem with cfg(target_os) in build.rs, so use our workaround
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

View File

@@ -349,6 +349,10 @@ fn init_ndk_context() -> JniResult<()> {
jvm.get_java_vm_pointer() as _, jvm.get_java_vm_pointer() as _,
ctx.as_obj().as_raw() as _, ctx.as_obj().as_raw() as _,
); );
#[cfg(feature = "hwcodec")]
hwcodec::android::ffmpeg_set_java_vm(
jvm.get_java_vm_pointer() as _,
);
} }
*lock = true; *lock = true;
return Ok(()); return Ok(());

View File

@@ -21,7 +21,7 @@ use crate::{
use hbb_common::{ use hbb_common::{
anyhow::anyhow, anyhow::anyhow,
bail, bail,
config::{keys::OPTION_ENABLE_HWCODEC, option2bool, Config, PeerConfig}, config::{option2bool, Config, PeerConfig},
lazy_static, log, lazy_static, log,
message_proto::{ message_proto::{
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames, supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
@@ -836,7 +836,9 @@ impl Decoder {
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))] #[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
pub fn enable_hwcodec_option() -> bool { pub fn enable_hwcodec_option() -> bool {
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android") { use hbb_common::config::keys::OPTION_ENABLE_HWCODEC;
if !cfg!(target_os = "ios") {
return option2bool( return option2bool(
OPTION_ENABLE_HWCODEC, OPTION_ENABLE_HWCODEC,
&Config::get_option(OPTION_ENABLE_HWCODEC), &Config::get_option(OPTION_ENABLE_HWCODEC),
@@ -846,6 +848,8 @@ pub fn enable_hwcodec_option() -> bool {
} }
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
pub fn enable_vram_option(encode: bool) -> bool { pub fn enable_vram_option(encode: bool) -> bool {
use hbb_common::config::keys::OPTION_ENABLE_HWCODEC;
if cfg!(windows) { if cfg!(windows) {
let enable = option2bool( let enable = option2bool(
OPTION_ENABLE_HWCODEC, OPTION_ENABLE_HWCODEC,

View File

@@ -32,13 +32,16 @@ pub fn convert_to_yuv(
dst_fmt.h dst_fmt.h
); );
} }
if src_pixfmt == crate::Pixfmt::BGRA || src_pixfmt == crate::Pixfmt::RGBA { if src_pixfmt == crate::Pixfmt::BGRA
|| src_pixfmt == crate::Pixfmt::RGBA
|| src_pixfmt == crate::Pixfmt::RGB565LE
{
// stride is calculated, not real, so we need to check it // stride is calculated, not real, so we need to check it
if src_stride[0] < src_width * 4 { if src_stride[0] < src_width * src_pixfmt.bytes_per_pixel() {
bail!( bail!(
"src_stride[0] < src_width * 4: {} < {}", "src_stride too small: {} < {}",
src_stride[0], src_stride[0],
src_width * 4 src_width * src_pixfmt.bytes_per_pixel()
); );
} }
if src.len() < src_stride[0] * src_height { if src.len() < src_stride[0] * src_height {
@@ -51,19 +54,26 @@ pub fn convert_to_yuv(
} }
} }
let align = |x: usize| (x + 63) / 64 * 64; let align = |x: usize| (x + 63) / 64 * 64;
let unsupported = format!(
"unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}",
dst_fmt.pixfmt
);
match (src_pixfmt, dst_fmt.pixfmt) { match (src_pixfmt, dst_fmt.pixfmt) {
(crate::Pixfmt::BGRA, crate::Pixfmt::I420) | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) => { (crate::Pixfmt::BGRA, crate::Pixfmt::I420)
| (crate::Pixfmt::RGBA, crate::Pixfmt::I420)
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I420) => {
let dst_stride_y = dst_fmt.stride[0]; let dst_stride_y = dst_fmt.stride[0];
let dst_stride_uv = dst_fmt.stride[1]; let dst_stride_uv = dst_fmt.stride[1];
dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
let dst_y = dst.as_mut_ptr(); let dst_y = dst.as_mut_ptr();
let dst_u = dst[dst_fmt.u..].as_mut_ptr(); let dst_u = dst[dst_fmt.u..].as_mut_ptr();
let dst_v = dst[dst_fmt.v..].as_mut_ptr(); let dst_v = dst[dst_fmt.v..].as_mut_ptr();
let f = if src_pixfmt == crate::Pixfmt::BGRA { let f = match src_pixfmt {
ARGBToI420 crate::Pixfmt::BGRA => ARGBToI420,
} else { crate::Pixfmt::RGBA => ABGRToI420,
ABGRToI420 crate::Pixfmt::RGB565LE => RGB565ToI420,
_ => bail!(unsupported),
}; };
call_yuv!(f( call_yuv!(f(
src.as_ptr(), src.as_ptr(),
@@ -78,7 +88,9 @@ pub fn convert_to_yuv(
src_height as _, src_height as _,
)); ));
} }
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12) | (crate::Pixfmt::RGBA, crate::Pixfmt::NV12) => { (crate::Pixfmt::BGRA, crate::Pixfmt::NV12)
| (crate::Pixfmt::RGBA, crate::Pixfmt::NV12)
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::NV12) => {
let dst_stride_y = dst_fmt.stride[0]; let dst_stride_y = dst_fmt.stride[0];
let dst_stride_uv = dst_fmt.stride[1]; let dst_stride_uv = dst_fmt.stride[1];
dst.resize( dst.resize(
@@ -87,14 +99,33 @@ pub fn convert_to_yuv(
); );
let dst_y = dst.as_mut_ptr(); let dst_y = dst.as_mut_ptr();
let dst_uv = dst[dst_fmt.u..].as_mut_ptr(); let dst_uv = dst[dst_fmt.u..].as_mut_ptr();
let f = if src_pixfmt == crate::Pixfmt::BGRA { let (input, input_stride) = match src_pixfmt {
ARGBToNV12 crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
} else { crate::Pixfmt::RGBA => (src.as_ptr(), src_stride[0]),
ABGRToNV12 crate::Pixfmt::RGB565LE => {
let mid_stride = src_width * 4;
mid_data.resize(mid_stride * src_height, 0);
call_yuv!(RGB565ToARGB(
src.as_ptr(),
src_stride[0] as _,
mid_data.as_mut_ptr(),
mid_stride as _,
src_width as _,
src_height as _,
));
(mid_data.as_ptr(), mid_stride)
}
_ => bail!(unsupported),
};
let f = match src_pixfmt {
crate::Pixfmt::BGRA => ARGBToNV12,
crate::Pixfmt::RGBA => ABGRToNV12,
crate::Pixfmt::RGB565LE => ARGBToNV12,
_ => bail!(unsupported),
}; };
call_yuv!(f( call_yuv!(f(
src.as_ptr(), input,
src_stride[0] as _, input_stride as _,
dst_y, dst_y,
dst_stride_y as _, dst_stride_y as _,
dst_uv, dst_uv,
@@ -103,7 +134,9 @@ pub fn convert_to_yuv(
src_height as _, src_height as _,
)); ));
} }
(crate::Pixfmt::BGRA, crate::Pixfmt::I444) | (crate::Pixfmt::RGBA, crate::Pixfmt::I444) => { (crate::Pixfmt::BGRA, crate::Pixfmt::I444)
| (crate::Pixfmt::RGBA, crate::Pixfmt::I444)
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I444) => {
let dst_stride_y = dst_fmt.stride[0]; let dst_stride_y = dst_fmt.stride[0];
let dst_stride_u = dst_fmt.stride[1]; let dst_stride_u = dst_fmt.stride[1];
let dst_stride_v = dst_fmt.stride[2]; let dst_stride_v = dst_fmt.stride[2];
@@ -115,23 +148,39 @@ pub fn convert_to_yuv(
let dst_y = dst.as_mut_ptr(); let dst_y = dst.as_mut_ptr();
let dst_u = dst[dst_fmt.u..].as_mut_ptr(); let dst_u = dst[dst_fmt.u..].as_mut_ptr();
let dst_v = dst[dst_fmt.v..].as_mut_ptr(); let dst_v = dst[dst_fmt.v..].as_mut_ptr();
let src = if src_pixfmt == crate::Pixfmt::BGRA { let (input, input_stride) = match src_pixfmt {
src crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
} else { crate::Pixfmt::RGBA => {
mid_data.resize(src.len(), 0); mid_data.resize(src.len(), 0);
call_yuv!(ABGRToARGB( call_yuv!(ABGRToARGB(
src.as_ptr(), src.as_ptr(),
src_stride[0] as _, src_stride[0] as _,
mid_data.as_mut_ptr(), mid_data.as_mut_ptr(),
src_stride[0] as _, src_stride[0] as _,
src_width as _, src_width as _,
src_height as _, src_height as _,
)); ));
mid_data (mid_data.as_ptr(), src_stride[0])
}
crate::Pixfmt::RGB565LE => {
let mid_stride = src_width * 4;
mid_data.resize(mid_stride * src_height, 0);
call_yuv!(RGB565ToARGB(
src.as_ptr(),
src_stride[0] as _,
mid_data.as_mut_ptr(),
mid_stride as _,
src_width as _,
src_height as _,
));
(mid_data.as_ptr(), mid_stride)
}
_ => bail!(unsupported),
}; };
call_yuv!(ARGBToI444( call_yuv!(ARGBToI444(
src.as_ptr(), input,
src_stride[0] as _, input_stride as _,
dst_y, dst_y,
dst_stride_y as _, dst_stride_y as _,
dst_u, dst_u,
@@ -143,10 +192,7 @@ pub fn convert_to_yuv(
)); ));
} }
_ => { _ => {
bail!( bail!(unsupported);
"convert not support, {src_pixfmt:?} -> {:?}",
dst_fmt.pixfmt
);
} }
} }
Ok(()) Ok(())

View File

@@ -192,19 +192,21 @@ impl EncoderApi for HwRamEncoder {
} }
fn support_abr(&self) -> bool { fn support_abr(&self) -> bool {
["qsv", "vaapi", "mediacodec"] ["qsv", "vaapi", "mediacodec", "videotoolbox"]
.iter() .iter()
.all(|&x| !self.config.name.contains(x)) .all(|&x| !self.config.name.contains(x))
} }
fn support_changing_quality(&self) -> bool { fn support_changing_quality(&self) -> bool {
["vaapi", "mediacodec"] ["vaapi", "mediacodec", "videotoolbox"]
.iter() .iter()
.all(|&x| !self.config.name.contains(x)) .all(|&x| !self.config.name.contains(x))
} }
fn latency_free(&self) -> bool { fn latency_free(&self) -> bool {
!self.config.name.contains("mediacodec") ["mediacodec", "videotoolbox"]
.iter()
.all(|&x| !self.config.name.contains(x))
} }
fn is_hardware(&self) -> bool { fn is_hardware(&self) -> bool {
@@ -501,12 +503,12 @@ pub struct HwCodecConfig {
// portable: ui start check process, check process send to ui // portable: ui start check process, check process send to ui
// sciter and unilink: get from ipc server // sciter and unilink: get from ipc server
impl HwCodecConfig { impl HwCodecConfig {
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn set(config: String) { pub fn set(config: String) {
let config = serde_json::from_str(&config).unwrap_or_default(); let config = serde_json::from_str(&config).unwrap_or_default();
log::info!("set hwcodec config"); log::info!("set hwcodec config");
log::debug!("{config:?}"); log::debug!("{config:?}");
#[cfg(windows)] #[cfg(any(windows, target_os = "macos"))]
hbb_common::config::common_store(&config, "_hwcodec"); hbb_common::config::common_store(&config, "_hwcodec");
*CONFIG.lock().unwrap() = Some(config); *CONFIG.lock().unwrap() = Some(config);
*CONFIG_SET_BY_IPC.lock().unwrap() = true; *CONFIG_SET_BY_IPC.lock().unwrap() = true;
@@ -578,7 +580,7 @@ impl HwCodecConfig {
..Default::default() ..Default::default()
} }
} }
#[cfg(windows)] #[cfg(any(windows, target_os = "macos"))]
{ {
let config = CONFIG.lock().unwrap().clone(); let config = CONFIG.lock().unwrap().clone();
match config { match config {
@@ -606,13 +608,13 @@ impl HwCodecConfig {
{ {
CONFIG.lock().unwrap().clone().unwrap_or_default() CONFIG.lock().unwrap().clone().unwrap_or_default()
} }
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(target_os = "ios")]
{ {
HwCodecConfig::default() HwCodecConfig::default()
} }
} }
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_set_value() -> Option<HwCodecConfig> { pub fn get_set_value() -> Option<HwCodecConfig> {
let set = CONFIG_SET_BY_IPC.lock().unwrap().clone(); let set = CONFIG_SET_BY_IPC.lock().unwrap().clone();
if set { if set {
@@ -622,7 +624,7 @@ impl HwCodecConfig {
} }
} }
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn already_set() -> bool { pub fn already_set() -> bool {
CONFIG_SET_BY_IPC.lock().unwrap().clone() CONFIG_SET_BY_IPC.lock().unwrap().clone()
} }
@@ -690,7 +692,7 @@ pub fn check_available_hwcodec() -> String {
serde_json::to_string(&c).unwrap_or_default() serde_json::to_string(&c).unwrap_or_default()
} }
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn start_check_process() { pub fn start_check_process() {
if !enable_hwcodec_option() || HwCodecConfig::already_set() { if !enable_hwcodec_option() || HwCodecConfig::already_set() {
return; return;

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