diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..32a440b28
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,50 @@
+FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04
+ENV HOME=/home/vscode
+ENV WORKDIR=$HOME/rustdesk
+
+WORKDIR $HOME
+RUN sudo apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+WORKDIR /
+
+RUN git clone https://github.com/microsoft/vcpkg
+WORKDIR vcpkg
+RUN git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
+RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics
+ENV VCPKG_ROOT=/vcpkg
+RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus
+
+WORKDIR /
+RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz
+
+
+USER vscode
+WORKDIR $HOME
+RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh
+RUN chmod +x rustup.sh
+RUN $HOME/rustup.sh -y
+RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android
+RUN $HOME/.cargo/bin/cargo install cargo-ndk
+
+# Install Flutter
+RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz
+RUN tar xf flutter_linux_3.7.3-stable.tar.xz && rm flutter_linux_3.7.3-stable.tar.xz
+ENV PATH="$PATH:$HOME/flutter/bin"
+RUN dart pub global activate ffigen 5.0.1
+
+
+# Install packages
+RUN sudo apt-get install -y libclang-dev
+RUN sudo apt install -y gcc-multilib
+
+WORKDIR $WORKDIR
+ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670
+
+# Somehow try to automate flutter pub get
+# https://rustdesk.com/docs/en/dev/build/android/
+# Put below steps in entrypoint.sh
+# cd flutter
+# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz
+# tar xzf so.tar.gz
+
+# own /opt/android
diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh
new file mode 100755
index 000000000..df87aace7
--- /dev/null
+++ b/.devcontainer/build.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+set -e
+
+MODE=${1:---debug}
+TYPE=${2:-linux}
+MODE=${MODE/*-/}
+
+
+build(){
+ pwd
+ $WORKDIR/entrypoint $1
+}
+
+build_arm64(){
+ CWD=$(pwd)
+ cd $WORKDIR/flutter
+ flutter pub get
+ cd $WORKDIR
+ $WORKDIR/flutter/ndk_arm64.sh
+ cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
+ cd $CWD
+}
+
+build_apk(){
+ cd $WORKDIR/flutter
+ MODE=$1 $WORKDIR/flutter/build_android.sh
+ cd $WORKDIR
+}
+
+key_gen(){
+ if [ ! -f $WORKDIR/flutter/android/key.properties ]
+ then
+ if [ ! -f $HOME/upload-keystore.jks ]
+ then
+ $WORKDIR/.devcontainer/setup.sh key
+ fi
+ read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password
+ echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties
+ else
+ echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties"
+ fi
+}
+
+android_build(){
+ if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ]
+ then
+ $WORKDIR/.devcontainer/setup.sh android
+ fi
+ build_arm64
+ case $1 in
+ debug)
+ build_apk debug
+ ;;
+ release)
+ key_gen
+ build_apk release
+ ;;
+ esac
+}
+
+case "$MODE:$TYPE" in
+ "debug:linux")
+ build
+ ;;
+ "release:linux")
+ build --release
+ ;;
+ "debug:android")
+ android_build debug
+ ;;
+ "release:android")
+ android_build release
+ ;;
+esac
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..cd82c75e3
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,34 @@
+{
+ "name": "rustdesk",
+ "build": {
+ "dockerfile": "./Dockerfile",
+ "context": "."
+ },
+ "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache",
+ "workspaceFolder": "/home/vscode/rustdesk",
+ "postStartCommand": ".devcontainer/build.sh",
+ "features": {
+ "ghcr.io/devcontainers/features/java:1": {},
+ "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": {
+ "PACKAGES": "platform-tools,ndk;22.1.7171670"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "vadimcn.vscode-lldb",
+ "mutantdino.resourcemonitor",
+ "rust-lang.rust-analyzer",
+ "tamasfe.even-better-toml",
+ "serayuzgur.crates",
+ "mhutchie.git-graph",
+ "eamodio.gitlens"
+ ],
+ "settings": {
+ "files.watcherExclude": {
+ "**/target/**": true
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh
new file mode 100755
index 000000000..c972f47b2
--- /dev/null
+++ b/.devcontainer/setup.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+set -e
+case $1 in
+ android)
+ # install deps
+ cd $WORKDIR/flutter
+ flutter pub get
+ wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz
+ tar xzf so.tar.gz
+ rm so.tar.gz
+ sudo chown -R $(whoami) $ANDROID_HOME
+ echo "Setup is Done."
+ ;;
+ linux)
+ echo "Linux Setup"
+ ;;
+ key)
+ echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!"
+ keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
+ ;;
+esac
+
+
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index c2d92097c..fea1a3672 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -1,14 +1,6 @@
name: 🐞 Bug report
description: Thanks for taking the time to fill out this bug report! Please fill the form in **English**
-title: "[Bug] "
body:
- - type: checkboxes
- attributes:
- label: Is there an existing issue for this?
- description: Please search to see if an issue related to this already exists.
- options:
- - label: I have searched the existing issues
- required: true
- type: textarea
id: desc
attributes:
@@ -52,7 +44,9 @@ body:
id: screenshots
attributes:
label: Screenshots
- description: If applicable, please add screenshots to help explain your problem
+ description: Please add screenshots to help explain your problem, if applicable, please upload video.
+ validations:
+ required: true
- type: textarea
id: context
attributes:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 7b43e397b..2da6bbaf1 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,3 +1,4 @@
+blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/rustdesk/rustdesk/discussions/category_choices
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 50cd6d0cf..29b0d0e0f 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -1,15 +1,6 @@
name: 🛠️ Feature request
description: Suggest an idea for RustDesk
-title: "[FR] "
body:
- - type: checkboxes
- attributes:
- label: Is there an existing issue for this?
- description: Please search to see if an issue related to this already exists.
- options:
- - label: I have searched the existing issues
- required: true
-
- type: textarea
id: desc
attributes:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2e1702a60..bba114315 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,9 @@ name: CI
on:
workflow_dispatch:
pull_request:
+ paths-ignore:
+ - "docs/**"
+ - "README.md"
push:
branches:
- master
@@ -14,6 +17,8 @@ on:
- '*'
paths-ignore:
- ".github/**"
+ - "docs/**"
+ - "README.md"
jobs:
# ensure_cargo_fmt:
diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml
index 5d4cf39c9..74e4efa99 100644
--- a/.github/workflows/flutter-ci.yml
+++ b/.github/workflows/flutter-ci.yml
@@ -3,6 +3,9 @@ name: Full Flutter CI
on:
workflow_dispatch:
pull_request:
+ paths-ignore:
+ - "docs/**"
+ - "README.md"
push:
branches:
- master
@@ -10,6 +13,8 @@ on:
- '*'
paths-ignore:
- ".github/**"
+ - "docs/**"
+ - "README.md"
env:
LLVM_VERSION: "15.0.6"
@@ -105,7 +110,7 @@ jobs:
- name: Install build runtime
run: |
- brew install llvm create-dmg nasm yasm cmake gcc wget ninja
+ brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
@@ -588,7 +593,7 @@ jobs:
x86_64)
# no need mock on x86_64
export VCPKG_ROOT=/opt/artifacts/vcpkg
- cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release
+ cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
;;
esac
@@ -756,7 +761,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm64-linux/include
export VCPKG_ROOT=/vcpkg
# disable hwcodec for compilation
- cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release
+ cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
;;
armv7)
cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/
@@ -766,7 +771,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm-linux/include
export VCPKG_ROOT=/vcpkg
# disable hwcodec for compilation
- cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release
+ cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
;;
esac
diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml
index f03cd0be8..b08193971 100644
--- a/.github/workflows/flutter-nightly.yml
+++ b/.github/workflows/flutter-nightly.yml
@@ -183,7 +183,7 @@ jobs:
- name: Install build runtime
run: |
- brew install llvm create-dmg nasm yasm cmake gcc wget ninja
+ brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
@@ -242,9 +242,9 @@ jobs:
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm rustdesk-${{ env.VERSION }}.dmg || true
- codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v
+ 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 rustdesk-${{ env.VERSION }}.dmg -v
+ 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
@@ -732,7 +732,7 @@ jobs:
x86_64)
# no need mock on x86_64
export VCPKG_ROOT=/opt/artifacts/vcpkg
- cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release
+ cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
;;
esac
@@ -900,7 +900,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm64-linux/include
export VCPKG_ROOT=/vcpkg
# disable hwcodec for compilation
- cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release
+ cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
;;
armv7)
cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/
@@ -910,7 +910,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm-linux/include
export VCPKG_ROOT=/vcpkg
# disable hwcodec for compilation
- cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release
+ cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
;;
esac
diff --git a/.gitignore b/.gitignore
index fd5b5955e..a71c71a4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,8 @@ flatpak/.flatpak-builder/debian-binary
flatpak/build/**
# bridge file
lib/generated_bridge.dart
+# vscode devcontainer
+.gitconfig
+.vscode-server/
+.ssh
+.devcontainer/.*
diff --git a/Cargo.lock b/Cargo.lock
index 83f623ca7..a2cdf91a4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -153,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb"
dependencies = [
"clipboard-win",
"core-graphics 0.22.3",
- "image",
+ "image 0.23.14",
"log",
"objc",
"objc-foundation",
@@ -254,9 +254,9 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -271,31 +271,31 @@ version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
name = "atk"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd"
+checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf"
dependencies = [
"atk-sys",
"bitflags",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
]
[[package]]
name = "atk-sys"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6"
+checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148"
dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -377,8 +377,8 @@ dependencies = [
"lazycell",
"log",
"peeking_take_while",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"regex",
"rustc-hash",
"shlex",
@@ -397,14 +397,20 @@ dependencies = [
"lazy_static",
"lazycell",
"peeking_take_while",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"regex",
"rustc-hash",
"shlex",
- "syn",
+ "syn 1.0.105",
]
+[[package]]
+name = "bit_field"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -508,24 +514,25 @@ dependencies = [
[[package]]
name = "cairo-rs"
-version = "0.15.12"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
+checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d"
dependencies = [
"bitflags",
"cairo-sys-rs",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
+ "once_cell",
"thiserror",
]
[[package]]
name = "cairo-sys-rs"
-version = "0.15.1"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
+checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421"
dependencies = [
- "glib-sys 0.15.10",
+ "glib-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -581,11 +588,11 @@ dependencies = [
"heck 0.4.0",
"indexmap",
"log",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"serde 1.0.149",
"serde_json 1.0.89",
- "syn",
+ "syn 1.0.105",
"tempfile",
"toml",
]
@@ -714,9 +721,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -972,7 +979,7 @@ dependencies = [
"alsa",
"core-foundation-sys 0.8.3",
"coreaudio-rs",
- "jni",
+ "jni 0.19.0",
"js-sys",
"lazy_static",
"libc",
@@ -1059,6 +1066,12 @@ dependencies = [
"cfg-if 1.0.0",
]
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1106,10 +1119,10 @@ dependencies = [
"cc",
"codespan-reporting",
"once_cell",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"scratch",
- "syn",
+ "syn 1.0.105",
]
[[package]]
@@ -1124,16 +1137,16 @@ version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
name = "dark-light"
-version = "0.2.3"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a"
+checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645"
dependencies = [
"dconf_rs",
"detect-desktop-environment",
@@ -1146,14 +1159,38 @@ dependencies = [
"zvariant",
]
+[[package]]
+name = "darling"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+dependencies = [
+ "darling_core 0.10.2",
+ "darling_macro 0.10.2",
+]
+
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.13.4",
+ "darling_macro 0.13.4",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "strsim 0.9.3",
+ "syn 1.0.105",
]
[[package]]
@@ -1164,10 +1201,21 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"strsim 0.10.0",
- "syn",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
+dependencies = [
+ "darling_core 0.10.2",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1176,9 +1224,9 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
- "darling_core",
- "quote",
- "syn",
+ "darling_core 0.13.4",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1360,9 +1408,9 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1371,9 +1419,21 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "derive_setters"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b"
+dependencies = [
+ "darling 0.10.2",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1468,6 +1528,29 @@ dependencies = [
"libloading",
]
+[[package]]
+name = "dlopen"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937"
+dependencies = [
+ "dlopen_derive",
+ "lazy_static",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "dlopen_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581"
+dependencies = [
+ "libc",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
[[package]]
name = "dlv-list"
version = "0.3.0"
@@ -1553,7 +1636,6 @@ version = "0.0.14"
dependencies = [
"core-graphics 0.22.3",
"hbb_common",
- "libc",
"log",
"objc",
"pkg-config",
@@ -1580,9 +1662,9 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1592,9 +1674,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb"
dependencies = [
"once_cell",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1613,9 +1695,9 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1658,10 +1740,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e"
dependencies = [
"proc-macro-error",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"rustversion",
- "syn",
+ "syn 1.0.105",
"synstructure",
]
@@ -1712,6 +1794,22 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+[[package]]
+name = "exr"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide 0.6.2",
+ "smallvec",
+ "threadpool",
+ "zune-inflate",
+]
+
[[package]]
name = "extend"
version = "1.1.2"
@@ -1719,9 +1817,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610"
dependencies = [
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1794,6 +1892,19 @@ dependencies = [
"time 0.3.9",
]
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "nanorand",
+ "pin-project",
+ "spin 0.9.5",
+]
+
[[package]]
name = "flutter_rust_bridge"
version = "1.61.1"
@@ -1837,11 +1948,11 @@ dependencies = [
"lazy_static",
"log",
"pathdiff",
- "quote",
+ "quote 1.0.21",
"regex",
"serde 1.0.149",
"serde_yaml",
- "syn",
+ "syn 1.0.105",
"tempfile",
"thiserror",
"toml",
@@ -1994,9 +2105,9 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2040,63 +2151,90 @@ dependencies = [
[[package]]
name = "gdk"
-version = "0.15.4"
+version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8"
+checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1"
dependencies = [
"bitflags",
"cairo-rs",
"gdk-pixbuf",
"gdk-sys",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
"pango",
]
[[package]]
name = "gdk-pixbuf"
-version = "0.15.11"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a"
+checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05"
dependencies = [
"bitflags",
"gdk-pixbuf-sys",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
-version = "0.15.10"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7"
+checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016"
dependencies = [
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
[[package]]
name = "gdk-sys"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88"
+checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"pango-sys",
"pkg-config",
"system-deps 6.0.3",
]
+[[package]]
+name = "gdkwayland-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba"
+dependencies = [
+ "gdk-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
+ "libc",
+ "pkg-config",
+ "system-deps 6.0.3",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af"
+dependencies = [
+ "gdk-sys",
+ "glib-sys 0.16.3",
+ "libc",
+ "system-deps 6.0.3",
+ "x11 2.20.1",
+]
+
[[package]]
name = "generic-array"
version = "0.14.6"
@@ -2124,8 +2262,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
+ "js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
]
[[package]]
@@ -2136,34 +2286,24 @@ checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "gio"
-version = "0.15.12"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
+checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-io",
- "gio-sys 0.15.10",
- "glib 0.15.12",
+ "futures-util",
+ "gio-sys",
+ "glib 0.16.5",
"libc",
"once_cell",
+ "pin-project-lite",
+ "smallvec",
"thiserror",
]
-[[package]]
-name = "gio-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d"
-dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
- "libc",
- "system-deps 6.0.3",
- "winapi 0.3.9",
-]
-
[[package]]
name = "gio-sys"
version = "0.16.3"
@@ -2196,26 +2336,6 @@ dependencies = [
"once_cell",
]
-[[package]]
-name = "glib"
-version = "0.15.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
-dependencies = [
- "bitflags",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-task",
- "glib-macros 0.15.11",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
- "libc",
- "once_cell",
- "smallvec",
- "thiserror",
-]
-
[[package]]
name = "glib"
version = "0.16.5"
@@ -2228,7 +2348,7 @@ dependencies = [
"futures-executor",
"futures-task",
"futures-util",
- "gio-sys 0.16.3",
+ "gio-sys",
"glib-macros 0.16.3",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
@@ -2249,24 +2369,9 @@ dependencies = [
"itertools 0.9.0",
"proc-macro-crate 0.1.5",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "glib-macros"
-version = "0.15.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64"
-dependencies = [
- "anyhow",
- "heck 0.4.0",
- "proc-macro-crate 1.2.1",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2279,9 +2384,9 @@ dependencies = [
"heck 0.4.0",
"proc-macro-crate 1.2.1",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2294,16 +2399,6 @@ dependencies = [
"system-deps 1.3.2",
]
-[[package]]
-name = "glib-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
-dependencies = [
- "libc",
- "system-deps 6.0.3",
-]
-
[[package]]
name = "glib-sys"
version = "0.16.3"
@@ -2331,17 +2426,6 @@ dependencies = [
"system-deps 1.3.2",
]
-[[package]]
-name = "gobject-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
-dependencies = [
- "glib-sys 0.15.10",
- "libc",
- "system-deps 6.0.3",
-]
-
[[package]]
name = "gobject-sys"
version = "0.16.3"
@@ -2370,7 +2454,7 @@ dependencies = [
"gstreamer-sys",
"libc",
"muldiv",
- "num-rational",
+ "num-rational 0.3.2",
"once_cell",
"paste",
"pretty-hex",
@@ -2488,9 +2572,9 @@ dependencies = [
[[package]]
name = "gtk"
-version = "0.15.5"
+version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0"
+checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6"
dependencies = [
"atk",
"bitflags",
@@ -2500,7 +2584,7 @@ dependencies = [
"gdk",
"gdk-pixbuf",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"gtk-sys",
"gtk3-macros",
"libc",
@@ -2511,17 +2595,17 @@ dependencies = [
[[package]]
name = "gtk-sys"
-version = "0.15.3"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84"
+checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3"
dependencies = [
"atk-sys",
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk-sys",
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"pango-sys",
"system-deps 6.0.3",
@@ -2529,16 +2613,16 @@ dependencies = [
[[package]]
name = "gtk3-macros"
-version = "0.15.4"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9"
+checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff"
dependencies = [
"anyhow",
"proc-macro-crate 1.2.1",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2560,6 +2644,15 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -2574,6 +2667,7 @@ name = "hbb_common"
version = "0.1.0"
dependencies = [
"anyhow",
+ "backtrace",
"bytes",
"chrono",
"confy",
@@ -2584,9 +2678,11 @@ dependencies = [
"futures",
"futures-util",
"lazy_static",
+ "libc",
"log",
"mac_address",
"machine-uid",
+ "osascript",
"protobuf",
"protobuf-codegen",
"quinn",
@@ -2597,6 +2693,7 @@ dependencies = [
"serde_json 1.0.89",
"socket2 0.3.19",
"sodiumoxide",
+ "sysinfo",
"tokio",
"tokio-socks",
"tokio-util",
@@ -2781,10 +2878,29 @@ dependencies = [
"byteorder",
"color_quant",
"num-iter",
- "num-rational",
+ "num-rational 0.3.2",
"num-traits 0.2.15",
- "png",
- "tiff",
+ "png 0.16.8",
+ "tiff 0.6.1",
+]
+
+[[package]]
+name = "image"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder 0.3.0",
+ "num-rational 0.4.1",
+ "num-traits 0.2.15",
+ "png 0.17.7",
+ "scoped_threadpool",
+ "tiff 0.8.1",
]
[[package]]
@@ -2810,8 +2926,8 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
]
[[package]]
@@ -2915,6 +3031,20 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.0"
@@ -2936,6 +3066,15 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
+dependencies = [
+ "rayon",
+]
+
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -2955,6 +3094,17 @@ dependencies = [
"winapi-build",
]
+[[package]]
+name = "keyboard-types"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68"
+dependencies = [
+ "bitflags",
+ "serde 1.0.149",
+ "unicode-segmentation",
+]
+
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -2968,12 +3118,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
-name = "libappindicator"
-version = "0.7.1"
+name = "lebe"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "libappindicator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f"
dependencies = [
- "glib 0.15.12",
+ "glib 0.16.5",
"gtk",
"gtk-sys",
"libappindicator-sys",
@@ -2982,9 +3138,9 @@ dependencies = [
[[package]]
name = "libappindicator-sys"
-version = "0.7.3"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa"
+checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918"
dependencies = [
"gtk-sys",
"libloading",
@@ -3085,6 +3241,25 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "libxdo"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db"
+dependencies = [
+ "libxdo-sys",
+]
+
+[[package]]
+name = "libxdo-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
+dependencies = [
+ "libc",
+ "x11 2.20.1",
+]
+
[[package]]
name = "link-cplusplus"
version = "1.0.7"
@@ -3340,12 +3515,41 @@ dependencies = [
"glob",
]
+[[package]]
+name = "muda"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6"
+dependencies = [
+ "cocoa",
+ "crossbeam-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gtk",
+ "keyboard-types",
+ "libxdo",
+ "objc",
+ "once_cell",
+ "png 0.17.7",
+ "thiserror",
+ "windows-sys 0.45.0",
+]
+
[[package]]
name = "muldiv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204"
+[[package]]
+name = "nanorand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
+dependencies = [
+ "getrandom",
+]
+
[[package]]
name = "ndk"
version = "0.5.0"
@@ -3428,11 +3632,11 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
dependencies = [
- "darling",
+ "darling 0.13.4",
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3579,9 +3783,9 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3616,6 +3820,17 @@ dependencies = [
"num-traits 0.2.15",
]
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-integer",
+ "num-traits 0.2.15",
+]
+
[[package]]
name = "num-traits"
version = "0.1.43"
@@ -3660,9 +3875,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
dependencies = [
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3728,7 +3943,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1"
dependencies = [
- "jni",
+ "jni 0.19.0",
"ndk 0.6.0",
"ndk-context",
"num-derive",
@@ -3747,9 +3962,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "openssl-probe"
@@ -3784,19 +3999,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
-name = "padlock"
-version = "0.2.0"
+name = "osascript"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f"
+checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc"
+dependencies = [
+ "serde 1.0.149",
+ "serde_derive",
+ "serde_json 1.0.89",
+]
[[package]]
name = "pango"
-version = "0.15.10"
+version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f"
+checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94"
dependencies = [
"bitflags",
- "glib 0.15.12",
+ "gio",
+ "glib 0.16.5",
"libc",
"once_cell",
"pango-sys",
@@ -3804,12 +4025,12 @@ dependencies = [
[[package]]
name = "pango-sys"
-version = "0.15.10"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa"
+checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f"
dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -3970,9 +4191,9 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -4005,6 +4226,18 @@ dependencies = [
"miniz_oxide 0.3.7",
]
+[[package]]
+name = "png"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "flate2",
+ "miniz_oxide 0.6.2",
+]
+
[[package]]
name = "polling"
version = "2.5.1"
@@ -4067,9 +4300,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
"version_check",
]
@@ -4079,11 +4312,20 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"version_check",
]
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.47"
@@ -4211,13 +4453,22 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
- "proc-macro2",
+ "proc-macro2 1.0.47",
]
[[package]]
@@ -4405,12 +4656,13 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
-source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130"
+source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc"
dependencies = [
"cocoa",
"core-foundation 0.9.3",
"core-foundation-sys 0.8.3",
"core-graphics 0.22.3",
+ "dispatch",
"enum-map",
"epoll",
"inotify",
@@ -4547,7 +4799,7 @@ dependencies = [
"cc",
"libc",
"once_cell",
- "spin",
+ "spin 0.5.2",
"untrusted",
"web-sys",
"winapi 0.3.9",
@@ -4663,7 +4915,6 @@ dependencies = [
"arboard",
"async-process",
"async-trait",
- "backtrace",
"base64",
"bytes",
"cc",
@@ -4683,6 +4934,7 @@ dependencies = [
"dbus-crossroads",
"default-net",
"dispatch",
+ "dlopen",
"enigo",
"errno",
"evdev",
@@ -4690,16 +4942,13 @@ dependencies = [
"flutter_rust_bridge",
"flutter_rust_bridge_codegen",
"fruitbasket",
- "glib 0.16.5",
- "gtk",
"hbb_common",
"hound",
+ "image 0.24.5",
"impersonate_system",
"include_dir",
- "jni",
+ "jni 0.19.0",
"lazy_static",
- "libappindicator",
- "libc",
"libpulse-binding",
"libpulse-simple-binding",
"mac_address",
@@ -4728,9 +4977,9 @@ dependencies = [
"shutdown_hooks",
"simple_rc",
"sys-locale",
- "sysinfo",
"system_shutdown",
- "tray-item",
+ "tao",
+ "tray-icon",
"trayicon",
"url",
"uuid",
@@ -4742,6 +4991,7 @@ dependencies = [
"winreg 0.10.1",
"winres",
"wol-rs",
+ "xrandr-parser",
]
[[package]]
@@ -4868,6 +5118,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -4889,9 +5145,8 @@ dependencies = [
"gstreamer-video",
"hbb_common",
"hwcodec",
- "jni",
+ "jni 0.19.0",
"lazy_static",
- "libc",
"log",
"ndk 0.7.0",
"num_cpus",
@@ -4992,9 +5247,9 @@ version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5026,9 +5281,9 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5127,6 +5382,12 @@ version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+[[package]]
+name = "simd-adler32"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18"
+
[[package]]
name = "simple_rc"
version = "0.1.0"
@@ -5217,6 +5478,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+[[package]]
+name = "spin"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc"
+dependencies = [
+ "lock_api",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -5247,6 +5517,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+[[package]]
+name = "strsim"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+
[[package]]
name = "strsim"
version = "0.10.0"
@@ -5272,9 +5548,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
dependencies = [
"heck 0.3.3",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5284,10 +5560,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.0",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"rustversion",
- "syn",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
]
[[package]]
@@ -5296,8 +5583,8 @@ version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"unicode-ident",
]
@@ -5307,10 +5594,10 @@ version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "unicode-xid",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+ "unicode-xid 0.2.4",
]
[[package]]
@@ -5399,6 +5686,61 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "tao"
+version = "0.17.0"
+source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "cc",
+ "cocoa",
+ "core-foundation 0.9.3",
+ "core-graphics 0.22.3",
+ "crossbeam-channel",
+ "dispatch",
+ "gdk",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gio",
+ "glib 0.16.5",
+ "glib-sys 0.16.3",
+ "gtk",
+ "image 0.24.5",
+ "instant",
+ "jni 0.20.0",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.6.0",
+ "ndk-context",
+ "ndk-sys 0.3.0",
+ "objc",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "png 0.17.7",
+ "raw-window-handle 0.5.0",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "uuid",
+ "windows 0.44.0",
+ "windows-implement",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.0"
+source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
[[package]]
name = "tap"
version = "1.0.1"
@@ -5489,9 +5831,9 @@ version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5509,11 +5851,22 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
dependencies = [
- "jpeg-decoder",
+ "jpeg-decoder 0.1.22",
"miniz_oxide 0.4.4",
"weezl",
]
+[[package]]
+name = "tiff"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
+dependencies = [
+ "flate2",
+ "jpeg-decoder 0.3.0",
+ "weezl",
+]
+
[[package]]
name = "time"
version = "0.1.45"
@@ -5584,9 +5937,9 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5673,9 +6026,9 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5698,21 +6051,22 @@ dependencies = [
]
[[package]]
-name = "tray-item"
-version = "0.7.1"
+name = "tray-icon"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f"
+checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3"
dependencies = [
"cocoa",
"core-graphics 0.22.3",
- "gtk",
+ "crossbeam-channel",
+ "dirs-next",
"libappindicator",
- "libc",
+ "muda",
"objc",
- "objc-foundation",
- "objc_id",
- "padlock",
- "winapi 0.3.9",
+ "once_cell",
+ "png 0.17.7",
+ "thiserror",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -5785,6 +6139,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
[[package]]
name = "unicode-xid"
version = "0.2.4"
@@ -5811,9 +6171,9 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.2.2"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
+checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom",
]
@@ -5929,9 +6289,9 @@ dependencies = [
"bumpalo",
"log",
"once_cell",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
"wasm-bindgen-shared",
]
@@ -5953,7 +6313,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
- "quote",
+ "quote 1.0.21",
"wasm-bindgen-macro-support",
]
@@ -5963,9 +6323,9 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -6033,8 +6393,8 @@ version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"xml-rs",
]
@@ -6242,6 +6602,39 @@ dependencies = [
"windows_x86_64_msvc 0.34.0",
]
+[[package]]
+name = "windows"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
[[package]]
name = "windows-service"
version = "0.4.0"
@@ -6287,19 +6680,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
- "windows_aarch64_msvc 0.42.0",
- "windows_i686_gnu 0.42.0",
- "windows_i686_msvc 0.42.0",
- "windows_x86_64_gnu 0.42.0",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm",
- "windows_x86_64_msvc 0.42.0",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
@@ -6327,9 +6744,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
@@ -6357,9 +6774,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
@@ -6387,9 +6804,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
@@ -6417,15 +6834,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
@@ -6453,9 +6870,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winit"
@@ -6566,12 +6983,12 @@ dependencies = [
[[package]]
name = "x11-dl"
-version = "2.20.1"
+version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
- "lazy_static",
"libc",
+ "once_cell",
"pkg-config",
]
@@ -6602,6 +7019,16 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+[[package]]
+name = "xrandr-parser"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1"
+dependencies = [
+ "derive_setters",
+ "serde 1.0.149",
+]
+
[[package]]
name = "yaml-rust"
version = "0.4.5"
@@ -6657,10 +7084,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e"
dependencies = [
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"regex",
- "syn",
+ "syn 1.0.105",
]
[[package]]
@@ -6703,6 +7130,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "zune-inflate"
+version = "0.2.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d"
+dependencies = [
+ "simd-adler32",
+]
+
[[package]]
name = "zvariant"
version = "3.9.0"
@@ -6724,7 +7160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81"
dependencies = [
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
diff --git a/Cargo.toml b/Cargo.toml
index c171e84e5..d768005fe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ inline = []
hbbs = []
cli = []
with_rc = ["simple_rc"]
+flutter_texture_render = []
appimage = []
flatpak = []
use_samplerate = ["samplerate"]
@@ -43,7 +44,6 @@ cfg-if = "1.0"
lazy_static = "1.4"
sha2 = "0.10"
repng = "0.2"
-libc = "0.2"
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
runas = "0.2"
@@ -56,7 +56,6 @@ uuid = { version = "1.0", features = ["v4"] }
clap = "3.0"
rpassword = "7.0"
base64 = "0.13"
-sysinfo = "0.24"
num_cpus = "1.13"
bytes = { version = "1.2", features = ["serde"] }
default-net = "0.12.0"
@@ -65,6 +64,7 @@ flutter_rust_bridge = { version = "1.61.1", optional = true }
errno = "0.2.8"
rdev = { git = "https://github.com/fufesou/rdev" }
url = { version = "2.1", features = ["serde"] }
+dlopen = "0.1"
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
chrono = "0.4.23"
@@ -86,7 +86,6 @@ arboard = "2.0"
system_shutdown = "3.0.0"
[target.'cfg(target_os = "windows")'.dependencies]
-#systray = { git = "https://github.com/open-trade/systray-rs" }
trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
winit = "0.26"
winapi = { version = "0.3", features = ["winuser"] }
@@ -104,11 +103,15 @@ dispatch = "0.2"
core-foundation = "0.9"
core-graphics = "0.22"
include_dir = "0.7.2"
-tray-item = "0.7" # looks better than trayicon
-dark-light = "0.2"
+dark-light = "1.0"
fruitbasket = "0.10.0"
objc_id = "0.1.1"
+[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
+tray-icon = "0.4"
+tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }
+image = "0.24"
+
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.25" }
pulse = { package = "libpulse-binding", version = "2.26" }
@@ -118,10 +121,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
-gtk = "0.15"
-libappindicator = "0.7"
-glib = "0.16.5"
-backtrace = "0.3"
+xrandr-parser = "0.3.0"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11"
@@ -157,7 +157,6 @@ identifier = "com.carriez.rustdesk"
icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"]
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"]
osx_minimum_system_version = "10.14"
-resources = ["res/mac-tray-light.png","res/mac-tray-dark.png", "res/mac-tray-light-x2.png","res/mac-tray-dark-x2.png"]
#https://github.com/johnthagen/min-sized-rust
[profile.release]
diff --git a/README.md b/README.md
index 866063726..8af79915b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Servers •
Build •
Docker •
@@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c
RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started.
-[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
@@ -41,6 +41,14 @@ Below are the servers you are using for free, they may change over time. If you
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM |
+## Dev Container
+
+[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
+
+Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
+
## Dependencies
Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only.
diff --git a/build.py b/build.py
index dce434720..727b53fe0 100755
--- a/build.py
+++ b/build.py
@@ -239,6 +239,7 @@ def get_features(args):
features.append('hwcodec')
if args.flutter:
features.append('flutter')
+ features.append('flutter_texture_render')
if args.flatpak:
features.append('flatpak')
if args.appimage:
@@ -322,7 +323,6 @@ def build_flutter_dmg(version, features):
os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h')
os.chdir('flutter')
os.system('flutter build macos --release')
- os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk')
os.system(
"create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md
new file mode 100644
index 000000000..067e0ecf9
--- /dev/null
+++ b/docs/DEVCONTAINER.md
@@ -0,0 +1,14 @@
+
+After the start of devcontainer in docker container, a linux binary in debug mode is created.
+
+Currently devcontainer offers linux and android builds in both debug and release mode.
+
+Below is the table on commands to run from root of the project for creating specific builds.
+
+Command|Build Type|Mode
+-|-|-|
+`.devcontainer/build.sh --debug linux`|Linux|debug
+`.devcontainer/build.sh --release linux`|Linux|release
+`.devcontainer/build.sh --debug android`|android-arm64|debug
+`.devcontainer/build.sh --release android`|android-arm64|debug
+
diff --git a/docs/README-DE.md b/docs/README-DE.md
index 0b51d8fdd..8ee4a51fa 100644
--- a/docs/README-DE.md
+++ b/docs/README-DE.md
@@ -1,63 +1,84 @@
- Server •
- Kompilieren •
+ Server •
+ Kompilieren •
Docker •
Dateistruktur •
Screenshots
- [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ]
- Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren
+ [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ]
+ Wir brauchen deine Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in deine Muttersprache zu übersetzen.
-Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[](https://ko-fi.com/I2I04VU09)
-Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
+RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
-RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start.
+
-[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst.
-## Kostenlose öffentliche Server
+[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
-Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
+[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases)
-| Standort | Serverart | Spezifikationen | Kommentare |
-| --------- | ------------- | ------------------ | ---------- |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | |
-| Germany | Codext | 2 vCPU / 4GB RAM |
-| Germany | Hetzner | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Freie öffentliche Server
+
+Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
+| Standort | Anbieter | Spezifikation |
+| --------- | ------------- | ------------------ |
+| Südkorea (Seoul) | AWS lightsail | 1 vCPU / 0,5 GB RAM |
+| Deutschland | Hetzner | 2 vCPU / 4 GB RAM |
+| Deutschland | Codext | 4 vCPU / 8 GB RAM |
+| Finnland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
+| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
+| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM |
## Abhängigkeiten
-Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter.
+Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.
+
+Bitte lade die dynamische Bibliothek Sciter selbst herunter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
-[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-## Die groben Schritte zum Kompilieren
+## Grobe Schritte zum Kompilieren
-- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor
+- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor
-- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu
+- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die Systemumgebungsvariable `VCPKG_ROOT` hinzu
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- - Linux/MacOS: `vcpkg install libvpx libyuv opus`
+ - Linux/macOS: `vcpkg install libvpx libyuv opus`
- Nutze `cargo run`
+## [Erstellen](https://rustdesk.com/docs/de/dev/build/)
+
## Kompilieren auf Linux
### Ubuntu 18 (Debian 10)
```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)
```sh
@@ -82,7 +103,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus
```
-### libvpx reparieren (Für Fedora)
+### libvpx reparieren (für Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -105,16 +126,40 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
-cargo run
+VCPKG_ROOT=$HOME/vcpkg cargo run
```
-### Ändere Wayland zu X11 (Xorg)
+### Wayland zu X11 (Xorg) ändern
-RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen.
+RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen.
-## Auf Docker Kompilieren
+## Wayland-Unterstützung
-Beginne damit das Repository zu klonen und den Docker Container zu bauen:
+Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene).
+
+Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen:
+```bash
+# Dienst uinput starten
+$ sudo rustdesk --service
+$ rustdesk
+```
+**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast.
+```bash
+$ dbus-send --session --print-reply \
+ --dest=org.freedesktop.portal.Desktop \
+ /org/freedesktop/portal/desktop \
+ org.freedesktop.DBus.Properties.Get \
+ string:org.freedesktop.portal.ScreenCast string:version
+# Keine Unterstützung
+Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
+# Unterstützung
+method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
+ variant uint32 4
+```
+
+## Auf Docker kompilieren
+
+Beginne damit, das Repository zu klonen und den Docker-Container zu bauen:
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -122,13 +167,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
-Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl:
+Führe jedes Mal, wenn du das Programm kompilieren musst, folgenden Befehl aus:
```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
```
-Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen:
+Bedenke, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn du verschiedene Argumente für den Kompilierbefehl angeben musst, kannst du dies am Ende des Befehls an der Position `` tun. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm findest du im Zielordner auf deinem System. Du kannst es mit folgendem Befehl ausführen:
```sh
target/debug/rustdesk
@@ -140,18 +185,20 @@ Oder, wenn du eine Releaseversion benutzt:
target/release/rustdesk
```
-Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
+Bitte stelle sicher, dass du diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
## Dateistruktur
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme
-- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
-- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient
## Screenshots
diff --git a/docs/README-JP.md b/docs/README-JP.md
index 6d3b6d380..36c74dfed 100644
--- a/docs/README-JP.md
+++ b/docs/README-JP.md
@@ -14,7 +14,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[](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)。

@@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
-## [Build](https://rustdesk.com/docs/en/dev/build/)
+## [ビルド](https://rustdesk.com/docs/en/dev/build/)
## Linuxでのビルド手順
@@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
-### Build
+### ビルド
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -154,7 +154,7 @@ target/release/rustdesk
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
-## File Structure
+## ファイル構造
- **[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)**: スクリーンキャプチャ
@@ -165,7 +165,7 @@ target/release/rustdesk
- **[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/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
-## Snapshot
+## スナップショット

diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index 04b2ccc9a..9b25f4973 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -16,6 +16,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..65291b96e
--- /dev/null
+++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index eac2fe724..d05404d3a 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..3742f241f
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..964c5faa0
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png
new file mode 100644
index 000000000..79a814f59
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
new file mode 100644
index 000000000..814ba4549
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 8c01e98de..f16b3d61d 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..de17ccbda
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..2136a2f3c
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png
new file mode 100644
index 000000000..c179bf053
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index d32c8f8e8..d9bd8fdfe 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..f8ced45f1
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..415eca622
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..d82d1a81b
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index a2f07afb4..eba179347 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0f46fafaf
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..87889c953
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..2cbe6eaf1
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index e8c754f4a..a8d80d2a2 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..88eafe8dd
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..00709a815
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..209c5f977
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..ab9832824
--- /dev/null
+++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #ffffff
+
\ No newline at end of file
diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg
new file mode 100644
index 000000000..ef0bb12a7
--- /dev/null
+++ b/flutter/assets/GitHub.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/Github.svg b/flutter/assets/Github.svg
deleted file mode 100644
index a5bd1de81..000000000
--- a/flutter/assets/Github.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg
index b7bb2f42f..df394a84f 100644
--- a/flutter/assets/Google.svg
+++ b/flutter/assets/Google.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg
index 0fa45b93d..931e72844 100644
--- a/flutter/assets/Okta.svg
+++ b/flutter/assets/Okta.svg
@@ -1,30 +1 @@
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg
new file mode 100644
index 000000000..3049f3b89
--- /dev/null
+++ b/flutter/assets/actions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg
new file mode 100644
index 000000000..4185945e1
--- /dev/null
+++ b/flutter/assets/actions_mobile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg
index e46dab11e..6fd89c9ab 100644
--- a/flutter/assets/android.svg
+++ b/flutter/assets/android.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/arrow.svg b/flutter/assets/arrow.svg
new file mode 100644
index 000000000..d0f032bc2
--- /dev/null
+++ b/flutter/assets/arrow.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg
new file mode 100644
index 000000000..7c07ee25d
--- /dev/null
+++ b/flutter/assets/call_end.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg
new file mode 100644
index 000000000..530f12a97
--- /dev/null
+++ b/flutter/assets/call_wait.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg
index 03491be6e..c4ab3c92d 100644
--- a/flutter/assets/chat.svg
+++ b/flutter/assets/chat.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg
new file mode 100644
index 000000000..fb18eabd2
--- /dev/null
+++ b/flutter/assets/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg
new file mode 100644
index 000000000..9d107d699
--- /dev/null
+++ b/flutter/assets/display.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/dots.svg b/flutter/assets/dots.svg
new file mode 100644
index 000000000..19563b849
--- /dev/null
+++ b/flutter/assets/dots.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/file.svg b/flutter/assets/file.svg
new file mode 100644
index 000000000..21c7fb9de
--- /dev/null
+++ b/flutter/assets/file.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/folder.svg b/flutter/assets/folder.svg
new file mode 100644
index 000000000..3959f7874
--- /dev/null
+++ b/flutter/assets/folder.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/folder_new.svg b/flutter/assets/folder_new.svg
new file mode 100644
index 000000000..22b729204
--- /dev/null
+++ b/flutter/assets/folder_new.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg
new file mode 100644
index 000000000..93f27bf7b
--- /dev/null
+++ b/flutter/assets/fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg
new file mode 100644
index 000000000..f244631fe
--- /dev/null
+++ b/flutter/assets/fullscreen_exit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/home.svg b/flutter/assets/home.svg
new file mode 100644
index 000000000..45a018f5d
--- /dev/null
+++ b/flutter/assets/home.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg
index 37bb196e3..5a344dd04 100644
--- a/flutter/assets/insecure.svg
+++ b/flutter/assets/insecure.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg
index f08bee6a6..17b474e6e 100644
--- a/flutter/assets/insecure_relay.svg
+++ b/flutter/assets/insecure_relay.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg
index 69f0c96cb..163e045e1 100644
--- a/flutter/assets/kb_layout_iso.svg
+++ b/flutter/assets/kb_layout_iso.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg
index 09a055be3..cfbb046ca 100644
--- a/flutter/assets/kb_layout_not_iso.svg
+++ b/flutter/assets/kb_layout_not_iso.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg
new file mode 100644
index 000000000..d72033f6d
--- /dev/null
+++ b/flutter/assets/keyboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg
index 74248b5f0..2c3697be9 100644
--- a/flutter/assets/linux.svg
+++ b/flutter/assets/linux.svg
@@ -1,6 +1 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico
deleted file mode 100644
index d5080c1f7..000000000
Binary files a/flutter/assets/logo.ico and /dev/null differ
diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png
deleted file mode 100644
index ede0e00c4..000000000
Binary files a/flutter/assets/logo.png and /dev/null differ
diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg
index 0001d0762..4d43f8bcd 100644
--- a/flutter/assets/logo.svg
+++ b/flutter/assets/logo.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg
index 8092b3af3..ccf9c7aab 100644
--- a/flutter/assets/mac.svg
+++ b/flutter/assets/mac.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg
new file mode 100644
index 000000000..a8715011b
--- /dev/null
+++ b/flutter/assets/pinned.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg
new file mode 100644
index 000000000..09aa55e2a
--- /dev/null
+++ b/flutter/assets/rec.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg
index e1b962124..bbd948c73 100644
--- a/flutter/assets/record_screen.svg
+++ b/flutter/assets/record_screen.svg
@@ -1,24 +1 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/refresh.svg b/flutter/assets/refresh.svg
new file mode 100644
index 000000000..f77fcfd4c
--- /dev/null
+++ b/flutter/assets/refresh.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/search.svg b/flutter/assets/search.svg
new file mode 100644
index 000000000..295136d7e
--- /dev/null
+++ b/flutter/assets/search.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg
index 29e1d3c4f..fcd99f2f5 100644
--- a/flutter/assets/secure.svg
+++ b/flutter/assets/secure.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg
index 8ecbdb47b..af54808a8 100644
--- a/flutter/assets/secure_relay.svg
+++ b/flutter/assets/secure_relay.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/trash.svg b/flutter/assets/trash.svg
new file mode 100644
index 000000000..f9037e0e1
--- /dev/null
+++ b/flutter/assets/trash.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg
new file mode 100644
index 000000000..7e93a7a35
--- /dev/null
+++ b/flutter/assets/unpinned.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg
index 5654befc7..bf90ec958 100644
--- a/flutter/assets/voice_call.svg
+++ b/flutter/assets/voice_call.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg
index fd8334f92..f1771c3fd 100644
--- a/flutter/assets/voice_call_waiting.svg
+++ b/flutter/assets/voice_call_waiting.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg
index 326f7829d..a0f7e3def 100644
--- a/flutter/assets/win.svg
+++ b/flutter/assets/win.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/build_android.sh b/flutter/build_android.sh
index 01ff23488..c6b639f87 100755
--- a/flutter/build_android.sh
+++ b/flutter/build_android.sh
@@ -1,8 +1,10 @@
#!/usr/bin/env bash
-$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
-flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info
-flutter build apk ---split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info
-flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info
+
+MODE=${MODE:=release}
+$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
+flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info
+flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info
+flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info
# build in linux
# $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index a731f0b08..ff373cc9c 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -19,6 +19,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
+import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -45,10 +46,20 @@ var isWebDesktop = false;
var version = "";
int androidVersion = 0;
+/// Incriment count for textureId.
+int _textureId = 0;
+int get newTextureId => _textureId++;
+final textureRenderer = TextureRgbaRenderer();
+
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
+/// Check if the app is running with single view mode.
+bool isSingleViewApp() {
+ return desktopType == DesktopType.cm;
+}
+
/// * debug or test only, DO NOT enable in release build
bool isTest = false;
@@ -147,7 +158,7 @@ class MyTheme {
static const Color canvasColor = Color(0xFF212121);
static const Color border = Color(0xFFCCCCCC);
static const Color idColor = Color(0xFF00B6F0);
- static const Color darkGray = Color(0xFFB9BABC);
+ static const Color darkGray = Color.fromARGB(255, 148, 148, 148);
static const Color cmIdColor = Color(0xFF21790B);
static const Color dark = Colors.black87;
static const Color button = Color(0xFF2C8CFF);
@@ -155,8 +166,8 @@ class MyTheme {
static ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
- backgroundColor: Color(0xFFFFFFFF),
- scaffoldBackgroundColor: Color(0xFFEEEEEE),
+ hoverColor: Color.fromARGB(255, 224, 224, 224),
+ scaffoldBackgroundColor: Color(0xFFFFFFFF),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
@@ -164,8 +175,8 @@ class MyTheme {
bodyMedium:
TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
+ cardColor: Color(0xFFEEEEEE),
hintColor: Color(0xFFAAAAAA),
- primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.black87,
@@ -178,6 +189,10 @@ class MyTheme {
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)
: null,
+ colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith(
+ brightness: Brightness.light,
+ background: Color(0xFFEEEEEE),
+ ),
).copyWith(
extensions: >[
ColorThemeExtension.light,
@@ -186,8 +201,8 @@ class MyTheme {
);
static ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
- backgroundColor: Color(0xFF252525),
- scaffoldBackgroundColor: Color(0xFF141414),
+ hoverColor: Color.fromARGB(255, 45, 46, 53),
+ scaffoldBackgroundColor: Color(0xFF18191E),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19),
titleSmall: TextStyle(fontSize: 14),
@@ -195,8 +210,7 @@ class MyTheme {
bodyMedium: TextStyle(fontSize: 14, height: 1.25),
labelLarge: TextStyle(
fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)),
- cardColor: Color(0xFF252525),
- primarySwatch: Colors.blue,
+ cardColor: Color(0xFF24252B),
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.white70,
@@ -212,6 +226,12 @@ class MyTheme {
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)
: null,
+ checkboxTheme:
+ const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)),
+ colorScheme: ColorScheme.fromSwatch(
+ brightness: Brightness.dark,
+ primarySwatch: Colors.blue,
+ ).copyWith(background: Color(0xFF24252B)),
).copyWith(
extensions: >[
ColorThemeExtension.dark,
@@ -331,6 +351,9 @@ closeConnection({String? id}) {
}
void window_on_top(int? id) {
+ if (!isDesktop) {
+ return;
+ }
if (id == null) {
// main window
windowManager.restore();
@@ -492,12 +515,14 @@ class OverlayDialogManager {
Offstage(
offstage: !showCancel,
child: Center(
- child: TextButton(
- style: flatButtonStyle,
- onPressed: cancel,
- child: Text(translate('Cancel'),
- style:
- const TextStyle(color: MyTheme.accent)))))
+ child: isDesktop
+ ? dialogButton('Cancel', onPressed: cancel)
+ : TextButton(
+ style: flatButtonStyle,
+ onPressed: cancel,
+ child: Text(translate('Cancel'),
+ style: const TextStyle(
+ color: MyTheme.accent)))))
])),
onCancel: showCancel ? cancel : null,
);
@@ -624,6 +649,7 @@ class CustomAlertDialog extends StatelessWidget {
if (!scopeNode.hasFocus) scopeNode.requestFocus();
});
const double padding = 16;
+ bool tabTapped = false;
return FocusScope(
node: scopeNode,
autofocus: true,
@@ -633,13 +659,15 @@ class CustomAlertDialog extends StatelessWidget {
onCancel?.call();
}
return KeyEventResult.handled; // avoid TextField exception on escape
- } else if (onSubmit != null &&
+ } else if (!tabTapped &&
+ onSubmit != null &&
key.logicalKey == LogicalKeyboardKey.enter) {
if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
if (key is RawKeyDownEvent) {
scopeNode.nextFocus();
+ tabTapped = true;
}
return KeyEventResult.handled;
}
@@ -648,8 +676,9 @@ class CustomAlertDialog extends StatelessWidget {
child: AlertDialog(
scrollable: true,
title: title,
- contentPadding: EdgeInsets.fromLTRB(
- contentPadding ?? padding, 25, contentPadding ?? padding, 10),
+ titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0),
+ contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25,
+ contentPadding ?? padding, actions is List ? 10 : padding),
content: ConstrainedBox(
constraints: contentBoxConstraints,
child: Theme(
@@ -659,7 +688,7 @@ class CustomAlertDialog extends StatelessWidget {
child: content),
),
actions: actions,
- actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding),
+ actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding),
),
);
}
@@ -667,7 +696,7 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox(String id, String type, String title, String text, String link,
OverlayDialogManager dialogManager,
- {bool? hasCancel}) {
+ {bool? hasCancel, ReconnectHandle? reconnect}) {
dialogManager.dismissAll();
List buttons = [];
bool hasOk = false;
@@ -707,6 +736,13 @@ void msgBox(String id, String type, String title, String text, String link,
dialogManager.dismissAll();
}));
}
+ if (reconnect != null && title == "Connection Error") {
+ buttons.insert(
+ 0,
+ dialogButton('Reconnect', isOutline: true, onPressed: () {
+ reconnect(dialogManager, id, false);
+ }));
+ }
if (link.isNotEmpty) {
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
}
@@ -1399,13 +1435,14 @@ bool callUniLinksUriHandler(Uri uri) {
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
- required bool isRDP}) async {
+ required bool isRDP,
+ bool? forceRelay}) async {
if (isFileTransfer) {
- await rustDeskWinManager.newFileTransfer(id);
+ await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
- await rustDeskWinManager.newPortForward(id, isRDP);
+ await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
} else {
- await rustDeskWinManager.newRemoteDesktop(id);
+ await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
}
}
@@ -1416,7 +1453,8 @@ connectMainDesktop(String id,
connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
- bool isRDP = false}) async {
+ bool isRDP = false,
+ bool forceRelay = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
assert(!(isFileTransfer && isTcpTunneling && isRDP),
@@ -1424,18 +1462,18 @@ connect(BuildContext context, String id,
if (isDesktop) {
if (desktopType == DesktopType.main) {
- await connectMainDesktop(
- id,
- isFileTransfer: isFileTransfer,
- isTcpTunneling: isTcpTunneling,
- isRDP: isRDP,
- );
+ await connectMainDesktop(id,
+ isFileTransfer: isFileTransfer,
+ isTcpTunneling: isTcpTunneling,
+ isRDP: isRDP,
+ forceRelay: forceRelay);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
+ "forceRelay": forceRelay,
});
}
} else {
@@ -1729,6 +1767,7 @@ Future updateSystemWindowTheme() async {
}
}
}
+
/// macOS only
///
/// Note: not found a general solution for rust based AVFoundation bingding.
@@ -1756,3 +1795,43 @@ Future osxCanRecordAudio() async {
Future osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
}
+
+class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
+ /// Creates scroll physics that does not let the user scroll.
+ const DraggableNeverScrollableScrollPhysics({super.parent});
+
+ @override
+ DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
+ return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
+ }
+
+ @override
+ bool shouldAcceptUserOffset(ScrollMetrics position) {
+ // TODO: find a better solution to check if the offset change is caused by the scrollbar.
+ // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
+ if (position is ScrollPositionWithSingleContext) {
+ // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
+ return position.activity is IdleScrollActivity;
+ }
+ return false;
+ }
+
+ @override
+ bool get allowImplicitScrolling => false;
+}
+
+Widget futureBuilder(
+ {required Future? future, required Widget Function(dynamic data) hasData}) {
+ return FutureBuilder(
+ future: future,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ if (snapshot.hasData) {
+ return hasData(snapshot.data!);
+ } else {
+ if (snapshot.hasError) {
+ debugPrint(snapshot.error.toString());
+ }
+ return Container();
+ }
+ });
+}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 5c1e1218c..88a5aaaa3 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -43,14 +43,8 @@ class _AddressBookState extends State {
return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
return Center(
- child: InkWell(
- onTap: loginDialog,
- child: Text(
- translate("Login"),
- style: const TextStyle(decoration: TextDecoration.underline),
- ),
- ),
- );
+ child: ElevatedButton(
+ onPressed: loginDialog, child: Text(translate("Login"))));
} else {
if (gFFI.abModel.abLoading.value) {
return const Center(
@@ -156,13 +150,13 @@ class _AddressBookState extends State {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(translate('Tags')),
- GestureDetector(
- onTapDown: (e) {
- final x = e.globalPosition.dx;
- final y = e.globalPosition.dy;
+ Listener(
+ onPointerDown: (e) {
+ final x = e.position.dx;
+ final y = e.position.dy;
menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
- onTap: () => _showMenu(menuPos),
+ onPointerUp: (_) => _showMenu(menuPos),
child: ActionMore()),
],
);
@@ -389,7 +383,7 @@ class _AddressBookState extends State {
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index 62f81b797..c1991633a 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -75,7 +75,8 @@ class ChatPage extends StatelessWidget implements PageShape {
hintText:
"${translate('Write a message')}...",
filled: true,
- fillColor: Theme.of(context).backgroundColor,
+ fillColor:
+ Theme.of(context).colorScheme.background,
contentPadding: EdgeInsets.all(10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
@@ -88,7 +89,8 @@ class ChatPage extends StatelessWidget implements PageShape {
: defaultInputDecoration(
hintText:
"${translate('Write a message')}...",
- fillColor: Theme.of(context).backgroundColor),
+ fillColor:
+ Theme.of(context).colorScheme.background),
sendButtonBuilder: defaultSendButton(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 0),
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index 837a197dc..cdce6f12a 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -1,18 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
+abstract class ValidationRule {
+ String get name;
+ bool validate(String value);
+}
+
+class LengthRangeValidationRule extends ValidationRule {
+ final int _min;
+ final int _max;
+
+ LengthRangeValidationRule(this._min, this._max);
+
+ @override
+ String get name => translate('length %min% to %max%')
+ .replaceAll('%min%', _min.toString())
+ .replaceAll('%max%', _max.toString());
+
+ @override
+ bool validate(String value) {
+ return value.length >= _min && value.length <= _max;
+ }
+}
+
+class RegexValidationRule extends ValidationRule {
+ final String _name;
+ final RegExp _regex;
+
+ RegexValidationRule(this._name, this._regex);
+
+ @override
+ String get name => translate(_name);
+
+ @override
+ bool validate(String value) {
+ return value.isNotEmpty ? value.contains(_regex) : false;
+ }
+}
+
void changeIdDialog() {
var newId = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController();
+ final RxString rxId = controller.text.trim().obs;
+
+ final rules = [
+ RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
+ LengthRangeValidationRule(6, 16),
+ RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
+ ];
+
gFFI.dialogManager.show((setState, close) {
submit() async {
debugPrint("onSubmit");
newId = controller.text.trim();
+
+ final Iterable violations = rules.where((r) => !r.validate(newId));
+ if (violations.isNotEmpty) {
+ setState(() {
+ msg =
+ '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
+ });
+ return;
+ }
+
setState(() {
msg = "";
isInProgress = true;
@@ -31,7 +87,7 @@ void changeIdDialog() {
}
setState(() {
isInProgress = false;
- msg = translate(status);
+ msg = '${translate('Prompt')}: ${translate(status)}';
});
}
@@ -46,18 +102,47 @@ void changeIdDialog() {
),
TextField(
decoration: InputDecoration(
+ labelText: translate('Your new ID'),
border: const OutlineInputBorder(),
- errorText: msg.isEmpty ? null : translate(msg)),
+ errorText: msg.isEmpty ? null : translate(msg),
+ suffixText: '${rxId.value.length}/16',
+ suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)),
inputFormatters: [
LengthLimitingTextInputFormatter(16),
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
],
- maxLength: 16,
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
+ onChanged: (value) {
+ setState(() {
+ rxId.value = value.trim();
+ msg = '';
+ });
+ },
),
const SizedBox(
- height: 4.0,
+ height: 8.0,
+ ),
+ Obx(() => Wrap(
+ runSpacing: 8,
+ spacing: 4,
+ children: rules.map((e) {
+ var checked = e.validate(rxId.value);
+ return Chip(
+ label: Text(
+ e.name,
+ style: TextStyle(
+ color: checked
+ ? const Color(0xFF0A9471)
+ : Color.fromARGB(255, 198, 86, 157)),
+ ),
+ backgroundColor: checked
+ ? const Color(0xFFD0F7ED)
+ : Color.fromARGB(255, 247, 205, 232));
+ }).toList(),
+ )),
+ const SizedBox(
+ height: 8.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
@@ -99,7 +184,7 @@ void changeWhiteList({Function()? callback}) async {
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
- focusNode: FocusNode()..requestFocus()),
+ autofocus: true),
),
],
),
@@ -186,7 +271,7 @@ Future changeDirectAccessPort(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
- focusNode: FocusNode()..requestFocus()),
+ autofocus: true),
),
],
),
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index 05fc1fc5c..43dc3a658 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -197,24 +197,25 @@ class _WidgetOPState extends State {
_failedMsg = '';
}
return Offstage(
- offstage:
- _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
- child: Row(
- children: [
- Text(
- _stateMsg,
- style: TextStyle(fontSize: 12),
- ),
- SizedBox(width: 8),
- Text(
- _failedMsg,
- style: TextStyle(
- fontSize: 14,
- color: Colors.red,
- ),
+ offstage:
+ _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
+ child: RichText(
+ text: TextSpan(
+ text: '$_stateMsg ',
+ style:
+ DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
+ children: [
+ TextSpan(
+ text: _failedMsg,
+ style: DefaultTextStyle.of(context).style.copyWith(
+ fontSize: 14,
+ color: Colors.red,
+ ),
),
],
- ));
+ ),
+ ),
+ );
}),
Obx(
() => Offstage(
@@ -323,13 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget {
children: [
const SizedBox(height: 8.0),
DialogTextField(
- title: '${translate("Username")}:',
+ title: translate("Username"),
controller: username,
focusNode: userFocusNode,
prefixIcon: Icon(Icons.account_circle_outlined),
errorText: usernameMsg),
DialogTextField(
- title: '${translate("Password")}:',
+ title: translate("Password"),
obscureText: true,
controller: pass,
prefixIcon: Icon(Icons.lock_outline),
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index c9af6328c..20387de48 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -170,8 +170,8 @@ class _PeerCardState extends State<_PeerCard>
),
Expanded(
child: Container(
- decoration:
- BoxDecoration(color: Theme.of(context).backgroundColor),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background),
child: Row(
children: [
Expanded(
@@ -266,7 +266,7 @@ class _PeerCardState extends State<_PeerCard>
),
),
Container(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -515,15 +515,31 @@ abstract class BasePeerCard extends StatelessWidget {
String id, Future Function() reloadFunc,
{bool isLan = false}) {
return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Remove'),
- style: style,
+ childBuilder: (TextStyle? style) => Row(
+ children: [
+ Text(
+ translate('Delete'),
+ style: style?.copyWith(color: Colors.red),
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: Icon(Icons.delete_forever, color: Colors.red),
+ ),
+ ).marginOnly(right: 4)),
+ ],
),
proc: () {
() async {
if (isLan) {
// TODO
} else {
+ final favs = (await bind.mainGetFav()).toList();
+ if (favs.remove(id)) {
+ await bind.mainStoreFav(favs: favs);
+ }
await bind.mainRemovePeer(id: id);
}
removePreference(id);
@@ -553,9 +569,21 @@ abstract class BasePeerCard extends StatelessWidget {
@protected
MenuEntryBase _addFavAction(String id) {
return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Add to Favorites'),
- style: style,
+ childBuilder: (TextStyle? style) => Row(
+ children: [
+ Text(
+ translate('Add to Favorites'),
+ style: style,
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: Icon(Icons.star_outline),
+ ),
+ ).marginOnly(right: 4)),
+ ],
),
proc: () {
() async {
@@ -575,9 +603,21 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase _rmFavAction(
String id, Future Function() reloadFunc) {
return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Remove from Favorites'),
- style: style,
+ childBuilder: (TextStyle? style) => Row(
+ children: [
+ Text(
+ translate('Remove from Favorites'),
+ style: style,
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: Icon(Icons.star),
+ ),
+ ).marginOnly(right: 4)),
+ ],
),
proc: () {
() async {
@@ -641,9 +681,10 @@ abstract class BasePeerCard extends StatelessWidget {
child: Form(
child: TextFormField(
controller: controller,
- focusNode: FocusNode()..requestFocus(),
- decoration:
- const InputDecoration(border: OutlineInputBorder()),
+ autofocus: true,
+ decoration: InputDecoration(
+ border: OutlineInputBorder(),
+ labelText: translate('Name')),
),
),
),
@@ -677,6 +718,9 @@ class RecentPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
+
+ final List favs = (await bind.mainGetFav()).toList();
+
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
@@ -690,16 +734,29 @@ class RecentPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- menuItems.add(_removeAction(peer.id, () async {
- await bind.mainLoadRecentPeers();
- }));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
- menuItems.add(_addFavAction(peer.id));
- if (!gFFI.abModel.idContainBy(peer.id)) {
+
+ if (!favs.contains(peer.id)) {
+ menuItems.add(_addFavAction(peer.id));
+ } else {
+ menuItems.add(_rmFavAction(peer.id, () async {}));
+ }
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ // if (!gFFI.abModel.idContainBy(peer.id)) {
+ // menuItems.add(_addToAb(peer));
+ // } else {
+ // menuItems.add(_removeFromAb(peer));
+ // }
menuItems.add(_addToAb(peer));
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {
+ await bind.mainLoadRecentPeers();
+ }));
return menuItems;
}
@@ -732,18 +789,26 @@ class FavoritePeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- menuItems.add(_removeAction(peer.id, () async {
- await bind.mainLoadFavPeers();
- }));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
menuItems.add(_rmFavAction(peer.id, () async {
await bind.mainLoadFavPeers();
}));
- if (!gFFI.abModel.idContainBy(peer.id)) {
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ // if (!gFFI.abModel.idContainBy(peer.id)) {
+ // menuItems.add(_addToAb(peer));
+ // } else {
+ // menuItems.add(_removeFromAb(peer));
+ // }
menuItems.add(_addToAb(peer));
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {
+ await bind.mainLoadFavPeers();
+ }));
return menuItems;
}
@@ -763,6 +828,9 @@ class DiscoveredPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
+
+ final List favs = (await bind.mainGetFav()).toList();
+
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
@@ -774,11 +842,24 @@ class DiscoveredPeerCard extends BasePeerCard {
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
- menuItems.add(MenuEntryDivider());
- menuItems.add(_removeAction(peer.id, () async {}));
- if (!gFFI.abModel.idContainBy(peer.id)) {
+
+ if (!favs.contains(peer.id)) {
+ menuItems.add(_addFavAction(peer.id));
+ } else {
+ menuItems.add(_rmFavAction(peer.id, () async {}));
+ }
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ // if (!gFFI.abModel.idContainBy(peer.id)) {
+ // menuItems.add(_addToAb(peer));
+ // } else {
+ // menuItems.add(_removeFromAb(peer));
+ // }
menuItems.add(_addToAb(peer));
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {}));
return menuItems;
}
@@ -811,13 +892,15 @@ class AddressBookPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- menuItems.add(_removeAction(peer.id, () async {}));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
if (gFFI.abModel.tags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id));
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {}));
return menuItems;
}
@@ -996,14 +1079,11 @@ void _rdpDialog(String id) async {
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
+ constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Port')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
inputFormatters: [
@@ -1013,25 +1093,19 @@ void _rdpDialog(String id) async {
decoration: const InputDecoration(
border: OutlineInputBorder(), hintText: '3389'),
controller: portController,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
+ constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Username')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration:
@@ -1040,19 +1114,15 @@ void _rdpDialog(String id) async {
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text("${translate('Password')}:")
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ "${translate('Password')}:",
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
@@ -1067,7 +1137,7 @@ void _rdpDialog(String id) async {
)),
),
],
- ),
+ ).marginOnly(bottom: 8),
],
),
),
@@ -1103,7 +1173,7 @@ class ActionMore extends StatelessWidget {
radius: 14,
backgroundColor: _hover.value
? Theme.of(context).scaffoldBackgroundColor
- : Theme.of(context).backgroundColor,
+ : Theme.of(context).colorScheme.background,
child: Icon(Icons.more_vert,
size: 18,
color: _hover.value
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index 4080f9c11..da7e37e6b 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -156,7 +156,7 @@ class _PeerTabPageState extends State
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: model.currentTab == t
- ? Theme.of(context).backgroundColor
+ ? Theme.of(context).colorScheme.background
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
@@ -231,7 +231,8 @@ class _PeerTabPageState extends State
Widget _createPeerViewTypeSwitch(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
- final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor);
+ final activeDeco =
+ BoxDecoration(color: Theme.of(context).colorScheme.background);
return Row(
children: [PeerUiType.grid, PeerUiType.list]
.map((type) => Obx(
@@ -351,7 +352,7 @@ class _PeerSearchBarState extends State {
return Container(
width: 120,
decoration: BoxDecoration(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
),
child: Obx(() => Row(
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index 2fb409970..dd39cbdfd 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_hbb/models/state_model.dart';
import '../../models/input_model.dart';
@@ -26,8 +25,7 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true,
focusNode: focusNode,
onFocusChange: onFocusChange,
- onKey:
- stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null,
+ onKey: inputModel.handleRawKeyEvent,
child: child));
}
}
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index 26e25a209..537784918 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -20,6 +20,7 @@ const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
+const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide";
const String kWindowEventShow = "show";
@@ -50,6 +51,20 @@ const int kMobileMaxDisplayHeight = 1280;
const int kDesktopMaxDisplayWidth = 1920;
const int kDesktopMaxDisplayHeight = 1080;
+const double kDesktopFileTransferNameColWidth = 200;
+const double kDesktopFileTransferModifiedColWidth = 120;
+const double kDesktopFileTransferMinimumWidth = 100;
+const double kDesktopFileTransferMaximumWidth = 300;
+const double kDesktopFileTransferRowHeight = 30.0;
+const double kDesktopFileTransferHeaderHeight = 25.0;
+
+// https://en.wikipedia.org/wiki/Non-breaking_space
+const int $nbsp = 0x00A0;
+
+extension StringExtension on String {
+ String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp));
+}
+
const Size kConnectionManagerWindowSize = Size(300, 400);
// Tabbar transition duration, now we remove the duration
const Duration kTabTransitionDuration = Duration.zero;
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index eee4c6a20..4aad66eee 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -66,7 +66,8 @@ class _ConnectionPageState extends State
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
// select all to faciliate removing text, just following the behavior of address input of chrome
- _idController.selection = TextSelection(baseOffset: 0, extentOffset: _idController.value.text.length);
+ _idController.selection = TextSelection(
+ baseOffset: 0, extentOffset: _idController.value.text.length);
});
windowManager.addListener(this);
}
@@ -120,7 +121,7 @@ class _ConnectionPageState extends State
scrollController: _scrollController,
child: CustomScrollView(
controller: _scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
@@ -149,8 +150,11 @@ class _ConnectionPageState extends State
/// Callback for the connect button.
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
- final id = _idController.id;
- connect(context, id, isFileTransfer: isFileTransfer);
+ var id = _idController.id;
+ var forceRelay = id.endsWith(r'/r');
+ if (forceRelay) id = id.substring(0, id.length - 2);
+ connect(context, id,
+ isFileTransfer: isFileTransfer, forceRelay: forceRelay);
}
/// UI for the remote ID TextField.
@@ -160,7 +164,7 @@ class _ConnectionPageState extends State
width: 320 + 20 * 2,
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
decoration: BoxDecoration(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(13)),
),
child: Ink(
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index 2986adc7a..dfa5762b0 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -70,11 +71,12 @@ class _DesktopHomePageState extends State
value: gFFI.serverModel,
child: Container(
width: 200,
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
child: DesktopScrollWrapper(
scrollController: _leftPaneScrollController,
child: SingleChildScrollView(
controller: _leftPaneScrollController,
+ physics: DraggableNeverScrollableScrollPhysics(),
child: Column(
children: [
buildTip(context),
@@ -183,7 +185,7 @@ class _DesktopHomePageState extends State
radius: 15,
backgroundColor: hover.value
? Theme.of(context).scaffoldBackgroundColor
- : Theme.of(context).backgroundColor,
+ : Theme.of(context).colorScheme.background,
child: Icon(
Icons.more_vert_outlined,
size: 20,
@@ -497,6 +499,10 @@ class _DesktopHomePageState extends State
if (watchIsInputMonitoring) {
if (bind.mainIsCanInputMonitoring(prompt: false)) {
watchIsInputMonitoring = false;
+ // Do not notify for now.
+ // Monitoring may not take effect until the process is restarted.
+ // rustDeskWinManager.call(
+ // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, '');
setState(() {});
}
}
@@ -556,6 +562,7 @@ class _DesktopHomePageState extends State
isFileTransfer: call.arguments['isFileTransfer'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
+ forceRelay: call.arguments['forceRelay'],
);
}
});
@@ -594,13 +601,13 @@ void setPasswordDialog() async {
});
final pass = p0.text.trim();
if (pass.isNotEmpty) {
- for (var r in rules) {
- if (!r.validate(pass)) {
- setState(() {
- errMsg0 = '${translate('Prompt')}: ${r.name}';
- });
- return;
- }
+ final Iterable violations = rules.where((r) => !r.validate(pass));
+ if (violations.isNotEmpty) {
+ setState(() {
+ errMsg0 =
+ '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
+ });
+ return;
}
}
if (p1.text.trim() != pass) {
@@ -634,9 +641,12 @@ void setPasswordDialog() async {
border: const OutlineInputBorder(),
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
controller: p0,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
onChanged: (value) {
rxPass.value = value.trim();
+ setState(() {
+ errMsg0 = '';
+ });
},
),
),
@@ -660,6 +670,11 @@ void setPasswordDialog() async {
labelText: translate('Confirmation'),
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
controller: p1,
+ onChanged: (value) {
+ setState(() {
+ errMsg1 = '';
+ });
+ },
),
),
],
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index 4b6cf2a62..e041b591d 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -108,7 +108,7 @@ class _DesktopSettingPageState extends State
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: Row(
children: [
SizedBox(
@@ -128,7 +128,7 @@ class _DesktopSettingPageState extends State
scrollController: controller,
child: PageView(
controller: controller,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: const [
_General(),
_Safety(),
@@ -170,7 +170,7 @@ class _DesktopSettingPageState extends State
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: tabs
.asMap()
@@ -234,7 +234,7 @@ class _GeneralState extends State<_General> {
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
theme(),
@@ -319,7 +319,7 @@ class _GeneralState extends State<_General> {
bind.mainSetOption(key: 'audio-input', value: device);
}
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
List devices = (await bind.mainGetSoundInputs()).toList();
if (Platform.isWindows) {
devices.insert(0, 'System Sound');
@@ -346,7 +346,7 @@ class _GeneralState extends State<_General> {
}
Widget record(BuildContext context) {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
String customDirectory =
await bind.mainGetOption(key: 'video-save-directory');
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
@@ -399,7 +399,7 @@ class _GeneralState extends State<_General> {
}
Widget language() {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
String langs = await bind.mainGetLangs();
String lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
return {'langs': langs, 'lang': lang};
@@ -456,7 +456,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return DesktopScrollWrapper(
scrollController: scrollController,
child: SingleChildScrollView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
child: Column(
children: [
@@ -487,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget _permissions(context, bool stopService) {
bool enabled = !locked;
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
return await bind.mainGetOption(key: 'access-mode');
}(), hasData: (data) {
String accessMode = data! as String;
@@ -650,7 +650,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
context, onChanged != null)),
),
],
- ).paddingSymmetric(horizontal: 10),
+ ).paddingOnly(right: 10),
onTap: () => onChanged?.call(value),
))
.toList();
@@ -675,6 +675,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword) radios[0],
if (usePassword)
_SubLabeledWidget(
+ context,
'One-time password length',
Row(
children: [
@@ -701,6 +702,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
enabled: enabled),
),
+ shareRdp(context, enabled),
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled),
...directIp(context),
@@ -708,6 +710,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
]);
}
+ shareRdp(BuildContext context, bool enabled) {
+ onChanged(bool b) async {
+ await bind.mainSetShareRdp(enable: b);
+ setState(() {});
+ }
+
+ bool value = bind.mainIsShareRdp();
+ return Offstage(
+ offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()),
+ child: GestureDetector(
+ child: Row(
+ children: [
+ Checkbox(
+ value: value,
+ onChanged: enabled ? (_) => onChanged(!value) : null)
+ .marginOnly(right: 5),
+ Expanded(
+ child: Text(translate('Enable RDP session sharing'),
+ style:
+ TextStyle(color: _disabledTextColor(context, enabled))),
+ )
+ ],
+ ).marginOnly(left: _kCheckBoxLeftMargin),
+ onTap: enabled ? () => onChanged(!value) : null),
+ );
+ }
+
List directIp(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
@@ -715,7 +744,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return [
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
update: update, enabled: !locked),
- _futureBuilder(
+ futureBuilder(
future: () async {
String enabled = await bind.mainGetOption(key: 'direct-server');
String port = await bind.mainGetOption(key: 'direct-access-port');
@@ -728,9 +757,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
controller.text = data['port'].toString();
return Offstage(
offstage: !enabled,
- child: Row(children: [
- _SubLabeledWidget(
- 'Port',
+ child: _SubLabeledWidget(
+ context,
+ 'Port',
+ Row(children: [
SizedBox(
width: 80,
child: TextField(
@@ -744,28 +774,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
textAlign: TextAlign.end,
decoration: const InputDecoration(
hintText: '21118',
- border: InputBorder.none,
- contentPadding: EdgeInsets.only(right: 5),
+ border: OutlineInputBorder(),
+ contentPadding:
+ EdgeInsets.only(bottom: 10, top: 10, right: 10),
isCollapsed: true,
),
- ),
+ ).marginOnly(right: 15),
),
- enabled: enabled && !locked,
- ).marginOnly(left: 5),
- Obx(() => ElevatedButton(
- onPressed: applyEnabled.value && enabled && !locked
- ? () async {
- applyEnabled.value = false;
- await bind.mainSetOption(
- key: 'direct-access-port',
- value: controller.text);
- }
- : null,
- child: Text(
- translate('Apply'),
- ),
- ).marginOnly(left: 20))
- ]),
+ Obx(() => ElevatedButton(
+ onPressed: applyEnabled.value && enabled && !locked
+ ? () async {
+ applyEnabled.value = false;
+ await bind.mainSetOption(
+ key: 'direct-access-port',
+ value: controller.text);
+ }
+ : null,
+ child: Text(
+ translate('Apply'),
+ ),
+ ))
+ ]),
+ enabled: enabled && !locked,
+ ),
);
},
),
@@ -774,7 +805,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget whitelist() {
bool enabled = !locked;
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
return await bind.mainGetOption(key: 'whitelist');
}(), hasData: (data) {
RxBool hasWhitelist = (data as String).isNotEmpty.obs;
@@ -880,7 +911,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
scrollController: scrollController,
child: ListView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: [
_lock(locked, 'Unlock Network Settings', () {
locked = false;
@@ -900,7 +931,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
server(bool enabled) {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
return await bind.mainGetOptions();
}(), hasData: (data) {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
@@ -1043,7 +1074,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
- ).marginOnly(top: 15),
+ ).marginOnly(top: 10),
],
)
]);
@@ -1066,7 +1097,7 @@ class _DisplayState extends State<_Display> {
scrollController: scrollController,
child: ListView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: [
viewStyle(context),
scrollStyle(context),
@@ -1306,7 +1337,7 @@ class _AccountState extends State<_Account> {
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
_Card(title: 'Account', children: [accountAction()]),
@@ -1335,7 +1366,7 @@ class _About extends StatefulWidget {
class _AboutState extends State<_About> {
@override
Widget build(BuildContext context) {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
final license = await bind.mainGetLicense();
final version = await bind.mainGetVersion();
final buildDate = await bind.mainGetBuildDate();
@@ -1350,7 +1381,7 @@ class _AboutState extends State<_About> {
scrollController: scrollController,
child: SingleChildScrollView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
child: _Card(title: '${translate('About')} RustDesk', children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1469,7 +1500,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
bool enabled = true,
Icon? checkedIcon,
bool? fakeValue}) {
- return _futureBuilder(
+ return futureBuilder(
future: bind.mainGetOption(key: key),
hasData: (data) {
bool value = option2bool(key, data.toString());
@@ -1586,63 +1617,22 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) {
}
// ignore: non_constant_identifier_names
-Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) {
- RxBool hover = false.obs;
+Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
+ {bool enabled = true}) {
return Row(
children: [
- MouseRegion(
- onEnter: (_) => hover.value = true,
- onExit: (_) => hover.value = false,
- child: Obx(
- () {
- return Container(
- height: 32,
- decoration: BoxDecoration(
- border: Border.all(
- color: hover.value && enabled
- ? const Color(0xFFD7D7D7)
- : const Color(0xFFCBCBCB),
- width: hover.value && enabled ? 2 : 1)),
- child: Row(
- children: [
- Container(
- height: 28,
- color: (hover.value && enabled)
- ? const Color(0xFFD7D7D7)
- : const Color(0xFFCBCBCB),
- alignment: Alignment.center,
- padding: const EdgeInsets.symmetric(
- horizontal: 5, vertical: 2),
- child: Text(
- '${translate(label)}: ',
- style: const TextStyle(fontWeight: FontWeight.w300),
- ),
- ).paddingAll(2),
- child,
- ],
- ));
- },
- )),
+ Text(
+ '${translate(label)}: ',
+ style: TextStyle(color: _disabledTextColor(context, enabled)),
+ ),
+ SizedBox(
+ width: 10,
+ ),
+ child,
],
).marginOnly(left: _kContentHSubMargin);
}
-Widget _futureBuilder(
- {required Future? future, required Widget Function(dynamic data) hasData}) {
- return FutureBuilder(
- future: future,
- builder: (BuildContext context, AsyncSnapshot snapshot) {
- if (snapshot.hasData) {
- return hasData(snapshot.data!);
- } else {
- if (snapshot.hasError) {
- debugPrint(snapshot.error.toString());
- }
- return Container();
- }
- });
-}
-
Widget _lock(
bool locked,
String label,
@@ -1691,33 +1681,30 @@ _LabeledTextField(
bool secure) {
return Row(
children: [
- Spacer(flex: 1),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate(label)}:',
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ fontSize: 16, color: _disabledTextColor(context, enabled)),
+ ).marginOnly(right: 10)),
Expanded(
- flex: 4,
- child: Text(
- '${translate(label)}:',
- textAlign: TextAlign.right,
- style: TextStyle(color: _disabledTextColor(context, enabled)),
- ),
- ),
- Spacer(flex: 1),
- Expanded(
- flex: 10,
child: TextField(
controller: controller,
enabled: enabled,
obscureText: secure,
decoration: InputDecoration(
isDense: true,
- contentPadding: EdgeInsets.symmetric(vertical: 15),
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(14, 15, 14, 15),
errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle(
color: _disabledTextColor(context, enabled),
)),
),
- Spacer(flex: 1),
],
- );
+ ).marginOnly(bottom: 8);
}
// ignore: must_be_immutable
@@ -1804,6 +1791,7 @@ void changeSocks5Proxy() async {
var proxyController = TextEditingController(text: proxy);
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: password);
+ RxBool obscure = true.obs;
var isInProgress = false;
gFFI.dialogManager.show((setState, close) {
@@ -1849,35 +1837,30 @@ void changeSocks5Proxy() async {
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Hostname")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Hostname")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
controller: proxyController,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Username")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Username")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration: const InputDecoration(
@@ -1887,32 +1870,30 @@ void changeSocks5Proxy() async {
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Password")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Password")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
- child: TextField(
- decoration: const InputDecoration(
- border: OutlineInputBorder(),
- ),
- controller: pwdController,
- ),
+ child: Obx(() => TextField(
+ obscureText: obscure.value,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ suffixIcon: IconButton(
+ onPressed: () => obscure.value = !obscure.value,
+ icon: Icon(obscure.value
+ ? Icons.visibility_off
+ : Icons.visibility))),
+ controller: pwdController,
+ )),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart
index 35d5a61ef..053a2d8a2 100644
--- a/flutter/lib/desktop/pages/desktop_tab_page.dart
+++ b/flutter/lib/desktop/pages/desktop_tab_page.dart
@@ -65,7 +65,7 @@ class _DesktopTabPageState extends State {
Widget build(BuildContext context) {
final tabWidget = Container(
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
tail: ActionIcon(
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index 9955c2768..c8cb7c935 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -2,20 +2,24 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
+import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
+import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart';
+import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/file_model.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
+
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
-
import '../../common.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
@@ -46,8 +50,10 @@ enum MouseFocusScope {
}
class FileManagerPage extends StatefulWidget {
- const FileManagerPage({Key? key, required this.id}) : super(key: key);
+ const FileManagerPage({Key? key, required this.id, this.forceRelay})
+ : super(key: key);
final String id;
+ final bool? forceRelay;
@override
State createState() => _FileManagerPageState();
@@ -73,6 +79,10 @@ class _FileManagerPageState extends State
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
final _listSearchBufferLocal = TimeoutStringBuffer();
final _listSearchBufferRemote = TimeoutStringBuffer();
+ final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs;
+ final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs;
+ final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs;
+ final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs;
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime =
@@ -102,7 +112,7 @@ class _FileManagerPageState extends State
void initState() {
super.initState();
_ffi = FFI();
- _ffi.start(widget.id, isFileTransfer: true);
+ _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
@@ -145,7 +155,7 @@ class _FileManagerPageState extends State
value: _ffi.fileModel,
child: Consumer(builder: (context, model, child) {
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row(
children: [
Flexible(flex: 3, child: body(isLocal: true)),
@@ -190,35 +200,42 @@ class _FileManagerPageState extends State
];
return Listener(
- onPointerDown: (e) {
- final x = e.position.dx;
- final y = e.position.dy;
- menuPos = RelativeRect.fromLTRB(x, y, x, y);
- },
- child: IconButton(
- icon: const Icon(Icons.more_vert),
- splashRadius: kDesktopIconButtonSplashRadius,
- onPressed: () => mod_menu.showMenu(
- context: context,
- position: menuPos,
- items: items
- .map((e) => e.build(
- context,
- MenuConfig(
- commonColor: CustomPopupMenuTheme.commonColor,
- height: CustomPopupMenuTheme.height,
- dividerHeight: CustomPopupMenuTheme.dividerHeight)))
- .expand((i) => i)
- .toList(),
- elevation: 8,
- ),
- ));
+ onPointerDown: (e) {
+ final x = e.position.dx;
+ final y = e.position.dy;
+ menuPos = RelativeRect.fromLTRB(x, y, x, y);
+ },
+ child: MenuButton(
+ onPressed: () => mod_menu.showMenu(
+ context: context,
+ position: menuPos,
+ items: items
+ .map(
+ (e) => e.build(
+ context,
+ MenuConfig(
+ commonColor: CustomPopupMenuTheme.commonColor,
+ height: CustomPopupMenuTheme.height,
+ dividerHeight: CustomPopupMenuTheme.dividerHeight),
+ ),
+ )
+ .expand((i) => i)
+ .toList(),
+ elevation: 8,
+ ),
+ child: SvgPicture.asset(
+ "assets/dots.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ );
}
Widget body({bool isLocal = false}) {
final scrollController = ScrollController();
return Container(
- decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(8.0),
child: DropTarget(
@@ -229,44 +246,31 @@ class _FileManagerPageState extends State
onDragExited: (exit) {
_dropMaskVisible.value = false;
},
- child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- headTools(isLocal),
- Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ headTools(isLocal),
+ Expanded(
child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: SingleChildScrollView(
- controller: scrollController,
- child: _buildDataTable(context, isLocal, scrollController),
- ),
- )
- ],
- )),
- ]),
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: _buildFileList(context, isLocal, scrollController),
+ )
+ ],
+ ),
+ ),
+ ],
+ ),
),
);
}
- Widget _buildDataTable(
+ Widget _buildFileList(
BuildContext context, bool isLocal, ScrollController scrollController) {
- const rowHeight = 25.0;
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
- final sortIndex = (SortBy style) {
- switch (style) {
- case SortBy.name:
- return 0;
- case SortBy.type:
- return 0;
- case SortBy.modified:
- return 1;
- case SortBy.size:
- return 2;
- }
- }(model.getSortStyle(isLocal));
- final sortAscending =
- isLocal ? model.localSortAscending : model.remoteSortAscending;
+ final selectedEntries = getSelectedItems(isLocal);
return MouseRegion(
onEnter: (evt) {
@@ -287,7 +291,6 @@ class _FileManagerPageState extends State
onNext: (buffer) {
debugPrint("searching next for $buffer");
assert(buffer.length == 1);
- final selectedEntries = getSelectedItems(isLocal);
assert(selectedEntries.length <= 1);
var skipCount = 0;
if (selectedEntries.items.isNotEmpty) {
@@ -299,11 +302,12 @@ class _FileManagerPageState extends State
}
var searchResult = entries
.skip(skipCount)
- .where((element) => element.name.startsWith(buffer));
+ .where((element) => element.name.toLowerCase().startsWith(buffer));
if (searchResult.isEmpty) {
// cannot find next, lets restart search from head
+ debugPrint("restart search from head");
searchResult =
- entries.where((element) => element.name.startsWith(buffer));
+ entries.where((element) => element.name.toLowerCase().startsWith(buffer));
}
if (searchResult.isEmpty) {
setState(() {
@@ -311,14 +315,14 @@ class _FileManagerPageState extends State
});
return;
}
- _jumpToEntry(
- isLocal, searchResult.first, scrollController, rowHeight, buffer);
+ _jumpToEntry(isLocal, searchResult.first, scrollController,
+ kDesktopFileTransferRowHeight);
},
onSearch: (buffer) {
debugPrint("searching for $buffer");
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
- entries.where((element) => element.name.startsWith(buffer));
+ entries.where((element) => element.name.toLowerCase().startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
setState(() {
@@ -326,8 +330,8 @@ class _FileManagerPageState extends State
});
return;
}
- _jumpToEntry(
- isLocal, searchResult.first, scrollController, rowHeight, buffer);
+ _jumpToEntry(isLocal, searchResult.first, scrollController,
+ kDesktopFileTransferRowHeight);
},
child: ObxValue(
(searchText) {
@@ -336,118 +340,154 @@ class _FileManagerPageState extends State
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
- return DataTable(
- key: ValueKey(isLocal ? 0 : 1),
- showCheckboxColumn: false,
- dataRowHeight: rowHeight,
- headingRowHeight: 30,
- horizontalMargin: 8,
- columnSpacing: 8,
- showBottomBorder: true,
- sortColumnIndex: sortIndex,
- sortAscending: sortAscending,
- columns: [
- DataColumn(
- label: Text(
- translate("Name"),
- ).marginSymmetric(horizontal: 4),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.name,
- isLocal: isLocal, ascending: ascending);
- }),
- DataColumn(
- label: Text(
- translate("Modified"),
- ),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.modified,
- isLocal: isLocal, ascending: ascending);
- }),
- DataColumn(
- label: Text(translate("Size")),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.size,
- isLocal: isLocal, ascending: ascending);
- }),
- ],
- rows: filteredEntries.map((entry) {
- final sizeStr =
- entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
- final lastModifiedStr = entry.isDrive
- ? " "
- : "${entry.lastModified().toString().replaceAll(".000", "")} ";
- return DataRow(
- key: ValueKey(entry.name),
- onSelectChanged: (s) {
- _onSelectedChanged(getSelectedItems(isLocal),
- filteredEntries, entry, isLocal);
- },
- selected: getSelectedItems(isLocal).contains(entry),
- cells: [
- DataCell(
- Container(
- width: 200,
- child: Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: entry.name,
- child: Row(children: [
- entry.isDrive
- ? Image(
- image: iconHardDrive,
- fit: BoxFit.scaleDown,
- color: Theme.of(context)
- .iconTheme
- .color
- ?.withOpacity(0.7))
- .paddingAll(4)
- : Icon(
- entry.isFile
- ? Icons.feed_outlined
- : Icons.folder,
- size: 20,
- color: Theme.of(context)
- .iconTheme
- .color
- ?.withOpacity(0.7),
- ).marginSymmetric(horizontal: 2),
- Expanded(
- child: Text(entry.name,
- overflow: TextOverflow.ellipsis))
- ]),
- )),
- onTap: () {
- final items = getSelectedItems(isLocal);
-
- // handle double click
- if (_checkDoubleClick(entry)) {
- openDirectory(entry.path, isLocal: isLocal);
- items.clear();
- return;
- }
- _onSelectedChanged(
- items, filteredEntries, entry, isLocal);
- },
+ final rows = filteredEntries.map((entry) {
+ final sizeStr =
+ entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
+ final lastModifiedStr = entry.isDrive
+ ? " "
+ : "${entry.lastModified().toString().replaceAll(".000", "")} ";
+ final isSelected = selectedEntries.contains(entry);
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 1),
+ child: Container(
+ decoration: BoxDecoration(
+ color: isSelected
+ ? Theme.of(context).hoverColor
+ : Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(5.0),
),
- DataCell(FittedBox(
- child: Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: lastModifiedStr,
- child: Text(
- lastModifiedStr,
- style: TextStyle(
- fontSize: 12, color: MyTheme.darkGray),
- )))),
- DataCell(Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: sizeStr,
- child: Text(
- sizeStr,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- fontSize: 10, color: MyTheme.darkGray),
- ))),
- ]);
- }).toList(growable: false),
+ ),
+ key: ValueKey(entry.name),
+ height: kDesktopFileTransferRowHeight,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Expanded(
+ child: InkWell(
+ child: Row(
+ children: [
+ GestureDetector(
+ child: Obx(
+ () => Container(
+ width: isLocal
+ ? _nameColWidthLocal.value
+ : _nameColWidthRemote.value,
+ child: Tooltip(
+ waitDuration:
+ Duration(milliseconds: 500),
+ message: entry.name,
+ child: Row(children: [
+ entry.isDrive
+ ? Image(
+ image: iconHardDrive,
+ fit: BoxFit.scaleDown,
+ color: Theme.of(context)
+ .iconTheme
+ .color
+ ?.withOpacity(0.7))
+ .paddingAll(4)
+ : SvgPicture.asset(
+ entry.isFile
+ ? "assets/file.svg"
+ : "assets/folder.svg",
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ ),
+ Expanded(
+ child: Text(
+ entry.name.nonBreaking,
+ overflow:
+ TextOverflow.ellipsis))
+ ]),
+ )),
+ ),
+ onTap: () {
+ final items = getSelectedItems(isLocal);
+ // handle double click
+ if (_checkDoubleClick(entry)) {
+ openDirectory(entry.path,
+ isLocal: isLocal);
+ items.clear();
+ return;
+ }
+ _onSelectedChanged(
+ items, filteredEntries, entry, isLocal);
+ },
+ ),
+ SizedBox(
+ width: 2.0,
+ ),
+ GestureDetector(
+ child: Obx(
+ () => SizedBox(
+ width: isLocal
+ ? _modifiedColWidthLocal.value
+ : _modifiedColWidthRemote.value,
+ child: Tooltip(
+ waitDuration:
+ Duration(milliseconds: 500),
+ message: lastModifiedStr,
+ child: Text(
+ lastModifiedStr,
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ )),
+ ),
+ ),
+ ),
+ // Divider from header.
+ SizedBox(
+ width: 2.0,
+ ),
+ Expanded(
+ // width: 100,
+ child: GestureDetector(
+ child: Tooltip(
+ waitDuration: Duration(milliseconds: 500),
+ message: sizeStr,
+ child: Text(
+ sizeStr,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ fontSize: 10,
+ color: MyTheme.darkGray),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ )),
+ );
+ }).toList(growable: false);
+
+ return Column(
+ children: [
+ // Header
+ Row(
+ children: [
+ Expanded(child: _buildFileBrowserHeader(context, isLocal)),
+ ],
+ ),
+ // Body
+ Expanded(
+ child: ListView.builder(
+ controller: scrollController,
+ itemExtent: kDesktopFileTransferRowHeight,
+ itemBuilder: (context, index) {
+ return rows[index];
+ },
+ itemCount: rows.length,
+ ),
+ ),
+ ],
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
@@ -457,7 +497,7 @@ class _FileManagerPageState extends State
}
void _jumpToEntry(bool isLocal, Entry entry,
- ScrollController scrollController, double rowHeight, String buffer) {
+ ScrollController scrollController, double rowHeight) {
final entries = model.getCurrentDir(isLocal).entries;
final index = entries.indexOf(entry);
if (index == -1) {
@@ -465,7 +505,7 @@ class _FileManagerPageState extends State
}
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
- entries.where((element) => element.name.startsWith(buffer));
+ entries.where((element) => element == entry);
selectedEntries.clear();
if (searchResult.isEmpty) {
return;
@@ -532,98 +572,156 @@ class _FileManagerPageState extends State
Widget statusList() {
return PreferredSize(
preferredSize: const Size(200, double.infinity),
- child: Container(
- margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
- padding: const EdgeInsets.all(8.0),
- decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
- child: Obx(
- () => ListView.builder(
- controller: ScrollController(),
- itemBuilder: (BuildContext context, int index) {
- final item = model.jobTable[index];
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Transform.rotate(
- angle: item.isRemote ? pi : 0,
- child: const Icon(Icons.send)),
- const SizedBox(
- width: 16.0,
- ),
- Expanded(
+ child: model.jobTable.isEmpty
+ ? Center(child: Text(translate("Empty")))
+ : Container(
+ margin:
+ const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
+ padding: const EdgeInsets.all(8.0),
+ child: Obx(
+ () => ListView.builder(
+ controller: ScrollController(),
+ itemBuilder: (BuildContext context, int index) {
+ final item = model.jobTable[index];
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 5),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(15.0),
+ ),
+ ),
child: Column(
mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: item.jobName,
- child: Text(
- item.jobName,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- )),
- Wrap(
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Text(
- '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '),
- Text(
- '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '),
- Offstage(
- offstage:
- item.state != JobState.inProgress,
- child: Text(
- '${"${readableFileSize(item.speed)}/s"} ')),
- Offstage(
- offstage: item.totalSize <= 0,
- child: Text(
- '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'),
+ Transform.rotate(
+ angle: item.isRemote ? pi : 0,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ ),
+ ).paddingOnly(left: 15),
+ const SizedBox(
+ width: 16.0,
+ ),
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Tooltip(
+ waitDuration:
+ Duration(milliseconds: 500),
+ message: item.jobName,
+ child: Text(
+ item.jobName,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ).paddingSymmetric(vertical: 10),
+ ),
+ Text(
+ '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state != JobState.inProgress,
+ child: Text(
+ '${translate("Speed")} ${readableFileSize(item.speed)}/s',
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state == JobState.inProgress,
+ child: Text(
+ translate(
+ item.display(),
+ ),
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state != JobState.inProgress,
+ child: LinearPercentIndicator(
+ padding: EdgeInsets.only(right: 15),
+ animateFromLastPercent: true,
+ center: Text(
+ '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
+ ),
+ barRadius: Radius.circular(15),
+ percent: item.finishedSize /
+ item.totalSize,
+ progressColor: MyTheme.accent,
+ backgroundColor:
+ Theme.of(context).hoverColor,
+ lineHeight:
+ kDesktopFileTransferRowHeight,
+ ).paddingSymmetric(vertical: 15),
+ ),
+ ],
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Offstage(
+ offstage: item.state != JobState.paused,
+ child: MenuButton(
+ onPressed: () {
+ model.resumeJob(item.id);
+ },
+ child: SvgPicture.asset(
+ "assets/refresh.svg",
+ color: Colors.white,
+ ),
+ color: MyTheme.accent,
+ hoverColor: MyTheme.accent80,
+ ),
+ ),
+ MenuButton(
+ padding: EdgeInsets.only(right: 15),
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Colors.white,
+ ),
+ onPressed: () {
+ model.jobTable.removeAt(index);
+ model.cancelJob(item.id);
+ },
+ color: MyTheme.accent,
+ hoverColor: MyTheme.accent80,
+ ),
+ ],
),
],
),
],
- ),
+ ).paddingSymmetric(vertical: 10),
),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Offstage(
- offstage: item.state != JobState.paused,
- child: IconButton(
- onPressed: () {
- model.resumeJob(item.id);
- },
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.restart_alt_rounded)),
- ),
- IconButton(
- icon: const Icon(Icons.close),
- splashRadius: 1,
- onPressed: () {
- model.jobTable.removeAt(index);
- model.cancelJob(item.id);
- },
- ),
- ],
- )
- ],
- ),
- SizedBox(
- height: 8.0,
- ),
- Divider(
- height: 2.0,
- )
- ],
- );
- },
- itemCount: model.jobTable.length,
- ),
- ),
- ));
+ );
+ },
+ itemCount: model.jobTable.length,
+ ),
+ ),
+ ));
}
Widget headTools(bool isLocal) {
@@ -632,95 +730,131 @@ class _FileManagerPageState extends State
final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
final selectedItems = getSelectedItems(isLocal);
return Container(
- child: Column(
- children: [
- // symbols
- PreferredSize(
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Container(
- width: 50,
- height: 50,
- decoration: BoxDecoration(color: Colors.blue),
- padding: EdgeInsets.all(8.0),
- child: FutureBuilder(
- future: bind.sessionGetPlatform(
- id: _ffi.id, isRemote: !isLocal),
- builder: (context, snapshot) {
- if (snapshot.hasData && snapshot.data!.isNotEmpty) {
- return getPlatformImage('${snapshot.data}');
- } else {
- return CircularProgressIndicator(
- color: Colors.white,
- );
- }
- })),
- Text(isLocal
- ? translate("Local Computer")
- : translate("Remote Computer"))
- .marginOnly(left: 8.0)
- ],
- ),
- preferredSize: Size(double.infinity, 70)),
- // buttons
- Row(
- children: [
- Row(
- children: [
- IconButton(
- icon: const Icon(Icons.arrow_back),
- splashRadius: kDesktopIconButtonSplashRadius,
- onPressed: () {
- selectedItems.clear();
- model.goBack(isLocal: isLocal);
- },
- ),
- IconButton(
- icon: const Icon(Icons.arrow_upward),
- splashRadius: kDesktopIconButtonSplashRadius,
- onPressed: () {
- selectedItems.clear();
- model.goToParentDirectory(isLocal: isLocal);
- },
- ),
- ],
- ),
- Expanded(
- child: GestureDetector(
- onTap: () {
- locationStatus.value =
- locationStatus.value == LocationStatus.bread
- ? LocationStatus.pathLocation
- : LocationStatus.bread;
- Future.delayed(Duration.zero, () {
- if (locationStatus.value == LocationStatus.pathLocation) {
- locationFocus.requestFocus();
- }
- });
- },
- child: Obx(() => Container(
- decoration: BoxDecoration(
- border: Border.all(
- color: locationStatus.value == LocationStatus.bread
- ? Colors.black12
- : Theme.of(context)
- .colorScheme
- .primary
- .withOpacity(0.5))),
+ child: Column(
+ children: [
+ // symbols
+ PreferredSize(
child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Expanded(
- child: locationStatus.value == LocationStatus.bread
- ? buildBread(isLocal)
- : buildPathLocation(isLocal)),
+ Container(
+ width: 50,
+ height: 50,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ color: MyTheme.accent,
+ ),
+ padding: EdgeInsets.all(8.0),
+ child: FutureBuilder(
+ future: bind.sessionGetPlatform(
+ id: _ffi.id, isRemote: !isLocal),
+ builder: (context, snapshot) {
+ if (snapshot.hasData &&
+ snapshot.data!.isNotEmpty) {
+ return getPlatformImage('${snapshot.data}');
+ } else {
+ return CircularProgressIndicator(
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ );
+ }
+ })),
+ Text(isLocal
+ ? translate("Local Computer")
+ : translate("Remote Computer"))
+ .marginOnly(left: 8.0)
],
- ))),
- )),
- Obx(() {
- switch (locationStatus.value) {
- case LocationStatus.bread:
- return IconButton(
+ ),
+ preferredSize: Size(double.infinity, 70))
+ .paddingOnly(bottom: 15),
+ // buttons
+ Row(
+ children: [
+ Row(
+ children: [
+ MenuButton(
+ padding: EdgeInsets.only(
+ right: 3,
+ ),
+ child: RotatedBox(
+ quarterTurns: 2,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ onPressed: () {
+ selectedItems.clear();
+ model.goBack(isLocal: isLocal);
+ },
+ ),
+ MenuButton(
+ child: RotatedBox(
+ quarterTurns: 3,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ onPressed: () {
+ selectedItems.clear();
+ model.goToParentDirectory(isLocal: isLocal);
+ },
+ ),
+ ],
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 3.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(8.0),
+ ),
+ ),
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 2.5),
+ child: GestureDetector(
+ onTap: () {
+ locationStatus.value =
+ locationStatus.value == LocationStatus.bread
+ ? LocationStatus.pathLocation
+ : LocationStatus.bread;
+ Future.delayed(Duration.zero, () {
+ if (locationStatus.value ==
+ LocationStatus.pathLocation) {
+ locationFocus.requestFocus();
+ }
+ });
+ },
+ child: Obx(
+ () => Container(
+ child: Row(
+ children: [
+ Expanded(
+ child: locationStatus.value ==
+ LocationStatus.bread
+ ? buildBread(isLocal)
+ : buildPathLocation(isLocal)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Obx(() {
+ switch (locationStatus.value) {
+ case LocationStatus.bread:
+ return MenuButton(
onPressed: () {
locationStatus.value = LocationStatus.fileSearchBar;
final focusNode =
@@ -728,49 +862,77 @@ class _FileManagerPageState extends State
Future.delayed(
Duration.zero, () => focusNode.requestFocus());
},
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: Icon(Icons.search));
- case LocationStatus.pathLocation:
- return IconButton(
- color: Theme.of(context).disabledColor,
+ child: SvgPicture.asset(
+ "assets/search.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ );
+ case LocationStatus.pathLocation:
+ return MenuButton(
onPressed: null,
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: Icon(Icons.close));
- case LocationStatus.fileSearchBar:
- return IconButton(
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
color: Theme.of(context).disabledColor,
+ hoverColor: Theme.of(context).hoverColor,
+ );
+ case LocationStatus.fileSearchBar:
+ return MenuButton(
onPressed: () {
onSearchText("", isLocal);
locationStatus.value = LocationStatus.bread;
},
- splashRadius: 1,
- icon: Icon(Icons.close));
- }
- }),
- IconButton(
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ );
+ }
+ }),
+ MenuButton(
+ padding: EdgeInsets.only(
+ left: 3,
+ ),
onPressed: () {
model.refresh(isLocal: isLocal);
},
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.refresh)),
- ],
- ),
- Row(
- textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
- children: [
- Expanded(
- child: Row(
- mainAxisAlignment:
- isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
- children: [
- IconButton(
- onPressed: () {
- model.goHome(isLocal: isLocal);
- },
- icon: const Icon(Icons.home_outlined),
- splashRadius: kDesktopIconButtonSplashRadius,
- ),
- IconButton(
+ child: SvgPicture.asset(
+ "assets/refresh.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ ],
+ ),
+ Row(
+ textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
+ children: [
+ Expanded(
+ child: Row(
+ mainAxisAlignment:
+ isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
+ children: [
+ MenuButton(
+ padding: EdgeInsets.only(
+ right: 3,
+ ),
+ onPressed: () {
+ model.goHome(isLocal: isLocal);
+ },
+ child: SvgPicture.asset(
+ "assets/home.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ MenuButton(
onPressed: () {
final name = TextEditingController();
_ffi.dialogManager.show((setState, close) {
@@ -798,7 +960,7 @@ class _FileManagerPageState extends State
"Please enter the folder name"),
),
controller: name,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
],
),
@@ -812,9 +974,14 @@ class _FileManagerPageState extends State
);
});
},
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.create_new_folder_outlined)),
- IconButton(
+ child: SvgPicture.asset(
+ "assets/folder_new.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ MenuButton(
onPressed: validItems(selectedItems)
? () async {
await (model.removeAction(selectedItems,
@@ -822,32 +989,80 @@ class _FileManagerPageState extends State
selectedItems.clear();
}
: null,
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.delete_forever_outlined)),
- menu(isLocal: isLocal),
- ],
+ child: SvgPicture.asset(
+ "assets/trash.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ menu(isLocal: isLocal),
+ ],
+ ),
),
- ),
- TextButton.icon(
+ ElevatedButton.icon(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(isLocal
+ ? EdgeInsets.only(left: 10)
+ : EdgeInsets.only(right: 10)),
+ backgroundColor: MaterialStateProperty.all(
+ selectedItems.length == 0
+ ? MyTheme.accent80
+ : MyTheme.accent,
+ ),
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ),
+ ),
+ ),
onPressed: validItems(selectedItems)
? () {
model.sendFiles(selectedItems, isRemote: !isLocal);
selectedItems.clear();
}
: null,
- icon: Transform.rotate(
- angle: isLocal ? 0 : pi,
- child: const Icon(
- Icons.send,
- ),
- ),
- label: Text(
- isLocal ? translate('Send') : translate('Receive'),
- )),
- ],
- ).marginOnly(top: 8.0)
- ],
- ));
+ icon: isLocal
+ ? Text(
+ translate('Send'),
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ color: selectedItems.length == 0
+ ? MyTheme.darkGray
+ : Colors.white,
+ ),
+ )
+ : RotatedBox(
+ quarterTurns: 2,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: selectedItems.length == 0
+ ? MyTheme.darkGray
+ : Colors.white,
+ alignment: Alignment.bottomRight,
+ ),
+ ),
+ label: isLocal
+ ? SvgPicture.asset(
+ "assets/arrow.svg",
+ color: selectedItems.length == 0
+ ? MyTheme.darkGray
+ : Colors.white,
+ )
+ : Text(
+ translate('Receive'),
+ style: TextStyle(
+ color: selectedItems.length == 0
+ ? MyTheme.darkGray
+ : Colors.white,
+ ),
+ ),
+ ),
+ ],
+ ).marginOnly(top: 8.0)
+ ],
+ ),
+ );
}
bool validItems(SelectedItems items) {
@@ -902,25 +1117,27 @@ class _FileManagerPageState extends State
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
- child: Listener(
- // handle mouse wheel
- onPointerSignal: (e) {
- if (e is PointerScrollEvent) {
- final sc = getBreadCrumbScrollController(isLocal);
- final scale = Platform.isWindows ? 2 : 4;
- sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
- }
- },
- child: BreadCrumb(
- items: items,
- divider: Icon(Icons.chevron_right),
- overflow: ScrollableOverflow(
- controller:
- getBreadCrumbScrollController(isLocal)),
- ))),
+ child: Listener(
+ // handle mouse wheel
+ onPointerSignal: (e) {
+ if (e is PointerScrollEvent) {
+ final sc = getBreadCrumbScrollController(isLocal);
+ final scale = Platform.isWindows ? 2 : 4;
+ sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
+ }
+ },
+ child: BreadCrumb(
+ items: items,
+ divider: const Icon(Icons.keyboard_arrow_right_rounded),
+ overflow: ScrollableOverflow(
+ controller: getBreadCrumbScrollController(isLocal),
+ ),
+ ),
+ ),
+ ),
ActionIcon(
message: "",
- icon: Icons.arrow_drop_down,
+ icon: Icons.keyboard_arrow_down_rounded,
onTap: () async {
final renderBox = locationBarKey.currentContext
?.findRenderObject() as RenderBox;
@@ -1033,13 +1250,23 @@ class _FileManagerPageState extends State
.marginSymmetric(horizontal: 4)));
} else {
final list = PathUtil.split(path, isWindows);
- breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
- content: TextButton(
+ breadCrumbList.addAll(
+ list.asMap().entries.map(
+ (e) => BreadCrumbItem(
+ content: TextButton(
child: Text(e.value),
style: ButtonStyle(
- minimumSize: MaterialStateProperty.all(Size(0, 0))),
- onPressed: () => onPressed(list.sublist(0, e.key + 1)))
- .marginSymmetric(horizontal: 4))));
+ minimumSize: MaterialStateProperty.all(
+ Size(0, 0),
+ ),
+ ),
+ onPressed: () => onPressed(
+ list.sublist(0, e.key + 1),
+ ),
+ ).marginSymmetric(horizontal: 4),
+ ),
+ ),
+ );
}
return breadCrumbList;
}
@@ -1066,29 +1293,35 @@ class _FileManagerPageState extends State
: searchTextObs.value;
final textController = TextEditingController(text: text)
..selection = TextSelection.collapsed(offset: text.length);
- return Row(children: [
- Icon(
- locationStatus.value == LocationStatus.pathLocation
- ? Icons.folder
- : Icons.search,
- color: Theme.of(context).hintColor,
- ).paddingSymmetric(horizontal: 2),
- Expanded(
+ return Row(
+ children: [
+ SvgPicture.asset(
+ locationStatus.value == LocationStatus.pathLocation
+ ? "assets/folder.svg"
+ : "assets/search.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ Expanded(
child: TextField(
- focusNode: focusNode,
- decoration: InputDecoration(
- border: InputBorder.none,
- isDense: true,
- prefix: Padding(padding: EdgeInsets.only(left: 4.0))),
- controller: textController,
- onSubmitted: (path) {
- openDirectory(path, isLocal: isLocal);
- },
- onChanged: locationStatus.value == LocationStatus.fileSearchBar
- ? (searchText) => onSearchText(searchText, isLocal)
- : null,
- ))
- ]);
+ focusNode: focusNode,
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ isDense: true,
+ prefix: Padding(
+ padding: EdgeInsets.only(left: 4.0),
+ ),
+ ),
+ controller: textController,
+ onSubmitted: (path) {
+ openDirectory(path, isLocal: isLocal);
+ },
+ onChanged: locationStatus.value == LocationStatus.fileSearchBar
+ ? (searchText) => onSearchText(searchText, isLocal)
+ : null,
+ ),
+ )
+ ],
+ );
}
onSearchText(String searchText, bool isLocal) {
@@ -1133,4 +1366,99 @@ class _FileManagerPageState extends State
}
});
}
+
+ Widget headerItemFunc(
+ double? width, SortBy sortBy, String name, bool isLocal) {
+ final headerTextStyle =
+ Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle();
+ return ObxValue>(
+ (ascending) => InkWell(
+ onTap: () {
+ if (ascending.value == null) {
+ ascending.value = true;
+ } else {
+ ascending.value = !ascending.value!;
+ }
+ model.changeSortStyle(sortBy,
+ isLocal: isLocal, ascending: ascending.value!);
+ },
+ child: SizedBox(
+ width: width,
+ height: kDesktopFileTransferHeaderHeight,
+ child: Row(
+ children: [
+ Flexible(
+ flex: 2,
+ child: Text(
+ name,
+ style: headerTextStyle,
+ overflow: TextOverflow.ellipsis,
+ ).marginSymmetric(horizontal: 4),
+ ),
+ Flexible(
+ flex: 1,
+ child: ascending.value != null
+ ? Icon(
+ ascending.value!
+ ? Icons.keyboard_arrow_up_rounded
+ : Icons.keyboard_arrow_down_rounded,
+ )
+ : const Offstage())
+ ],
+ ),
+ ),
+ ), () {
+ if (model.getSortStyle(isLocal) == sortBy) {
+ return model.getSortAscending(isLocal).obs;
+ } else {
+ return Rx(null);
+ }
+ }());
+ }
+
+ Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) {
+ final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote;
+ final modifiedColWidth =
+ isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote;
+ final padding = EdgeInsets.all(1.0);
+ return SizedBox(
+ height: kDesktopFileTransferHeaderHeight,
+ child: Row(
+ children: [
+ Obx(
+ () => headerItemFunc(
+ nameColWidth.value, SortBy.name, translate("Name"), isLocal),
+ ),
+ DraggableDivider(
+ axis: Axis.vertical,
+ onPointerMove: (dx) {
+ nameColWidth.value += dx;
+ nameColWidth.value = min(
+ kDesktopFileTransferMaximumWidth,
+ max(kDesktopFileTransferMinimumWidth,
+ nameColWidth.value));
+ },
+ padding: padding,
+ ),
+ Obx(
+ () => headerItemFunc(modifiedColWidth.value, SortBy.modified,
+ translate("Modified"), isLocal),
+ ),
+ DraggableDivider(
+ axis: Axis.vertical,
+ onPointerMove: (dx) {
+ modifiedColWidth.value += dx;
+ modifiedColWidth.value = min(
+ kDesktopFileTransferMaximumWidth,
+ max(kDesktopFileTransferMinimumWidth,
+ modifiedColWidth.value));
+ },
+ padding: padding),
+ Expanded(
+ child:
+ headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
+ ],
+ ),
+ );
+ }
}
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index b2566e267..148d928d9 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -41,7 +41,11 @@ class _FileManagerTabPageState extends State {
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => () => tabController.closeBy(params['id']),
- page: FileManagerPage(key: ValueKey(params['id']), id: params['id'])));
+ page: FileManagerPage(
+ key: ValueKey(params['id']),
+ id: params['id'],
+ forceRelay: params['forceRelay'],
+ )));
}
@override
@@ -64,7 +68,11 @@ class _FileManagerTabPageState extends State {
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
- page: FileManagerPage(key: ValueKey(id), id: id)));
+ page: FileManagerPage(
+ key: ValueKey(id),
+ id: id,
+ forceRelay: args['forceRelay'],
+ )));
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
@@ -82,7 +90,7 @@ class _FileManagerTabPageState extends State {
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).cardColor,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index f513a1c6a..ae070b47b 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -26,10 +26,12 @@ class _PortForward {
}
class PortForwardPage extends StatefulWidget {
- const PortForwardPage({Key? key, required this.id, required this.isRDP})
+ const PortForwardPage(
+ {Key? key, required this.id, required this.isRDP, this.forceRelay})
: super(key: key);
final String id;
final bool isRDP;
+ final bool? forceRelay;
@override
State createState() => _PortForwardPageState();
@@ -47,7 +49,7 @@ class _PortForwardPageState extends State
void initState() {
super.initState();
_ffi = FFI();
- _ffi.start(widget.id, isPortForward: true);
+ _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay);
Get.put(_ffi, tag: 'pf_${widget.id}');
if (!Platform.isLinux) {
Wakelock.enable();
@@ -89,7 +91,7 @@ class _PortForwardPageState extends State
Flexible(
child: Container(
decoration: BoxDecoration(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
border: Border.all(width: 1, color: MyTheme.border)),
child:
widget.isRDP ? buildRdp(context) : buildTunnel(context),
@@ -132,7 +134,7 @@ class _PortForwardPageState extends State
return Theme(
data: Theme.of(context)
- .copyWith(backgroundColor: Theme.of(context).backgroundColor),
+ .copyWith(backgroundColor: Theme.of(context).colorScheme.background),
child: Obx(() => ListView.builder(
controller: ScrollController(),
itemCount: pfs.length + 2,
@@ -167,7 +169,8 @@ class _PortForwardPageState extends State
return Container(
height: _kRowHeight,
- decoration: BoxDecoration(color: Theme.of(context).backgroundColor),
+ decoration:
+ BoxDecoration(color: Theme.of(context).colorScheme.background),
child: Row(children: [
buildTunnelInputCell(context,
controller: localPortController,
@@ -179,36 +182,33 @@ class _PortForwardPageState extends State
buildTunnelInputCell(context,
controller: remotePortController,
inputFormatters: portInputFormatter),
- SizedBox(
- width: _kColumn4Width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0, side: const BorderSide(color: MyTheme.border)),
- onPressed: () async {
- int? localPort = int.tryParse(localPortController.text);
- int? remotePort = int.tryParse(remotePortController.text);
- if (localPort != null &&
- remotePort != null &&
- (remoteHostController.text.isEmpty ||
- remoteHostController.text.trim().isNotEmpty)) {
- await bind.sessionAddPortForward(
- id: 'pf_${widget.id}',
- localPort: localPort,
- remoteHost: remoteHostController.text.trim().isEmpty
- ? 'localhost'
- : remoteHostController.text.trim(),
- remotePort: remotePort);
- localPortController.clear();
- remoteHostController.clear();
- remotePortController.clear();
- refreshTunnelConfig();
- }
- },
- child: Text(
- translate('Add'),
- ),
- ).marginAll(10),
- ),
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ elevation: 0, side: const BorderSide(color: MyTheme.border)),
+ onPressed: () async {
+ int? localPort = int.tryParse(localPortController.text);
+ int? remotePort = int.tryParse(remotePortController.text);
+ if (localPort != null &&
+ remotePort != null &&
+ (remoteHostController.text.isEmpty ||
+ remoteHostController.text.trim().isNotEmpty)) {
+ await bind.sessionAddPortForward(
+ id: 'pf_${widget.id}',
+ localPort: localPort,
+ remoteHost: remoteHostController.text.trim().isEmpty
+ ? 'localhost'
+ : remoteHostController.text.trim(),
+ remotePort: remotePort);
+ localPortController.clear();
+ remoteHostController.clear();
+ remotePortController.clear();
+ refreshTunnelConfig();
+ }
+ },
+ child: Text(
+ translate('Add'),
+ ),
+ ).marginAll(10),
]),
);
}
@@ -230,7 +230,7 @@ class _PortForwardPageState extends State
borderSide: BorderSide(color: MyTheme.color(context).border!)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: MyTheme.color(context).border!)),
- fillColor: Theme.of(context).backgroundColor,
+ fillColor: Theme.of(context).colorScheme.background,
contentPadding: const EdgeInsets.all(10),
hintText: hint,
hintStyle:
@@ -252,7 +252,7 @@ class _PortForwardPageState extends State
? MyTheme.currentThemeMode() == ThemeMode.dark
? const Color(0xFF202020)
: const Color(0xFFF4F5F6)
- : Theme.of(context).backgroundColor),
+ : Theme.of(context).colorScheme.background),
child: Row(children: [
text(pf.localPort.toString()),
const SizedBox(width: _kColumn1Width),
@@ -294,7 +294,7 @@ class _PortForwardPageState extends State
).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
- .copyWith(backgroundColor: Theme.of(context).backgroundColor),
+ .copyWith(backgroundColor: Theme.of(context).colorScheme.background),
child: ListView.builder(
controller: ScrollController(),
itemCount: 2,
@@ -313,8 +313,8 @@ class _PortForwardPageState extends State
} else {
return Container(
height: _kRowHeight,
- decoration:
- BoxDecoration(color: Theme.of(context).backgroundColor),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background),
child: Row(children: [
Expanded(
child: Align(
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index ca354f297..f2d75d00f 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State {
key: ValueKey(params['id']),
id: params['id'],
isRDP: isRDP,
+ forceRelay: params['forceRelay'],
)));
}
@@ -72,7 +73,12 @@ class _PortForwardTabPageState extends State {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
- page: PortForwardPage(id: id, isRDP: isRDP)));
+ page: PortForwardPage(
+ key: ValueKey(args['id']),
+ id: id,
+ isRDP: isRDP,
+ forceRelay: args['forceRelay'],
+ )));
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
@@ -90,7 +96,7 @@ class _PortForwardTabPageState extends State {
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: () async {
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index a7289335f..ab0daece7 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -21,6 +21,7 @@ import '../../mobile/widgets/dialog.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
+import '../../utils/image.dart';
import '../widgets/remote_menubar.dart';
import '../widgets/kb_layout_type_chooser.dart';
@@ -33,11 +34,13 @@ class RemotePage extends StatefulWidget {
required this.id,
required this.menubarState,
this.switchUuid,
+ this.forceRelay,
}) : super(key: key);
final String id;
final MenubarState menubarState;
final String? switchUuid;
+ final bool? forceRelay;
final SimpleWrapper?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
@@ -60,6 +63,9 @@ class _RemotePageState extends State
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
+ late RxInt _textureId;
+ late int _textureKey;
+ final useTextureRender = bind.mainUseTextureRender();
final _blockableOverlayState = BlockableOverlayState();
@@ -82,6 +88,8 @@ class _RemotePageState extends State
_showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id);
_remoteCursorMoved = RemoteCursorMovedState.find(id);
+ _textureKey = newTextureId;
+ _textureId = RxInt(-1);
}
void _removeStates(String id) {
@@ -106,6 +114,7 @@ class _RemotePageState extends State
_ffi.start(
widget.id,
switchUuid: widget.switchUuid,
+ forceRelay: widget.forceRelay,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -115,6 +124,18 @@ class _RemotePageState extends State
if (!Platform.isLinux) {
Wakelock.enable();
}
+ // Register texture.
+ _textureId.value = -1;
+ if (useTextureRender) {
+ textureRenderer.createTexture(_textureKey).then((id) async {
+ debugPrint("id: $id, texture_key: $_textureKey");
+ if (id != -1) {
+ final ptr = await textureRenderer.getTexturePtr(_textureKey);
+ platformFFI.registerTexture(widget.id, ptr);
+ _textureId.value = id;
+ }
+ });
+ }
_ffi.ffiModel.updateEventListener(widget.id);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// Session option should be set after models.dart/FFI.start
@@ -179,6 +200,10 @@ class _RemotePageState extends State
@override
void dispose() {
debugPrint("REMOTE PAGE dispose ${widget.id}");
+ if (useTextureRender) {
+ platformFFI.registerTexture(widget.id, 0);
+ textureRenderer.closeTexture(_textureKey);
+ }
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(id: widget.id, enter: false);
DesktopMultiWindow.removeListener(this);
@@ -200,7 +225,7 @@ class _RemotePageState extends State
Widget buildBody(BuildContext context) {
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
@@ -342,6 +367,8 @@ class _RemotePageState extends State
cursorOverImage: _cursorOverImage,
keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
+ textureId: _textureId,
+ useTextureRender: useTextureRender,
listenerBuilder: (child) =>
_buildRawPointerMouseRegion(child, enterView, leaveView),
);
@@ -379,6 +406,8 @@ class ImagePaint extends StatefulWidget {
final RxBool cursorOverImage;
final RxBool keyboardEnabled;
final RxBool remoteCursorMoved;
+ final RxInt textureId;
+ final bool useTextureRender;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
@@ -388,6 +417,8 @@ class ImagePaint extends StatefulWidget {
required this.cursorOverImage,
required this.keyboardEnabled,
required this.remoteCursorMoved,
+ required this.textureId,
+ required this.useTextureRender,
this.listenerBuilder})
: super(key: key);
@@ -462,10 +493,19 @@ class _ImagePaintState extends State {
final imageWidth = c.getDisplayWidth() * s;
final imageHeight = c.getDisplayHeight() * s;
final imageSize = Size(imageWidth, imageHeight);
- final imageWidget = CustomPaint(
- size: imageSize,
- painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
- );
+ late final Widget imageWidget;
+ if (widget.useTextureRender) {
+ imageWidget = SizedBox(
+ width: imageWidth,
+ height: imageHeight,
+ child: Obx(() => Texture(textureId: widget.textureId.value)),
+ );
+ } else {
+ imageWidget = CustomPaint(
+ size: imageSize,
+ painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
+ );
+ }
return NotificationListener(
onNotification: (notification) {
@@ -489,11 +529,31 @@ class _ImagePaintState extends State {
context, _buildListener(imageWidget), c.size, imageSize)),
));
} else {
- final imageWidget = CustomPaint(
- size: Size(c.size.width, c.size.height),
- painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
- );
- return mouseRegion(child: _buildListener(imageWidget));
+ late final Widget imageWidget;
+ if (c.size.width > 0 && c.size.height > 0) {
+ if (widget.useTextureRender) {
+ imageWidget = Stack(
+ children: [
+ Positioned(
+ left: c.x,
+ top: c.y,
+ width: c.getDisplayWidth() * s,
+ height: c.getDisplayHeight() * s,
+ child: Texture(textureId: widget.textureId.value),
+ )
+ ],
+ );
+ } else {
+ imageWidget = CustomPaint(
+ size: Size(c.size.width, c.size.height),
+ painter:
+ ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
+ );
+ }
+ return mouseRegion(child: _buildListener(imageWidget));
+ } else {
+ return Container();
+ }
}
}
@@ -685,40 +745,3 @@ class CursorPaint extends StatelessWidget {
);
}
}
-
-class ImagePainter extends CustomPainter {
- ImagePainter({
- required this.image,
- required this.x,
- required this.y,
- required this.scale,
- });
-
- ui.Image? image;
- double x;
- double y;
- double scale;
-
- @override
- void paint(Canvas canvas, Size size) {
- if (image == null) return;
- if (x.isNaN || y.isNaN) return;
- canvas.scale(scale, scale);
- // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
- // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
- var paint = Paint();
- if ((scale - 1.0).abs() > 0.001) {
- paint.filterQuality = FilterQuality.medium;
- if (scale > 10.00000) {
- paint.filterQuality = FilterQuality.high;
- }
- }
- canvas.drawImage(
- image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
- }
-
- @override
- bool shouldRepaint(CustomPainter oldDelegate) {
- return oldDelegate != this;
- }
-}
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 9b00b481f..0deb646c0 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -22,7 +22,10 @@ import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
- static const Color commonColor = MyTheme.accent;
+ static const Color blueColor = MyTheme.button;
+ static const Color hoverBlueColor = MyTheme.accent;
+ static const Color redColor = Colors.redAccent;
+ static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
@@ -70,6 +73,7 @@ class _ConnectionTabPageState extends State {
id: peerId,
menubarState: _menubarState,
switchUuid: params['switch_uuid'],
+ forceRelay: params['forceRelay'],
),
));
_update_remote_count();
@@ -104,8 +108,11 @@ class _ConnectionTabPageState extends State {
id: id,
menubarState: _menubarState,
switchUuid: switchUuid,
+ forceRelay: args['forceRelay'],
),
));
+ } else if (call.method == kWindowDisableGrabKeyboard) {
+ stateGlobal.grabKeyboard = false;
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
@@ -134,7 +141,7 @@ class _ConnectionTabPageState extends State {
width: stateGlobal.windowBorderWidth.value),
),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
@@ -280,7 +287,7 @@ class _ConnectionTabPageState extends State {
.map((entry) => entry.build(
context,
const MenuConfig(
- commonColor: _MenuTheme.commonColor,
+ commonColor: _MenuTheme.blueColor,
height: _MenuTheme.height,
dividerHeight: _MenuTheme.dividerHeight,
)))
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index b66a08e74..45591b79b 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -1,11 +1,13 @@
// original cm window in Sciter version.
import 'dart:async';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
+import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
@@ -47,8 +49,14 @@ class _DesktopServerPageState extends State
@override
void onWindowClose() {
- gFFI.serverModel.closeAll();
- gFFI.close();
+ Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
+ if (Platform.isMacOS) {
+ RdPlatformChannel.instance.terminate();
+ } else {
+ windowManager.setPreventClose(false);
+ windowManager.close();
+ }
+ });
super.onWindowClose();
}
@@ -71,7 +79,7 @@ class _DesktopServerPageState extends State
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
@@ -178,7 +186,7 @@ class ConnectionManagerState extends State {
windowManager.startDragging();
},
child: Container(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
),
),
),
diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart
new file mode 100644
index 000000000..3821b7e0d
--- /dev/null
+++ b/flutter/lib/desktop/widgets/dragable_divider.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter/src/widgets/placeholder.dart';
+
+class DraggableDivider extends StatefulWidget {
+ final Axis axis;
+ final double thickness;
+ final Color color;
+ final Function(double)? onPointerMove;
+ final VoidCallback? onHover;
+ final EdgeInsets padding;
+ const DraggableDivider({
+ super.key,
+ this.axis = Axis.horizontal,
+ this.thickness = 1.0,
+ this.color = const Color.fromARGB(200, 177, 175, 175),
+ this.onPointerMove,
+ this.padding = const EdgeInsets.symmetric(horizontal: 1.0),
+ this.onHover,
+ });
+
+ @override
+ State createState() => _DraggableDividerState();
+}
+
+class _DraggableDividerState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Listener(
+ onPointerMove: (event) {
+ final dl =
+ widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx;
+ widget.onPointerMove?.call(dl);
+ },
+ onPointerHover: (event) => widget.onHover?.call(),
+ child: MouseRegion(
+ cursor: SystemMouseCursors.resizeLeftRight,
+ child: Padding(
+ padding: widget.padding,
+ child: Container(
+ decoration: BoxDecoration(color: widget.color),
+ width: widget.axis == Axis.horizontal
+ ? double.infinity
+ : widget.thickness,
+ height: widget.axis == Axis.horizontal
+ ? widget.thickness
+ : double.infinity,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart
index 9598c3400..36128bf26 100644
--- a/flutter/lib/desktop/widgets/list_search_action_listener.dart
+++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart
@@ -55,6 +55,7 @@ class TimeoutStringBuffer {
}
ListSearchAction input(String ch) {
+ ch = ch.toLowerCase();
final curr = DateTime.now();
try {
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {
diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
index 666c9a6e2..3e85cb296 100644
--- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
+++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
@@ -5,6 +5,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
// Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame }
@@ -1391,22 +1393,20 @@ class PopupMenuButtonState extends State> {
onTap: widget.enabled ? showButtonMenu : null,
onHover: widget.onHover,
canRequestFocus: _canRequestFocus,
- radius: widget.splashRadius,
enableFeedback: enableFeedback,
child: widget.child,
),
);
}
- return IconButton(
- icon: widget.icon ?? Icon(Icons.adaptive.more),
- padding: widget.padding,
- splashRadius: widget.splashRadius,
- iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize,
+ return MenuButton(
+ child: widget.icon ?? Icon(Icons.adaptive.more),
tooltip:
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
onPressed: widget.enabled ? showButtonMenu : null,
enableFeedback: enableFeedback,
+ color: MyTheme.button,
+ hoverColor: MyTheme.accent,
);
}
}
diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart
new file mode 100644
index 000000000..df2c48ab4
--- /dev/null
+++ b/flutter/lib/desktop/widgets/menu_button.dart
@@ -0,0 +1,63 @@
+import 'package:flutter/material.dart';
+
+class MenuButton extends StatefulWidget {
+ final GestureTapCallback? onPressed;
+ final Color color;
+ final Color hoverColor;
+ final Color? splashColor;
+ final Widget child;
+ final String? tooltip;
+ final EdgeInsetsGeometry padding;
+ final bool enableFeedback;
+ const MenuButton({
+ super.key,
+ required this.onPressed,
+ required this.color,
+ required this.hoverColor,
+ required this.child,
+ this.splashColor,
+ this.tooltip = "",
+ this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6),
+ this.enableFeedback = true,
+ });
+
+ @override
+ State createState() => _MenuButtonState();
+}
+
+class _MenuButtonState extends State {
+ bool _isHover = false;
+ final double _borderRadius = 8.0;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: widget.padding,
+ child: Tooltip(
+ message: widget.tooltip,
+ child: Material(
+ type: MaterialType.transparency,
+ child: Ink(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(_borderRadius),
+ color: _isHover ? widget.hoverColor : widget.color,
+ ),
+ child: InkWell(
+ hoverColor: widget.hoverColor,
+ onHover: (val) {
+ setState(() {
+ _isHover = val;
+ });
+ },
+ borderRadius: BorderRadius.circular(_borderRadius),
+ splashColor: widget.splashColor,
+ enableFeedback: widget.enableFeedback,
+ onTap: widget.onPressed,
+ child: widget.child,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index 7db7d43aa..4f9a227bd 100644
--- a/flutter/lib/desktop/widgets/remote_menubar.dart
+++ b/flutter/lib/desktop/widgets/remote_menubar.dart
@@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:io';
-import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
@@ -22,7 +21,6 @@ import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
-import './material_mod_popup_menu.dart' as mod_menu;
import './kb_layout_type_chooser.dart';
class MenubarState {
@@ -94,10 +92,18 @@ class MenubarState {
}
class _MenubarTheme {
- static const Color commonColor = MyTheme.accent;
+ static const Color blueColor = MyTheme.button;
+ static const Color hoverBlueColor = MyTheme.accent;
+ static const Color redColor = Colors.redAccent;
+ static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
+
+ static const double buttonSize = 32;
+ static const double buttonHMargin = 3;
+ static const double buttonVMargin = 6;
+ static const double iconRadius = 8;
}
typedef DismissFunc = void Function();
@@ -276,7 +282,7 @@ class RemoteMenubar extends StatefulWidget {
final Function(Function(bool)) onEnterOrLeaveImageSetter;
final Function() onEnterOrLeaveImageCleaner;
- const RemoteMenubar({
+ RemoteMenubar({
Key? key,
required this.id,
required this.ffi,
@@ -292,7 +298,6 @@ class RemoteMenubar extends StatefulWidget {
class _RemoteMenubarState extends State {
late Debouncer _debouncerHide;
bool _isCursorOverImage = false;
- window_size.Screen? _screen;
final _fractionX = 0.5.obs;
final _dragging = false.obs;
@@ -343,7 +348,6 @@ class _RemoteMenubarState extends State {
@override
Widget build(BuildContext context) {
// No need to use future builder here.
- _updateScreen();
return Align(
alignment: Alignment.topCenter,
child: Obx(() => show.value
@@ -371,6 +375,591 @@ class _RemoteMenubarState extends State {
});
}
+ Widget _buildMenubar(BuildContext context) {
+ final List menubarItems = [];
+ if (!isWebDesktop) {
+ menubarItems.add(_PinMenu(state: widget.state));
+ menubarItems.add(
+ _FullscreenMenu(state: widget.state, setFullscreen: _setFullscreen));
+ menubarItems.add(_MobileActionMenu(ffi: widget.ffi));
+ }
+ menubarItems.add(_MonitorMenu(id: widget.id, ffi: widget.ffi));
+ menubarItems
+ .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
+ menubarItems.add(_DisplayMenu(
+ id: widget.id,
+ ffi: widget.ffi,
+ state: widget.state,
+ setFullscreen: _setFullscreen,
+ ));
+ menubarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
+ if (!isWeb) {
+ menubarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
+ menubarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
+ }
+ menubarItems.add(_RecordMenu());
+ menubarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(10)),
+ ),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Theme(
+ data: themeData(),
+ child: MenuBar(
+ children: [
+ SizedBox(width: _MenubarTheme.buttonHMargin),
+ ...menubarItems,
+ SizedBox(width: _MenubarTheme.buttonHMargin)
+ ],
+ ),
+ )),
+ ),
+ _buildDraggableShowHide(context),
+ ],
+ );
+ }
+
+ ThemeData themeData() {
+ return Theme.of(context).copyWith(
+ menuButtonTheme: MenuButtonThemeData(
+ style: ButtonStyle(
+ minimumSize: MaterialStatePropertyAll(Size(64, 36)),
+ textStyle: MaterialStatePropertyAll(
+ TextStyle(fontWeight: FontWeight.normal)))),
+ dividerTheme: DividerThemeData(space: 4),
+ );
+ }
+}
+
+class _PinMenu extends StatelessWidget {
+ final MenubarState state;
+ const _PinMenu({Key? key, required this.state}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Obx(
+ () => _IconMenuButton(
+ assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
+ tooltip: state.pin ? 'Unpin menubar' : 'Pin menubar',
+ onPressed: state.switchPin,
+ color: state.pin ? _MenubarTheme.blueColor : Colors.grey[800]!,
+ hoverColor:
+ state.pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!,
+ ),
+ );
+ }
+}
+
+class _FullscreenMenu extends StatelessWidget {
+ final MenubarState state;
+ final Function(bool) setFullscreen;
+ bool get isFullscreen => stateGlobal.fullscreen;
+ const _FullscreenMenu(
+ {Key? key, required this.state, required this.setFullscreen})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return _IconMenuButton(
+ assetName:
+ isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg",
+ tooltip: isFullscreen ? 'Exit Fullscreen' : 'Fullscreen',
+ onPressed: () => setFullscreen(!isFullscreen),
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ );
+ }
+}
+
+class _MobileActionMenu extends StatelessWidget {
+ final FFI ffi;
+ const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ if (!ffi.ffiModel.isPeerAndroid) return Offstage();
+ return _IconMenuButton(
+ assetName: 'assets/actions_mobile.svg',
+ tooltip: 'Mobile Actions',
+ onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ );
+ }
+}
+
+class _MonitorMenu extends StatelessWidget {
+ final String id;
+ final FFI ffi;
+ const _MonitorMenu({Key? key, required this.id, required this.ffi})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ if (stateGlobal.displaysCount.value < 2) return Offstage();
+ return _IconSubmenuButton(
+ icon: icon(),
+ ffi: ffi,
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ menuStyle: MenuStyle(
+ padding:
+ MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
+ menuChildren: [Row(children: displays(context))]);
+ }
+
+ icon() {
+ final pi = ffi.ffiModel.pi;
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ SvgPicture.asset(
+ "assets/display.svg",
+ color: Colors.white,
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 3.9),
+ child: Obx(() {
+ RxInt display = CurrentDisplayState.find(id);
+ return Text(
+ '${display.value + 1}/${pi.displays.length}',
+ style: const TextStyle(color: Colors.white, fontSize: 8),
+ );
+ }),
+ )
+ ],
+ );
+ }
+
+ List displays(BuildContext context) {
+ final List rowChildren = [];
+ final pi = ffi.ffiModel.pi;
+ for (int i = 0; i < pi.displays.length; i++) {
+ rowChildren.add(_IconMenuButton(
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ tooltip: "",
+ hMargin: 6,
+ vMargin: 12,
+ icon: Container(
+ alignment: AlignmentDirectional.center,
+ constraints: const BoxConstraints(minHeight: _MenubarTheme.height),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ SvgPicture.asset(
+ "assets/display.svg",
+ color: Colors.white,
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 3.5 /*2.5*/),
+ child: Text(
+ (i + 1).toString(),
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ )
+ ],
+ ),
+ ),
+ onPressed: () {
+ _menuDismissCallback(ffi);
+ RxInt display = CurrentDisplayState.find(id);
+ if (display.value != i) {
+ bind.sessionSwitchDisplay(id: id, value: i);
+ }
+ },
+ ));
+ }
+ return rowChildren;
+ }
+}
+
+class _ControlMenu extends StatelessWidget {
+ final String id;
+ final FFI ffi;
+ final MenubarState state;
+ _ControlMenu(
+ {Key? key, required this.id, required this.ffi, required this.state})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return _IconSubmenuButton(
+ svg: "assets/actions.svg",
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ ffi: ffi,
+ menuChildren: [
+ osPassword(),
+ transferFile(context),
+ tcpTunneling(context),
+ note(),
+ Divider(),
+ ctrlAltDel(),
+ restart(),
+ blockUserInput(),
+ switchSides(),
+ refresh(),
+ ]);
+ }
+
+ osPassword() {
+ return _MenuItemButton(
+ child: Text(translate('OS Password')),
+ trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
+ ffi: ffi,
+ onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager));
+ }
+
+ _showSetOSPassword(
+ String id, bool login, OverlayDialogManager dialogManager) async {
+ final controller = TextEditingController();
+ var password =
+ await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
+ var autoLogin =
+ await bind.sessionGetOption(id: id, arg: 'auto-login') != '';
+ controller.text = password;
+ dialogManager.show((setState, close) {
+ submit() {
+ var text = controller.text.trim();
+ bind.sessionPeerOption(id: id, name: 'os-password', value: text);
+ bind.sessionPeerOption(
+ id: id, name: 'auto-login', value: autoLogin ? 'Y' : '');
+ if (text != '' && login) {
+ bind.sessionInputOsPassword(id: id, value: text);
+ }
+ close();
+ }
+
+ return CustomAlertDialog(
+ title: Text(translate('OS Password')),
+ content: Column(mainAxisSize: MainAxisSize.min, children: [
+ PasswordWidget(controller: controller),
+ CheckboxListTile(
+ contentPadding: const EdgeInsets.all(0),
+ dense: true,
+ controlAffinity: ListTileControlAffinity.leading,
+ title: Text(
+ translate('Auto Login'),
+ ),
+ value: autoLogin,
+ onChanged: (v) {
+ if (v == null) return;
+ setState(() => autoLogin = v);
+ },
+ ),
+ ]),
+ actions: [
+ dialogButton('Cancel', onPressed: close, isOutline: true),
+ dialogButton('OK', onPressed: submit),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+ }
+
+ transferFile(BuildContext context) {
+ return _MenuItemButton(
+ child: Text(translate('Transfer File')),
+ ffi: ffi,
+ onPressed: () => connect(context, id, isFileTransfer: true));
+ }
+
+ tcpTunneling(BuildContext context) {
+ return _MenuItemButton(
+ child: Text(translate('TCP Tunneling')),
+ ffi: ffi,
+ onPressed: () => connect(context, id, isTcpTunneling: true));
+ }
+
+ note() {
+ final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn");
+ final visible = auditServer.isNotEmpty;
+ if (!visible) return Offstage();
+ return _MenuItemButton(
+ child: Text(translate('Note')),
+ ffi: ffi,
+ onPressed: () => _showAuditDialog(id, ffi.dialogManager),
+ );
+ }
+
+ _showAuditDialog(String id, dialogManager) async {
+ final controller = TextEditingController();
+ dialogManager.show((setState, close) {
+ submit() {
+ var text = controller.text.trim();
+ if (text != '') {
+ bind.sessionSendNote(id: id, note: text);
+ }
+ close();
+ }
+
+ late final focusNode = FocusNode(
+ onKey: (FocusNode node, RawKeyEvent evt) {
+ if (evt.logicalKey.keyLabel == 'Enter') {
+ if (evt is RawKeyDownEvent) {
+ int pos = controller.selection.base.offset;
+ controller.text =
+ '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
+ controller.selection =
+ TextSelection.fromPosition(TextPosition(offset: pos + 1));
+ }
+ return KeyEventResult.handled;
+ }
+ if (evt.logicalKey.keyLabel == 'Esc') {
+ if (evt is RawKeyDownEvent) {
+ close();
+ }
+ return KeyEventResult.handled;
+ } else {
+ return KeyEventResult.ignored;
+ }
+ },
+ );
+
+ return CustomAlertDialog(
+ title: Text(translate('Note')),
+ content: SizedBox(
+ width: 250,
+ height: 120,
+ child: TextField(
+ autofocus: true,
+ keyboardType: TextInputType.multiline,
+ textInputAction: TextInputAction.newline,
+ decoration: const InputDecoration.collapsed(
+ hintText: 'input note here',
+ ),
+ maxLines: null,
+ maxLength: 256,
+ controller: controller,
+ focusNode: focusNode,
+ )),
+ actions: [
+ dialogButton('Cancel', onPressed: close, isOutline: true),
+ dialogButton('OK', onPressed: submit)
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+ }
+
+ ctrlAltDel() {
+ final perms = ffi.ffiModel.permissions;
+ final pi = ffi.ffiModel.pi;
+ final visible = perms['keyboard'] != false &&
+ (pi.platform == kPeerPlatformLinux || pi.sasEnabled);
+ if (!visible) return Offstage();
+ return _MenuItemButton(
+ child: Text('${translate("Insert")} Ctrl + Alt + Del'),
+ ffi: ffi,
+ onPressed: () => bind.sessionCtrlAltDel(id: id));
+ }
+
+ restart() {
+ final perms = ffi.ffiModel.permissions;
+ final pi = ffi.ffiModel.pi;
+ final visible = perms['restart'] != false &&
+ (pi.platform == kPeerPlatformLinux ||
+ pi.platform == kPeerPlatformWindows ||
+ pi.platform == kPeerPlatformMacOS);
+ if (!visible) return Offstage();
+ return _MenuItemButton(
+ child: Text(translate('Restart Remote Device')),
+ ffi: ffi,
+ onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager));
+ }
+
+ blockUserInput() {
+ final perms = ffi.ffiModel.permissions;
+ final pi = ffi.ffiModel.pi;
+ final visible =
+ perms['keyboard'] != false && pi.platform == kPeerPlatformWindows;
+ if (!visible) return Offstage();
+ return _MenuItemButton(
+ child: Obx(() => Text(translate(
+ '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))),
+ ffi: ffi,
+ onPressed: () {
+ RxBool blockInput = BlockInputState.find(id);
+ bind.sessionToggleOption(
+ id: id, value: '${blockInput.value ? 'un' : ''}block-input');
+ blockInput.value = !blockInput.value;
+ });
+ }
+
+ switchSides() {
+ final perms = ffi.ffiModel.permissions;
+ final pi = ffi.ffiModel.pi;
+ final visible = perms['keyboard'] != false &&
+ pi.platform != kPeerPlatformAndroid &&
+ pi.platform != kPeerPlatformMacOS &&
+ version_cmp(pi.version, '1.2.0') >= 0;
+ if (!visible) return Offstage();
+ return _MenuItemButton(
+ child: Text(translate('Switch Sides')),
+ ffi: ffi,
+ onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager));
+ }
+
+ void _showConfirmSwitchSidesDialog(
+ String id, OverlayDialogManager dialogManager) async {
+ dialogManager.show((setState, close) {
+ submit() async {
+ await bind.sessionSwitchSides(id: id);
+ closeConnection(id: id);
+ }
+
+ return CustomAlertDialog(
+ content: msgboxContent('info', 'Switch Sides',
+ 'Please confirm if you want to share your desktop?'),
+ actions: [
+ dialogButton('Cancel', onPressed: close, isOutline: true),
+ dialogButton('OK', onPressed: submit),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+ }
+
+ refresh() {
+ final pi = ffi.ffiModel.pi;
+ final visible = pi.version.isNotEmpty;
+ if (!visible) return Offstage();
+ return _MenuItemButton(
+ child: Text(translate('Refresh')),
+ ffi: ffi,
+ onPressed: () => bind.sessionRefresh(id: id));
+ }
+}
+
+class _DisplayMenu extends StatefulWidget {
+ final String id;
+ final FFI ffi;
+ final MenubarState state;
+ final Function(bool) setFullscreen;
+ _DisplayMenu(
+ {Key? key,
+ required this.id,
+ required this.ffi,
+ required this.state,
+ required this.setFullscreen})
+ : super(key: key);
+
+ @override
+ State<_DisplayMenu> createState() => _DisplayMenuState();
+}
+
+class _DisplayMenuState extends State<_DisplayMenu> {
+ window_size.Screen? _screen;
+
+ bool get isFullscreen => stateGlobal.fullscreen;
+
+ int get windowId => stateGlobal.windowId;
+
+ Map get perms => widget.ffi.ffiModel.permissions;
+
+ PeerInfo get pi => widget.ffi.ffiModel.pi;
+
+ @override
+ Widget build(BuildContext context) {
+ _updateScreen();
+ return _IconSubmenuButton(
+ svg: "assets/display.svg",
+ ffi: widget.ffi,
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ menuChildren: [
+ adjustWindow(),
+ viewStyle(),
+ scrollStyle(),
+ imageQuality(),
+ codec(),
+ resolutions(),
+ Divider(),
+ showRemoteCursor(),
+ zoomCursor(),
+ showQualityMonitor(),
+ mute(),
+ fileCopyAndPaste(),
+ disableClipboard(),
+ lockAfterSessionEnd(),
+ privacyMode(),
+ ]);
+ }
+
+ adjustWindow() {
+ final visible = _isWindowCanBeAdjusted();
+ if (!visible) return Offstage();
+ return Column(
+ children: [
+ _MenuItemButton(
+ child: Text(translate('Adjust Window')),
+ onPressed: _doAdjustWindow,
+ ffi: widget.ffi),
+ Divider(),
+ ],
+ );
+ }
+
+ _doAdjustWindow() async {
+ await _updateScreen();
+ if (_screen != null) {
+ widget.setFullscreen(false);
+ double scale = _screen!.scaleFactor;
+ final wndRect = await WindowController.fromWindowId(windowId).getFrame();
+ final mediaSize = MediaQueryData.fromWindow(ui.window).size;
+ // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
+ // https://stackoverflow.com/a/7561083
+ double magicWidth =
+ wndRect.right - wndRect.left - mediaSize.width * scale;
+ double magicHeight =
+ wndRect.bottom - wndRect.top - mediaSize.height * scale;
+
+ final canvasModel = widget.ffi.canvasModel;
+ final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
+ canvasModel.windowBorderWidth * 2) *
+ scale +
+ magicWidth;
+ final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
+ canvasModel.tabBarHeight +
+ canvasModel.windowBorderWidth * 2) *
+ scale +
+ magicHeight;
+ double left = wndRect.left + (wndRect.width - width) / 2;
+ double top = wndRect.top + (wndRect.height - height) / 2;
+
+ Rect frameRect = _screen!.frame;
+ if (!isFullscreen) {
+ frameRect = _screen!.visibleFrame;
+ }
+ if (left < frameRect.left) {
+ left = frameRect.left;
+ }
+ if (top < frameRect.top) {
+ top = frameRect.top;
+ }
+ if ((left + width) > frameRect.right) {
+ left = frameRect.right - width;
+ }
+ if ((top + height) > frameRect.bottom) {
+ top = frameRect.bottom - height;
+ }
+ await WindowController.fromWindowId(windowId)
+ .setFrame(Rect.fromLTWH(left, top, width, height));
+ }
+ }
+
_updateScreen() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
@@ -391,620 +980,11 @@ class _RemoteMenubarState extends State {
}
}
- Widget _buildPointerTrackWidget(Widget child) {
- return Listener(
- onPointerHover: (PointerHoverEvent e) =>
- widget.ffi.inputModel.lastMousePos = e.position,
- child: MouseRegion(
- child: child,
- ),
- );
- }
-
- _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos();
-
- Widget _buildMenubar(BuildContext context) {
- final List menubarItems = [];
- if (!isWebDesktop) {
- menubarItems.add(_buildPinMenubar(context));
- menubarItems.add(_buildFullscreen(context));
- if (widget.ffi.ffiModel.isPeerAndroid) {
- menubarItems.add(IconButton(
- tooltip: translate('Mobile Actions'),
- color: _MenubarTheme.commonColor,
- icon: const Icon(Icons.build),
- onPressed: () {
- widget.ffi.dialogManager
- .toggleMobileActionsOverlay(ffi: widget.ffi);
- },
- ));
- }
+ _isWindowCanBeAdjusted() {
+ if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) {
+ return false;
}
- menubarItems.add(_buildMonitor(context));
- menubarItems.add(_buildControl(context));
- menubarItems.add(_buildDisplay(context));
- menubarItems.add(_buildKeyboard(context));
- if (!isWeb) {
- menubarItems.add(_buildChat(context));
- menubarItems.add(_buildVoiceCall(context));
- }
- menubarItems.add(_buildRecording(context));
- menubarItems.add(_buildClose(context));
- return PopupMenuTheme(
- data: const PopupMenuThemeData(
- textStyle: TextStyle(color: _MenubarTheme.commonColor)),
- child: Column(mainAxisSize: MainAxisSize.min, children: [
- Container(
- decoration: BoxDecoration(
- color: Colors.white,
- border: Border.all(color: MyTheme.border),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: menubarItems,
- )),
- _buildDraggableShowHide(context),
- ]));
- }
-
- Widget _buildPinMenubar(BuildContext context) {
- return Obx(() => IconButton(
- tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'),
- onPressed: () {
- widget.state.switchPin();
- },
- icon: Obx(() => Transform.rotate(
- angle: pin ? math.pi / 4 : 0,
- child: Icon(
- Icons.push_pin,
- color: pin ? _MenubarTheme.commonColor : Colors.grey,
- ))),
- ));
- }
-
- Widget _buildFullscreen(BuildContext context) {
- return IconButton(
- tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
- onPressed: () {
- _setFullscreen(!isFullscreen);
- },
- icon: isFullscreen
- ? const Icon(
- Icons.fullscreen_exit,
- color: _MenubarTheme.commonColor,
- )
- : const Icon(
- Icons.fullscreen,
- color: _MenubarTheme.commonColor,
- ),
- );
- }
-
- Widget _buildMonitor(BuildContext context) {
- final pi = widget.ffi.ffiModel.pi;
- return mod_menu.PopupMenuButton(
- tooltip: translate('Select Monitor'),
- padding: EdgeInsets.zero,
- position: mod_menu.PopupMenuPosition.under,
- icon: Stack(
- alignment: Alignment.center,
- children: [
- const Icon(
- Icons.personal_video,
- color: _MenubarTheme.commonColor,
- ),
- Padding(
- padding: const EdgeInsets.only(bottom: 3.9),
- child: Obx(() {
- RxInt display = CurrentDisplayState.find(widget.id);
- return Text(
- '${display.value + 1}/${pi.displays.length}',
- style: const TextStyle(
- color: _MenubarTheme.commonColor, fontSize: 8),
- );
- }),
- )
- ],
- ),
- itemBuilder: (BuildContext context) {
- final List rowChildren = [];
- for (int i = 0; i < pi.displays.length; i++) {
- rowChildren.add(
- Stack(
- alignment: Alignment.center,
- children: [
- const Icon(
- Icons.personal_video,
- color: _MenubarTheme.commonColor,
- ),
- TextButton(
- child: Container(
- alignment: AlignmentDirectional.center,
- constraints:
- const BoxConstraints(minHeight: _MenubarTheme.height),
- child: Padding(
- padding: const EdgeInsets.only(bottom: 2.5),
- child: Text(
- (i + 1).toString(),
- style:
- const TextStyle(color: _MenubarTheme.commonColor),
- ),
- )),
- onPressed: () {
- if (Navigator.canPop(context)) {
- Navigator.pop(context);
- _menuDismissCallback();
- }
- RxInt display = CurrentDisplayState.find(widget.id);
- if (display.value != i) {
- bind.sessionSwitchDisplay(id: widget.id, value: i);
- }
- },
- )
- ],
- ),
- );
- }
- return >[
- mod_menu.PopupMenuItem(
- height: _MenubarTheme.height,
- padding: EdgeInsets.zero,
- child: _buildPointerTrackWidget(
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: rowChildren,
- ),
- ),
- )
- ];
- },
- );
- }
-
- Widget _buildControl(BuildContext context) {
- return mod_menu.PopupMenuButton(
- padding: EdgeInsets.zero,
- icon: const Icon(
- Icons.bolt,
- color: _MenubarTheme.commonColor,
- ),
- tooltip: translate('Control Actions'),
- position: mod_menu.PopupMenuPosition.under,
- itemBuilder: (BuildContext context) => _getControlMenu(context)
- .map((entry) => entry.build(
- context,
- const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
- height: _MenubarTheme.height,
- dividerHeight: _MenubarTheme.dividerHeight,
- )))
- .expand((i) => i)
- .toList(),
- );
- }
-
- Widget _buildDisplay(BuildContext context) {
- return FutureBuilder(future: () async {
- widget.state.viewStyle.value =
- await bind.sessionGetViewStyle(id: widget.id) ?? '';
- final supportedHwcodec =
- await bind.sessionSupportedHwcodec(id: widget.id);
- return {'supportedHwcodec': supportedHwcodec};
- }(), builder: (context, snapshot) {
- if (snapshot.hasData) {
- return Obx(() {
- final remoteCount = RemoteCountState.find().value;
- return mod_menu.PopupMenuButton(
- padding: EdgeInsets.zero,
- icon: const Icon(
- Icons.tv,
- color: _MenubarTheme.commonColor,
- ),
- tooltip: translate('Display Settings'),
- position: mod_menu.PopupMenuPosition.under,
- menuWrapper: _buildPointerTrackWidget,
- itemBuilder: (BuildContext context) =>
- _getDisplayMenu(snapshot.data!, remoteCount)
- .map((entry) => entry.build(
- context,
- const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
- height: _MenubarTheme.height,
- dividerHeight: _MenubarTheme.dividerHeight,
- )))
- .expand((i) => i)
- .toList(),
- );
- });
- } else {
- return const Offstage();
- }
- });
- }
-
- Widget _buildKeyboard(BuildContext context) {
- FfiModel ffiModel = Provider.of(context);
- if (ffiModel.permissions['keyboard'] == false) {
- return Offstage();
- }
- return mod_menu.PopupMenuButton(
- padding: EdgeInsets.zero,
- icon: const Icon(
- Icons.keyboard,
- color: _MenubarTheme.commonColor,
- ),
- tooltip: translate('Keyboard Settings'),
- position: mod_menu.PopupMenuPosition.under,
- itemBuilder: (BuildContext context) => _getKeyboardMenu()
- .map((entry) => entry.build(
- context,
- const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
- height: _MenubarTheme.height,
- dividerHeight: _MenubarTheme.dividerHeight,
- )))
- .expand((i) => i)
- .toList(),
- );
- }
-
- Widget _buildRecording(BuildContext context) {
- return Consumer(builder: ((context, value, child) {
- if (value.permissions['recording'] != false) {
- return Consumer(
- builder: (context, value, child) => IconButton(
- tooltip: value.start
- ? translate('Stop session recording')
- : translate('Start session recording'),
- onPressed: () => value.toggle(),
- icon: value.start
- ? Icon(
- Icons.pause_circle_filled,
- color: _MenubarTheme.commonColor,
- )
- : SvgPicture.asset(
- "assets/record_screen.svg",
- color: _MenubarTheme.commonColor,
- width: Theme.of(context).iconTheme.size ?? 22.0,
- height: Theme.of(context).iconTheme.size ?? 22.0,
- ),
- ));
- } else {
- return Offstage();
- }
- }));
- }
-
- Widget _buildClose(BuildContext context) {
- return IconButton(
- tooltip: translate('Close'),
- onPressed: () {
- clientClose(widget.id, widget.ffi.dialogManager);
- },
- icon: const Icon(
- Icons.close,
- color: _MenubarTheme.commonColor,
- ),
- );
- }
-
- final _chatButtonKey = GlobalKey();
- Widget _buildChat(BuildContext context) {
- FfiModel ffiModel = Provider.of(context);
- return mod_menu.PopupMenuButton(
- key: _chatButtonKey,
- padding: EdgeInsets.zero,
- icon: SvgPicture.asset(
- "assets/chat.svg",
- color: _MenubarTheme.commonColor,
- width: Theme.of(context).iconTheme.size ?? 24.0,
- height: Theme.of(context).iconTheme.size ?? 24.0,
- ),
- tooltip: translate('Chat'),
- position: mod_menu.PopupMenuPosition.under,
- itemBuilder: (BuildContext context) => _getChatMenu(context)
- .map((entry) => entry.build(
- context,
- const MenuConfig(
- commonColor: _MenubarTheme.commonColor,
- height: _MenubarTheme.height,
- dividerHeight: _MenubarTheme.dividerHeight,
- )))
- .expand((i) => i)
- .toList(),
- );
- }
-
- Widget _getVoiceCallIcon() {
- switch (widget.ffi.chatModel.voiceCallStatus.value) {
- case VoiceCallStatus.waitingForResponse:
- return IconButton(
- onPressed: () {
- widget.ffi.chatModel.closeVoiceCall(widget.id);
- },
- icon: SvgPicture.asset(
- "assets/voice_call_waiting.svg",
- color: Colors.red,
- width: Theme.of(context).iconTheme.size ?? 20.0,
- height: Theme.of(context).iconTheme.size ?? 20.0,
- ));
- case VoiceCallStatus.connected:
- return IconButton(
- onPressed: () {
- widget.ffi.chatModel.closeVoiceCall(widget.id);
- },
- icon: Icon(
- Icons.phone_disabled_rounded,
- color: Colors.red,
- size: Theme.of(context).iconTheme.size ?? 22.0,
- ),
- );
- default:
- return const Offstage();
- }
- }
-
- String? _getVoiceCallTooltip() {
- switch (widget.ffi.chatModel.voiceCallStatus.value) {
- case VoiceCallStatus.waitingForResponse:
- return "Waiting";
- case VoiceCallStatus.connected:
- return "Disconnect";
- default:
- return null;
- }
- }
-
- Widget _buildVoiceCall(BuildContext context) {
- return Obx(
- () {
- final tooltipText = _getVoiceCallTooltip();
- return tooltipText == null
- ? const Offstage()
- : IconButton(
- padding: EdgeInsets.zero,
- icon: _getVoiceCallIcon(),
- tooltip: translate(tooltipText),
- onPressed: () => bind.sessionRequestVoiceCall(id: widget.id),
- );
- },
- );
- }
-
- List> _getChatMenu(BuildContext context) {
- final List> chatMenu = [];
- const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
- chatMenu.addAll([
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Text chat'),
- style: style,
- ),
- proc: () {
- RenderBox? renderBox =
- _chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
-
- Offset? initPos;
- if (renderBox != null) {
- final pos = renderBox.localToGlobal(Offset.zero);
- initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
- }
-
- widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
- widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
- },
- padding: padding,
- dismissOnClicked: true,
- ),
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Voice call'),
- style: style,
- ),
- proc: () {
- // Request a voice call.
- bind.sessionRequestVoiceCall(id: widget.id);
- },
- padding: padding,
- dismissOnClicked: true,
- ),
- ]);
- return chatMenu;
- }
-
- List> _getControlMenu(BuildContext context) {
- final pi = widget.ffi.ffiModel.pi;
- final perms = widget.ffi.ffiModel.permissions;
- final peer_version = widget.ffi.ffiModel.pi.version;
- const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
- final List> displayMenu = [];
- displayMenu.addAll([
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Container(
- alignment: AlignmentDirectional.center,
- height: _MenubarTheme.height,
- child: Row(
- children: [
- Text(
- translate('OS Password'),
- style: style,
- ),
- Expanded(
- child: Align(
- alignment: Alignment.centerRight,
- child: Transform.scale(
- scale: 0.8,
- child: IconButton(
- padding: EdgeInsets.zero,
- icon: const Icon(Icons.edit),
- onPressed: () {
- if (Navigator.canPop(context)) {
- Navigator.pop(context);
- _menuDismissCallback();
- }
- showSetOSPassword(
- widget.id, false, widget.ffi.dialogManager);
- })),
- ))
- ],
- )),
- proc: () {
- showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Transfer File'),
- style: style,
- ),
- proc: () {
- connect(context, widget.id, isFileTransfer: true);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('TCP Tunneling'),
- style: style,
- ),
- padding: padding,
- proc: () {
- connect(context, widget.id, isTcpTunneling: true);
- },
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- ]);
- // {handler.get_audit_server() && {translate('Note')} }
- final auditServer =
- bind.sessionGetAuditServerSync(id: widget.id, typ: "conn");
- if (auditServer.isNotEmpty) {
- displayMenu.add(
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Note'),
- style: style,
- ),
- proc: () {
- showAuditDialog(widget.id, widget.ffi.dialogManager);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- );
- }
- displayMenu.add(MenuEntryDivider());
- if (perms['keyboard'] != false) {
- if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
- displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding,
- dismissCallback: _menuDismissCallback));
- }
- }
- if (perms['restart'] != false &&
- (pi.platform == kPeerPlatformLinux ||
- pi.platform == kPeerPlatformWindows ||
- pi.platform == kPeerPlatformMacOS)) {
- displayMenu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Restart Remote Device'),
- style: style,
- ),
- proc: () {
- showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
-
- if (perms['keyboard'] != false) {
- displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding,
- dismissCallback: _menuDismissCallback));
-
- if (pi.platform == kPeerPlatformWindows) {
- displayMenu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Obx(() => Text(
- translate(
- '${BlockInputState.find(widget.id).value ? 'Unb' : 'B'}lock user input'),
- style: style,
- )),
- proc: () {
- RxBool blockInput = BlockInputState.find(widget.id);
- bind.sessionToggleOption(
- id: widget.id,
- value: '${blockInput.value ? 'un' : ''}block-input');
- blockInput.value = !blockInput.value;
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
- if (pi.platform != kPeerPlatformAndroid &&
- pi.platform != kPeerPlatformMacOS && // unsupport yet
- version_cmp(peer_version, '1.2.0') >= 0) {
- displayMenu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Switch Sides'),
- style: style,
- ),
- proc: () =>
- showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager),
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
- }
-
- if (pi.version.isNotEmpty) {
- displayMenu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Refresh'),
- style: style,
- ),
- proc: () {
- bind.sessionRefresh(id: widget.id);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
-
- if (!isWebDesktop) {
- // if (perms['keyboard'] != false && perms['clipboard'] != false) {
- // displayMenu.add(MenuEntryButton(
- // childBuilder: (TextStyle? style) => Text(
- // translate('Paste'),
- // style: style,
- // ),
- // proc: () {
- // () async {
- // ClipboardData? data =
- // await Clipboard.getData(Clipboard.kTextPlain);
- // if (data != null && data.text != null) {
- // bind.sessionInputString(id: widget.id, value: data.text ?? '');
- // }
- // }();
- // },
- // padding: padding,
- // dismissOnClicked: true,
- // dismissCallback: _menuDismissCallback,
- // ));
- // }
- }
- return displayMenu;
- }
-
- bool _isWindowCanBeAdjusted(int remoteCount) {
+ final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
@@ -1030,312 +1010,277 @@ class _RemoteMenubarState extends State {
selfHeight > (requiredHeight * scale);
}
- List> _getDisplayMenu(
- dynamic futureData, int remoteCount) {
- const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
- final peer_version = widget.ffi.ffiModel.pi.version;
- final displayMenu = [
- RemoteMenuEntry.viewStyle(
- widget.id,
- widget.ffi,
- padding,
- dismissCallback: _menuDismissCallback,
- rxViewStyle: widget.state.viewStyle,
- ),
- MenuEntryDivider(),
- MenuEntryRadios(
- text: translate('Image Quality'),
- optionsGetter: () => [
- MenuEntryRadioOption(
- text: translate('Good image quality'),
+ viewStyle() {
+ return futureBuilder(future: () async {
+ final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? '';
+ widget.state.viewStyle.value = viewStyle;
+ return viewStyle;
+ }(), hasData: (data) {
+ final groupValue = data as String;
+ onChanged(String? value) async {
+ if (value == null) return;
+ await bind.sessionSetViewStyle(id: widget.id, value: value);
+ widget.state.viewStyle.value = value;
+ widget.ffi.canvasModel.updateViewStyle();
+ }
+
+ return Column(children: [
+ _RadioMenuButton(
+ child: Text(translate('Scale original')),
+ value: kRemoteViewStyleOriginal,
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
+ ),
+ _RadioMenuButton(
+ child: Text(translate('Scale adaptive')),
+ value: kRemoteViewStyleAdaptive,
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
+ ),
+ Divider(),
+ ]);
+ });
+ }
+
+ scrollStyle() {
+ final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal;
+ if (!visible) return Offstage();
+ return futureBuilder(future: () async {
+ final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? '';
+ return scrollStyle;
+ }(), hasData: (data) {
+ final groupValue = data as String;
+ onChange(String? value) async {
+ if (value == null) return;
+ await bind.sessionSetScrollStyle(id: widget.id, value: value);
+ widget.ffi.canvasModel.updateScrollStyle();
+ }
+
+ final enabled = widget.ffi.canvasModel.imageOverflow.value;
+ return Column(children: [
+ _RadioMenuButton(
+ child: Text(translate('ScrollAuto')),
+ value: kRemoteScrollStyleAuto,
+ groupValue: groupValue,
+ onChanged: enabled ? (value) => onChange(value) : null,
+ ffi: widget.ffi,
+ ),
+ _RadioMenuButton(
+ child: Text(translate('Scrollbar')),
+ value: kRemoteScrollStyleBar,
+ groupValue: groupValue,
+ onChanged: enabled ? (value) => onChange(value) : null,
+ ffi: widget.ffi,
+ ),
+ Divider(),
+ ]);
+ });
+ }
+
+ imageQuality() {
+ return futureBuilder(future: () async {
+ final imageQuality =
+ await bind.sessionGetImageQuality(id: widget.id) ?? '';
+ return imageQuality;
+ }(), hasData: (data) {
+ final groupValue = data as String;
+ onChanged(String? value) async {
+ if (value == null) return;
+ await bind.sessionSetImageQuality(id: widget.id, value: value);
+ }
+
+ return SubmenuButton(
+ child: Text(translate('Image Quality')),
+ menuChildren: [
+ _RadioMenuButton(
+ child: Text(translate('Good image quality')),
value: kRemoteImageQualityBest,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
),
- MenuEntryRadioOption(
- text: translate('Balanced'),
+ _RadioMenuButton(
+ child: Text(translate('Balanced')),
value: kRemoteImageQualityBalanced,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
),
- MenuEntryRadioOption(
- text: translate('Optimize reaction time'),
+ _RadioMenuButton(
+ child: Text(translate('Optimize reaction time')),
value: kRemoteImageQualityLow,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
),
- MenuEntryRadioOption(
- text: translate('Custom'),
+ _RadioMenuButton(
+ child: Text(translate('Custom')),
value: kRemoteImageQualityCustom,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- ],
- curOptionGetter: () async =>
- // null means peer id is not found, which there's no need to care about
- await bind.sessionGetImageQuality(id: widget.id) ?? '',
- optionSetter: (String oldValue, String newValue) async {
- if (oldValue != newValue) {
- await bind.sessionSetImageQuality(id: widget.id, value: newValue);
- }
-
- double qualityInitValue = 50;
- double fpsInitValue = 30;
- bool qualitySet = false;
- bool fpsSet = false;
- setCustomValues({double? quality, double? fps}) async {
- if (quality != null) {
- qualitySet = true;
- await bind.sessionSetCustomImageQuality(
- id: widget.id, value: quality.toInt());
- }
- if (fps != null) {
- fpsSet = true;
- await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt());
- }
- if (!qualitySet) {
- qualitySet = true;
- await bind.sessionSetCustomImageQuality(
- id: widget.id, value: qualityInitValue.toInt());
- }
- if (!fpsSet) {
- fpsSet = true;
- await bind.sessionSetCustomFps(
- id: widget.id, fps: fpsInitValue.toInt());
- }
- }
-
- if (newValue == kRemoteImageQualityCustom) {
- final btnClose = dialogButton('Close', onPressed: () async {
- await setCustomValues();
- widget.ffi.dialogManager.dismissAll();
- });
-
- // quality
- final quality =
- await bind.sessionGetCustomImageQuality(id: widget.id);
- qualityInitValue = quality != null && quality.isNotEmpty
- ? quality[0].toDouble()
- : 50.0;
- const qualityMinValue = 10.0;
- const qualityMaxValue = 100.0;
- if (qualityInitValue < qualityMinValue) {
- qualityInitValue = qualityMinValue;
- }
- if (qualityInitValue > qualityMaxValue) {
- qualityInitValue = qualityMaxValue;
- }
- final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
- final debouncerQuality = Debouncer(
- Duration(milliseconds: 1000),
- onChanged: (double v) {
- setCustomValues(quality: v);
- },
- initialValue: qualityInitValue,
- );
- final qualitySlider = Obx(() => Row(
- children: [
- Slider(
- value: qualitySliderValue.value,
- min: qualityMinValue,
- max: qualityMaxValue,
- divisions: 18,
- onChanged: (double value) {
- qualitySliderValue.value = value;
- debouncerQuality.value = value;
- },
- ),
- SizedBox(
- width: 40,
- child: Text(
- '${qualitySliderValue.value.round()}%',
- style: const TextStyle(fontSize: 15),
- )),
- SizedBox(
- width: 50,
- child: Text(
- translate('Bitrate'),
- style: const TextStyle(fontSize: 15),
- ))
- ],
- ));
- // fps
- final fpsOption =
- await bind.sessionGetOption(id: widget.id, arg: 'custom-fps');
- fpsInitValue =
- fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
- if (fpsInitValue < 10 || fpsInitValue > 120) {
- fpsInitValue = 30;
- }
- final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
- final debouncerFps = Debouncer(
- Duration(milliseconds: 1000),
- onChanged: (double v) {
- setCustomValues(fps: v);
- },
- initialValue: qualityInitValue,
- );
- bool? direct;
- try {
- direct = ConnectionTypeState.find(widget.id).direct.value ==
- ConnectionType.strDirect;
- } catch (_) {}
- final fpsSlider = Offstage(
- offstage:
- (await bind.mainIsUsingPublicServer() && direct != true) ||
- version_cmp(peer_version, '1.2.0') < 0,
- child: Row(
- children: [
- Obx((() => Slider(
- value: fpsSliderValue.value,
- min: 10,
- max: 120,
- divisions: 22,
- onChanged: (double value) {
- fpsSliderValue.value = value;
- debouncerFps.value = value;
- },
- ))),
- SizedBox(
- width: 40,
- child: Obx(() => Text(
- '${fpsSliderValue.value.round()}',
- style: const TextStyle(fontSize: 15),
- ))),
- SizedBox(
- width: 50,
- child: Text(
- translate('FPS'),
- style: const TextStyle(fontSize: 15),
- ))
- ],
- ),
- );
-
- final content = Column(
- children: [qualitySlider, fpsSlider],
- );
- msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
- content, [btnClose]);
- }
- },
- padding: padding,
- ),
- MenuEntryDivider(),
- ];
-
- if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) {
- displayMenu.insert(
- 2,
- MenuEntryRadios(
- text: translate('Scroll Style'),
- optionsGetter: () => [
- MenuEntryRadioOption(
- text: translate('ScrollAuto'),
- value: kRemoteScrollStyleAuto,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- enabled: widget.ffi.canvasModel.imageOverflow,
- ),
- MenuEntryRadioOption(
- text: translate('Scrollbar'),
- value: kRemoteScrollStyleBar,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- enabled: widget.ffi.canvasModel.imageOverflow,
- ),
- ],
- curOptionGetter: () async =>
- // null means peer id is not found, which there's no need to care about
- await bind.sessionGetScrollStyle(id: widget.id) ?? '',
- optionSetter: (String oldValue, String newValue) async {
- await bind.sessionSetScrollStyle(id: widget.id, value: newValue);
- widget.ffi.canvasModel.updateScrollStyle();
+ groupValue: groupValue,
+ onChanged: (value) {
+ onChanged(value);
+ _customImageQualityDialog();
},
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- displayMenu.insert(3, MenuEntryDivider());
-
- if (_isWindowCanBeAdjusted(remoteCount)) {
- displayMenu.insert(
- 0,
- MenuEntryDivider(),
- );
- displayMenu.insert(
- 0,
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Container(
- child: Text(
- translate('Adjust Window'),
- style: style,
- )),
- proc: () {
- () async {
- await _updateScreen();
- if (_screen != null) {
- _setFullscreen(false);
- double scale = _screen!.scaleFactor;
- final wndRect =
- await WindowController.fromWindowId(windowId).getFrame();
- final mediaSize = MediaQueryData.fromWindow(ui.window).size;
- // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
- // https://stackoverflow.com/a/7561083
- double magicWidth =
- wndRect.right - wndRect.left - mediaSize.width * scale;
- double magicHeight =
- wndRect.bottom - wndRect.top - mediaSize.height * scale;
-
- final canvasModel = widget.ffi.canvasModel;
- final width =
- (canvasModel.getDisplayWidth() * canvasModel.scale +
- canvasModel.windowBorderWidth * 2) *
- scale +
- magicWidth;
- final height =
- (canvasModel.getDisplayHeight() * canvasModel.scale +
- canvasModel.tabBarHeight +
- canvasModel.windowBorderWidth * 2) *
- scale +
- magicHeight;
- double left = wndRect.left + (wndRect.width - width) / 2;
- double top = wndRect.top + (wndRect.height - height) / 2;
-
- Rect frameRect = _screen!.frame;
- if (!isFullscreen) {
- frameRect = _screen!.visibleFrame;
- }
- if (left < frameRect.left) {
- left = frameRect.left;
- }
- if (top < frameRect.top) {
- top = frameRect.top;
- }
- if ((left + width) > frameRect.right) {
- left = frameRect.right - width;
- }
- if ((top + height) > frameRect.bottom) {
- top = frameRect.bottom - height;
- }
- await WindowController.fromWindowId(windowId)
- .setFrame(Rect.fromLTWH(left, top, width, height));
- }
- }();
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
+ ffi: widget.ffi,
),
- );
+ ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(),
+ );
+ });
+ }
+
+ _customImageQualityDialog() async {
+ double qualityInitValue = 50;
+ double fpsInitValue = 30;
+ bool qualitySet = false;
+ bool fpsSet = false;
+ setCustomValues({double? quality, double? fps}) async {
+ if (quality != null) {
+ qualitySet = true;
+ await bind.sessionSetCustomImageQuality(
+ id: widget.id, value: quality.toInt());
+ }
+ if (fps != null) {
+ fpsSet = true;
+ await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt());
+ }
+ if (!qualitySet) {
+ qualitySet = true;
+ await bind.sessionSetCustomImageQuality(
+ id: widget.id, value: qualityInitValue.toInt());
+ }
+ if (!fpsSet) {
+ fpsSet = true;
+ await bind.sessionSetCustomFps(
+ id: widget.id, fps: fpsInitValue.toInt());
}
}
- /// Show Codec Preference
- if (bind.mainHasHwcodec()) {
+ final btnClose = dialogButton('Close', onPressed: () async {
+ await setCustomValues();
+ widget.ffi.dialogManager.dismissAll();
+ });
+
+ // quality
+ final quality = await bind.sessionGetCustomImageQuality(id: widget.id);
+ qualityInitValue =
+ quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
+ const qualityMinValue = 10.0;
+ const qualityMaxValue = 100.0;
+ if (qualityInitValue < qualityMinValue) {
+ qualityInitValue = qualityMinValue;
+ }
+ if (qualityInitValue > qualityMaxValue) {
+ qualityInitValue = qualityMaxValue;
+ }
+ final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
+ final debouncerQuality = Debouncer(
+ Duration(milliseconds: 1000),
+ onChanged: (double v) {
+ setCustomValues(quality: v);
+ },
+ initialValue: qualityInitValue,
+ );
+ final qualitySlider = Obx(() => Row(
+ children: [
+ Slider(
+ value: qualitySliderValue.value,
+ min: qualityMinValue,
+ max: qualityMaxValue,
+ divisions: 18,
+ onChanged: (double value) {
+ qualitySliderValue.value = value;
+ debouncerQuality.value = value;
+ },
+ ),
+ SizedBox(
+ width: 40,
+ child: Text(
+ '${qualitySliderValue.value.round()}%',
+ style: const TextStyle(fontSize: 15),
+ )),
+ SizedBox(
+ width: 50,
+ child: Text(
+ translate('Bitrate'),
+ style: const TextStyle(fontSize: 15),
+ ))
+ ],
+ ));
+ // fps
+ final fpsOption =
+ await bind.sessionGetOption(id: widget.id, arg: 'custom-fps');
+ fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
+ if (fpsInitValue < 10 || fpsInitValue > 120) {
+ fpsInitValue = 30;
+ }
+ final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
+ final debouncerFps = Debouncer(
+ Duration(milliseconds: 1000),
+ onChanged: (double v) {
+ setCustomValues(fps: v);
+ },
+ initialValue: qualityInitValue,
+ );
+ bool? direct;
+ try {
+ direct = ConnectionTypeState.find(widget.id).direct.value ==
+ ConnectionType.strDirect;
+ } catch (_) {}
+ final fpsSlider = Offstage(
+ offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
+ version_cmp(pi.version, '1.2.0') < 0,
+ child: Row(
+ children: [
+ Obx((() => Slider(
+ value: fpsSliderValue.value,
+ min: 10,
+ max: 120,
+ divisions: 22,
+ onChanged: (double value) {
+ fpsSliderValue.value = value;
+ debouncerFps.value = value;
+ },
+ ))),
+ SizedBox(
+ width: 40,
+ child: Obx(() => Text(
+ '${fpsSliderValue.value.round()}',
+ style: const TextStyle(fontSize: 15),
+ ))),
+ SizedBox(
+ width: 50,
+ child: Text(
+ translate('FPS'),
+ style: const TextStyle(fontSize: 15),
+ ))
+ ],
+ ),
+ );
+
+ final content = Column(
+ children: [qualitySlider, fpsSlider],
+ );
+ msgBoxCommon(
+ widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
+ }
+
+ codec() {
+ return futureBuilder(future: () async {
+ final supportedHwcodec =
+ await bind.sessionSupportedHwcodec(id: widget.id);
+ final codecPreference =
+ await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ??
+ '';
+ return {
+ 'supportedHwcodec': supportedHwcodec,
+ 'codecPreference': codecPreference
+ };
+ }(), hasData: (data) {
final List codecs = [];
try {
- final Map codecsJson = jsonDecode(futureData['supportedHwcodec']);
+ final Map codecsJson = jsonDecode(data['supportedHwcodec']);
final h264 = codecsJson['h264'] ?? false;
final h265 = codecsJson['h265'] ?? false;
codecs.add(h264);
@@ -1343,387 +1288,663 @@ class _RemoteMenubarState extends State {
} catch (e) {
debugPrint("Show Codec Preference err=$e");
}
- if (codecs.length == 2 && (codecs[0] || codecs[1])) {
- displayMenu.add(MenuEntryRadios(
- text: translate('Codec Preference'),
- optionsGetter: () {
- final list = [
- MenuEntryRadioOption(
- text: translate('Auto'),
- value: 'auto',
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- MenuEntryRadioOption(
- text: 'VP9',
- value: 'vp9',
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ),
- ];
- if (codecs[0]) {
- list.add(MenuEntryRadioOption(
- text: 'H264',
- value: 'h264',
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
- if (codecs[1]) {
- list.add(MenuEntryRadioOption(
- text: 'H265',
- value: 'h265',
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
- return list;
- },
- curOptionGetter: () async =>
- // null means peer id is not found, which there's no need to care about
- await bind.sessionGetOption(
- id: widget.id, arg: 'codec-preference') ??
- '',
- optionSetter: (String oldValue, String newValue) async {
- await bind.sessionPeerOption(
- id: widget.id, name: 'codec-preference', value: newValue);
- bind.sessionChangePreferCodec(id: widget.id);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
+ final visible = bind.mainHasHwcodec() &&
+ codecs.length == 2 &&
+ (codecs[0] || codecs[1]);
+ if (!visible) return Offstage();
+ final groupValue = data['codecPreference'] as String;
+ onChanged(String? value) async {
+ if (value == null) return;
+ await bind.sessionPeerOption(
+ id: widget.id, name: 'codec-preference', value: value);
+ bind.sessionChangePreferCodec(id: widget.id);
}
- }
- displayMenu.add(MenuEntryDivider());
- /// Show remote cursor
- if (!widget.ffi.canvasModel.cursorEmbedded) {
- displayMenu.add(RemoteMenuEntry.showRemoteCursor(
- widget.id,
- padding,
- dismissCallback: _menuDismissCallback,
- ));
- }
-
- /// Show remote cursor scaling with image
- if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) {
- displayMenu.add(() {
- final opt = 'zoom-cursor';
- final state = PeerBoolOption.find(widget.id, opt);
- return MenuEntrySwitch2(
- switchType: SwitchType.scheckbox,
- text: translate('Zoom cursor'),
- getter: () {
- return state;
- },
- setter: (bool v) async {
- await bind.sessionToggleOption(id: widget.id, value: opt);
- state.value =
- bind.sessionGetToggleOptionSync(id: widget.id, arg: opt);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- );
- }());
- }
-
- /// Show quality monitor
- displayMenu.add(MenuEntrySwitch(
- switchType: SwitchType.scheckbox,
- text: translate('Show quality monitor'),
- getter: () async {
- return bind.sessionGetToggleOptionSync(
- id: widget.id, arg: 'show-quality-monitor');
- },
- setter: (bool v) async {
- await bind.sessionToggleOption(
- id: widget.id, value: 'show-quality-monitor');
- widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
-
- final perms = widget.ffi.ffiModel.permissions;
- final pi = widget.ffi.ffiModel.pi;
-
- if (perms['audio'] != false) {
- displayMenu
- .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
- displayMenu
- .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
- }
-
- if (Platform.isWindows &&
- pi.platform == kPeerPlatformWindows &&
- perms['file'] != false) {
- displayMenu.add(_createSwitchMenuEntry(
- 'Allow file copy and paste', 'enable-file-transfer', padding, true));
- }
-
- if (perms['keyboard'] != false) {
- if (perms['clipboard'] != false) {
- displayMenu.add(RemoteMenuEntry.disableClipboard(
- widget.id,
- padding,
- dismissCallback: _menuDismissCallback,
- ));
- }
- displayMenu.add(_createSwitchMenuEntry(
- 'Lock after session end', 'lock-after-session-end', padding, true));
- if (pi.features.privacyMode) {
- displayMenu.add(MenuEntrySwitch2(
- switchType: SwitchType.scheckbox,
- text: translate('Privacy mode'),
- getter: () {
- return PrivacyModeState.find(widget.id);
- },
- setter: (bool v) async {
- await bind.sessionToggleOption(
- id: widget.id, value: 'privacy-mode');
- },
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: _menuDismissCallback,
- ));
- }
- }
- return displayMenu;
- }
-
- List> _getKeyboardMenu() {
- final List> keyboardMenu = [
- MenuEntryRadios(
- text: translate('Ratio'),
- optionsGetter: () {
- List list = [];
- List modes = [
- KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'),
- KeyboardModeMenu(key: 'map', menu: 'Map mode'),
- KeyboardModeMenu(key: 'translate', menu: 'Translate mode'),
- ];
-
- for (KeyboardModeMenu mode in modes) {
- if (bind.sessionIsKeyboardModeSupported(
- id: widget.id, mode: mode.key)) {
- if (mode.key == 'translate') {
- if (!Platform.isWindows ||
- widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
- continue;
- }
- }
- list.add(MenuEntryRadioOption(
- text: translate(mode.menu), value: mode.key));
- }
- }
- return list;
- },
- curOptionGetter: () async {
- return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy';
- },
- optionSetter: (String oldValue, String newValue) async {
- await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
- },
- )
- ];
- final localPlatform =
- getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform);
- if (localPlatform != '') {
- keyboardMenu.add(MenuEntryDivider());
- keyboardMenu.add(
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Container(
- alignment: AlignmentDirectional.center,
- height: _MenubarTheme.height,
- child: Row(
- children: [
- Obx(() => RichText(
- text: TextSpan(
- text: '${translate('Local keyboard type')}: ',
- style: DefaultTextStyle.of(context).style,
- children: [
- TextSpan(
- text: KBLayoutType.value,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- ],
- ),
- )),
- Expanded(
- child: Align(
- alignment: Alignment.centerRight,
- child: Transform.scale(
- scale: 0.8,
- child: IconButton(
- padding: EdgeInsets.zero,
- icon: const Icon(Icons.settings),
- onPressed: () {
- if (Navigator.canPop(context)) {
- Navigator.pop(context);
- _menuDismissCallback();
- }
- showKBLayoutTypeChooser(
- localPlatform, widget.ffi.dialogManager);
- },
- ),
- ),
- ))
- ],
- )),
- proc: () {},
- padding: EdgeInsets.zero,
- dismissOnClicked: false,
- dismissCallback: _menuDismissCallback,
- ),
- );
- }
- keyboardMenu.add(_createSwitchMenuEntry(
- 'Swap Control-Command Key', 'allow_swap_key', EdgeInsets.zero, true));
-
- return keyboardMenu;
- }
-
- MenuEntrySwitch _createSwitchMenuEntry(
- String text, String option, EdgeInsets? padding, bool dismissOnClicked) {
- return RemoteMenuEntry.createSwitchMenuEntry(
- widget.id, text, option, padding, dismissOnClicked,
- dismissCallback: _menuDismissCallback);
- }
-}
-
-void showSetOSPassword(
- String id, bool login, OverlayDialogManager dialogManager) async {
- final controller = TextEditingController();
- var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
- var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != '';
- controller.text = password;
- dialogManager.show((setState, close) {
- submit() {
- var text = controller.text.trim();
- bind.sessionPeerOption(id: id, name: 'os-password', value: text);
- bind.sessionPeerOption(
- id: id, name: 'auto-login', value: autoLogin ? 'Y' : '');
- if (text != '' && login) {
- bind.sessionInputOsPassword(id: id, value: text);
- }
- close();
- }
-
- return CustomAlertDialog(
- title: Text(translate('OS Password')),
- content: Column(mainAxisSize: MainAxisSize.min, children: [
- PasswordWidget(controller: controller),
- CheckboxListTile(
- contentPadding: const EdgeInsets.all(0),
- dense: true,
- controlAffinity: ListTileControlAffinity.leading,
- title: Text(
- translate('Auto Login'),
- ),
- value: autoLogin,
- onChanged: (v) {
- if (v == null) return;
- setState(() => autoLogin = v);
- },
- ),
- ]),
- actions: [
- dialogButton('Cancel', onPressed: close, isOutline: true),
- dialogButton('OK', onPressed: submit),
- ],
- onSubmit: submit,
- onCancel: close,
- );
- });
-}
-
-void showAuditDialog(String id, dialogManager) async {
- final controller = TextEditingController();
- dialogManager.show((setState, close) {
- submit() {
- var text = controller.text.trim();
- if (text != '') {
- bind.sessionSendNote(id: id, note: text);
- }
- close();
- }
-
- late final focusNode = FocusNode(
- onKey: (FocusNode node, RawKeyEvent evt) {
- if (evt.logicalKey.keyLabel == 'Enter') {
- if (evt is RawKeyDownEvent) {
- int pos = controller.selection.base.offset;
- controller.text =
- '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
- controller.selection =
- TextSelection.fromPosition(TextPosition(offset: pos + 1));
- }
- return KeyEventResult.handled;
- }
- if (evt.logicalKey.keyLabel == 'Esc') {
- if (evt is RawKeyDownEvent) {
- close();
- }
- return KeyEventResult.handled;
- } else {
- return KeyEventResult.ignored;
- }
- },
- );
-
- return CustomAlertDialog(
- title: Text(translate('Note')),
- content: SizedBox(
- width: 250,
- height: 120,
- child: TextField(
- autofocus: true,
- keyboardType: TextInputType.multiline,
- textInputAction: TextInputAction.newline,
- decoration: const InputDecoration.collapsed(
- hintText: 'input note here',
+ return SubmenuButton(
+ child: Text(translate('Codec')),
+ menuChildren: [
+ _RadioMenuButton(
+ child: Text(translate('Auto')),
+ value: 'auto',
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
),
- // inputFormatters: [
- // LengthLimitingTextInputFormatter(16),
- // // FilteringTextInputFormatter(RegExp(r'[a-zA-z][a-zA-z0-9\_]*'), allow: true)
- // ],
- maxLines: null,
- maxLength: 256,
- controller: controller,
- focusNode: focusNode,
- )),
- actions: [
- dialogButton('Cancel', onPressed: close, isOutline: true),
- dialogButton('OK', onPressed: submit)
- ],
- onSubmit: submit,
- onCancel: close,
- );
- });
-}
+ _RadioMenuButton(
+ child: Text(translate('VP9')),
+ value: 'vp9',
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
+ ),
+ _RadioMenuButton(
+ child: Text(translate('H264')),
+ value: 'h264',
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
+ ),
+ _RadioMenuButton(
+ child: Text(translate('H265')),
+ value: 'h265',
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
+ ),
+ ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList());
+ });
+ }
-void showConfirmSwitchSidesDialog(
- String id, OverlayDialogManager dialogManager) async {
- dialogManager.show((setState, close) {
- submit() async {
- await bind.sessionSwitchSides(id: id);
- closeConnection(id: id);
+ resolutions() {
+ final resolutions = widget.ffi.ffiModel.pi.resolutions;
+ final visible = widget.ffi.ffiModel.permissions["keyboard"] != false &&
+ resolutions.length > 1;
+ if (!visible) return Offstage();
+ final display = widget.ffi.ffiModel.display;
+ final groupValue = "${display.width}x${display.height}";
+ onChanged(String? value) async {
+ if (value == null) return;
+ final list = value.split('x');
+ if (list.length == 2) {
+ final w = int.tryParse(list[0]);
+ final h = int.tryParse(list[1]);
+ if (w != null && h != null) {
+ await bind.sessionChangeResolution(
+ id: widget.id, width: w, height: h);
+ Future.delayed(Duration(seconds: 3), () async {
+ final display = widget.ffi.ffiModel.display;
+ if (w == display.width && h == display.height) {
+ if (_isWindowCanBeAdjusted()) {
+ _doAdjustWindow();
+ }
+ }
+ });
+ }
+ }
}
- return CustomAlertDialog(
- content: msgboxContent('info', 'Switch Sides',
- 'Please confirm if you want to share your desktop?'),
- actions: [
- dialogButton('Cancel', onPressed: close, isOutline: true),
- dialogButton('OK', onPressed: submit),
+ return SubmenuButton(
+ menuChildren: resolutions
+ .map((e) => _RadioMenuButton(
+ value: '${e.width}x${e.height}',
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: widget.ffi,
+ child: Text('${e.width}x${e.height}')))
+ .toList()
+ .map((e) => _buildPointerTrackWidget(e, widget.ffi))
+ .toList(),
+ child: Text(translate("Resolution")));
+ }
+
+ showRemoteCursor() {
+ final visible = !widget.ffi.canvasModel.cursorEmbedded;
+ if (!visible) return Offstage();
+ final state = ShowRemoteCursorState.find(widget.id);
+ final option = 'show-remote-cursor';
+ return _CheckboxMenuButton(
+ value: state.value,
+ onChanged: (value) async {
+ if (value == null) return;
+ await bind.sessionToggleOption(id: widget.id, value: option);
+ state.value =
+ bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Show remote cursor')));
+ }
+
+ zoomCursor() {
+ final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal;
+ if (!visible) return Offstage();
+ final option = 'zoom-cursor';
+ final peerState = PeerBoolOption.find(widget.id, option);
+ return _CheckboxMenuButton(
+ value: peerState.value,
+ onChanged: (value) async {
+ if (value == null) return;
+ await bind.sessionToggleOption(id: widget.id, value: option);
+ peerState.value =
+ bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Zoom cursor')));
+ }
+
+ showQualityMonitor() {
+ final option = 'show-quality-monitor';
+ final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ return _CheckboxMenuButton(
+ value: value,
+ onChanged: (value) async {
+ if (value == null) return;
+ await bind.sessionToggleOption(id: widget.id, value: option);
+ widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Show quality monitor')));
+ }
+
+ mute() {
+ final visible = perms['audio'] != false;
+ if (!visible) return Offstage();
+ final option = 'disable-audio';
+ final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ return _CheckboxMenuButton(
+ value: value,
+ onChanged: (value) {
+ if (value == null) return;
+ bind.sessionToggleOption(id: widget.id, value: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Mute')));
+ }
+
+ fileCopyAndPaste() {
+ final visible = Platform.isWindows &&
+ pi.platform == kPeerPlatformWindows &&
+ perms['file'] != false;
+ if (!visible) return Offstage();
+ final option = 'enable-file-transfer';
+ final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ return _CheckboxMenuButton(
+ value: value,
+ onChanged: (value) {
+ if (value == null) return;
+ bind.sessionToggleOption(id: widget.id, value: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Allow file copy and paste')));
+ }
+
+ disableClipboard() {
+ final visible = perms['keyboard'] != false && perms['clipboard'] != false;
+ if (!visible) return Offstage();
+ final option = 'disable-clipboard';
+ final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ return _CheckboxMenuButton(
+ value: value,
+ onChanged: (value) {
+ if (value == null) return;
+ bind.sessionToggleOption(id: widget.id, value: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Disable clipboard')));
+ }
+
+ lockAfterSessionEnd() {
+ final visible = perms['keyboard'] != false;
+ if (!visible) return Offstage();
+ final option = 'lock-after-session-end';
+ final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ return _CheckboxMenuButton(
+ value: value,
+ onChanged: (value) {
+ if (value == null) return;
+ bind.sessionToggleOption(id: widget.id, value: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Lock after session end')));
+ }
+
+ privacyMode() {
+ bool visible = perms['keyboard'] != false && pi.features.privacyMode;
+ if (!visible) return Offstage();
+ final option = 'privacy-mode';
+ final rxValue = PrivacyModeState.find(widget.id);
+ return _CheckboxMenuButton(
+ value: rxValue.value,
+ onChanged: (value) {
+ if (value == null) return;
+ bind.sessionToggleOption(id: widget.id, value: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Privacy mode')));
+ }
+}
+
+class _KeyboardMenu extends StatelessWidget {
+ final String id;
+ final FFI ffi;
+ _KeyboardMenu({
+ Key? key,
+ required this.id,
+ required this.ffi,
+ }) : super(key: key);
+
+ PeerInfo get pi => ffi.ffiModel.pi;
+
+ @override
+ Widget build(BuildContext context) {
+ var ffiModel = Provider.of(context);
+ if (ffiModel.permissions['keyboard'] == false) return Offstage();
+ // Do not support peer 1.1.9.
+ if (stateGlobal.grabKeyboard) {
+ bind.sessionSetKeyboardMode(id: id, value: 'map');
+ return Offstage();
+ }
+ return _IconSubmenuButton(
+ svg: "assets/keyboard.svg",
+ ffi: ffi,
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ menuChildren: [mode(), localKeyboardType()]);
+ }
+
+ mode() {
+ return futureBuilder(future: () async {
+ return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy';
+ }(), hasData: (data) {
+ final groupValue = data as String;
+ List modes = [
+ KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'),
+ KeyboardModeMenu(key: 'map', menu: 'Map mode'),
+ KeyboardModeMenu(key: 'translate', menu: 'Translate mode'),
+ ];
+ List<_RadioMenuButton> list = [];
+ onChanged(String? value) async {
+ if (value == null) return;
+ await bind.sessionSetKeyboardMode(id: id, value: value);
+ }
+
+ for (KeyboardModeMenu mode in modes) {
+ if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) {
+ if (mode.key == 'translate') {
+ if (Platform.isLinux || pi.platform == kPeerPlatformLinux) {
+ continue;
+ }
+ }
+ var text = translate(mode.menu);
+ if (mode.key == 'translate') {
+ text = '$text beta';
+ }
+ list.add(_RadioMenuButton(
+ child: Text(text),
+ value: mode.key,
+ groupValue: groupValue,
+ onChanged: onChanged,
+ ffi: ffi,
+ ));
+ }
+ }
+ return Column(children: list);
+ });
+ }
+
+ localKeyboardType() {
+ final localPlatform = getLocalPlatformForKBLayoutType(pi.platform);
+ final visible = localPlatform != '';
+ if (!visible) return Offstage();
+ return Column(
+ children: [
+ Divider(),
+ _MenuItemButton(
+ child: Text(
+ '${translate('Local keyboard type')}: ${KBLayoutType.value}'),
+ trailingIcon: const Icon(Icons.settings),
+ ffi: ffi,
+ onPressed: () =>
+ showKBLayoutTypeChooser(localPlatform, ffi.dialogManager),
+ )
],
- onSubmit: submit,
- onCancel: close,
);
- });
+ }
+}
+
+class _ChatMenu extends StatefulWidget {
+ final String id;
+ final FFI ffi;
+ _ChatMenu({
+ Key? key,
+ required this.id,
+ required this.ffi,
+ }) : super(key: key);
+
+ @override
+ State<_ChatMenu> createState() => _ChatMenuState();
+}
+
+class _ChatMenuState extends State<_ChatMenu> {
+ // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`.
+ final chatButtonKey = GlobalKey();
+
+ @override
+ Widget build(BuildContext context) {
+ return _IconSubmenuButton(
+ key: chatButtonKey,
+ svg: 'assets/chat.svg',
+ ffi: widget.ffi,
+ color: _MenubarTheme.blueColor,
+ hoverColor: _MenubarTheme.hoverBlueColor,
+ menuChildren: [textChat(), voiceCall()]);
+ }
+
+ textChat() {
+ return _MenuItemButton(
+ child: Text(translate('Text chat')),
+ ffi: widget.ffi,
+ onPressed: () {
+ RenderBox? renderBox =
+ chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
+
+ Offset? initPos;
+ if (renderBox != null) {
+ final pos = renderBox.localToGlobal(Offset.zero);
+ initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
+ }
+
+ widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
+ widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
+ });
+ }
+
+ voiceCall() {
+ return _MenuItemButton(
+ child: Text(translate('Voice call')),
+ ffi: widget.ffi,
+ onPressed: () => bind.sessionRequestVoiceCall(id: widget.id),
+ );
+ }
+}
+
+class _VoiceCallMenu extends StatelessWidget {
+ final String id;
+ final FFI ffi;
+ _VoiceCallMenu({
+ Key? key,
+ required this.id,
+ required this.ffi,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Obx(
+ () {
+ final String tooltip;
+ final String icon;
+ switch (ffi.chatModel.voiceCallStatus.value) {
+ case VoiceCallStatus.waitingForResponse:
+ tooltip = "Waiting";
+ icon = "assets/call_wait.svg";
+ break;
+ case VoiceCallStatus.connected:
+ tooltip = "Disconnect";
+ icon = "assets/call_end.svg";
+ break;
+ default:
+ return Offstage();
+ }
+ return _IconMenuButton(
+ assetName: icon,
+ tooltip: tooltip,
+ onPressed: () => bind.sessionCloseVoiceCall(id: id),
+ color: _MenubarTheme.redColor,
+ hoverColor: _MenubarTheme.hoverRedColor);
+ },
+ );
+ }
+}
+
+class _RecordMenu extends StatelessWidget {
+ const _RecordMenu({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ var ffi = Provider.of(context);
+ final visible = ffi.permissions['recording'] != false;
+ if (!visible) return Offstage();
+ return Consumer(
+ builder: (context, value, child) => _IconMenuButton(
+ assetName: 'assets/rec.svg',
+ tooltip:
+ value.start ? 'Stop session recording' : 'Start session recording',
+ onPressed: () => value.toggle(),
+ color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor,
+ hoverColor: value.start
+ ? _MenubarTheme.hoverRedColor
+ : _MenubarTheme.hoverBlueColor,
+ ),
+ );
+ }
+}
+
+class _CloseMenu extends StatelessWidget {
+ final String id;
+ final FFI ffi;
+ const _CloseMenu({Key? key, required this.id, required this.ffi})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return _IconMenuButton(
+ assetName: 'assets/close.svg',
+ tooltip: 'Close',
+ onPressed: () => clientClose(id, ffi.dialogManager),
+ color: _MenubarTheme.redColor,
+ hoverColor: _MenubarTheme.hoverRedColor,
+ );
+ }
+}
+
+class _IconMenuButton extends StatefulWidget {
+ final String? assetName;
+ final Widget? icon;
+ final String tooltip;
+ final Color color;
+ final Color hoverColor;
+ final VoidCallback? onPressed;
+ final double? hMargin;
+ final double? vMargin;
+ const _IconMenuButton({
+ Key? key,
+ this.assetName,
+ this.icon,
+ required this.tooltip,
+ required this.color,
+ required this.hoverColor,
+ required this.onPressed,
+ this.hMargin,
+ this.vMargin,
+ }) : super(key: key);
+
+ @override
+ State<_IconMenuButton> createState() => _IconMenuButtonState();
+}
+
+class _IconMenuButtonState extends State<_IconMenuButton> {
+ bool hover = false;
+
+ @override
+ Widget build(BuildContext context) {
+ assert(widget.assetName != null || widget.icon != null);
+ final icon = widget.icon ??
+ SvgPicture.asset(
+ widget.assetName!,
+ color: Colors.white,
+ width: _MenubarTheme.buttonSize,
+ height: _MenubarTheme.buttonSize,
+ );
+ return SizedBox(
+ width: _MenubarTheme.buttonSize,
+ height: _MenubarTheme.buttonSize,
+ child: MenuItemButton(
+ style: ButtonStyle(
+ padding: MaterialStatePropertyAll(EdgeInsets.zero),
+ overlayColor: MaterialStatePropertyAll(Colors.transparent)),
+ onHover: (value) => setState(() {
+ hover = value;
+ }),
+ onPressed: widget.onPressed,
+ child: Tooltip(
+ message: translate(widget.tooltip),
+ child: Material(
+ type: MaterialType.transparency,
+ child: Ink(
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(_MenubarTheme.iconRadius),
+ color: hover ? widget.hoverColor : widget.color,
+ ),
+ child: icon))),
+ ),
+ ).marginSymmetric(
+ horizontal: widget.hMargin ?? _MenubarTheme.buttonHMargin,
+ vertical: widget.vMargin ?? _MenubarTheme.buttonVMargin);
+ }
+}
+
+class _IconSubmenuButton extends StatefulWidget {
+ final String? svg;
+ final Widget? icon;
+ final Color color;
+ final Color hoverColor;
+ final List menuChildren;
+ final MenuStyle? menuStyle;
+ final FFI ffi;
+
+ _IconSubmenuButton(
+ {Key? key,
+ this.svg,
+ this.icon,
+ required this.color,
+ required this.hoverColor,
+ required this.menuChildren,
+ required this.ffi,
+ this.menuStyle})
+ : super(key: key);
+
+ @override
+ State<_IconSubmenuButton> createState() => _IconSubmenuButtonState();
+}
+
+class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
+ bool hover = false;
+
+ @override
+ Widget build(BuildContext context) {
+ assert(widget.svg != null || widget.icon != null);
+ final icon = widget.icon ??
+ SvgPicture.asset(
+ widget.svg!,
+ color: Colors.white,
+ width: _MenubarTheme.buttonSize,
+ height: _MenubarTheme.buttonSize,
+ );
+ return SizedBox(
+ width: _MenubarTheme.buttonSize,
+ height: _MenubarTheme.buttonSize,
+ child: SubmenuButton(
+ menuStyle: widget.menuStyle,
+ style: ButtonStyle(
+ padding: MaterialStatePropertyAll(EdgeInsets.zero),
+ overlayColor: MaterialStatePropertyAll(Colors.transparent)),
+ onHover: (value) => setState(() {
+ hover = value;
+ }),
+ child: Material(
+ type: MaterialType.transparency,
+ child: Ink(
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(_MenubarTheme.iconRadius),
+ color: hover ? widget.hoverColor : widget.color,
+ ),
+ child: icon)),
+ menuChildren: widget.menuChildren
+ .map((e) => _buildPointerTrackWidget(e, widget.ffi))
+ .toList()))
+ .marginSymmetric(
+ horizontal: _MenubarTheme.buttonHMargin,
+ vertical: _MenubarTheme.buttonVMargin);
+ }
+}
+
+class _MenuItemButton extends StatelessWidget {
+ final VoidCallback? onPressed;
+ final Widget? trailingIcon;
+ final Widget? child;
+ final FFI ffi;
+ _MenuItemButton(
+ {Key? key,
+ this.onPressed,
+ this.trailingIcon,
+ required this.child,
+ required this.ffi})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MenuItemButton(
+ key: key,
+ onPressed: onPressed != null
+ ? () {
+ _menuDismissCallback(ffi);
+ onPressed?.call();
+ }
+ : null,
+ trailingIcon: trailingIcon,
+ child: child);
+ }
+}
+
+class _CheckboxMenuButton extends StatelessWidget {
+ final bool? value;
+ final ValueChanged? onChanged;
+ final Widget? child;
+ final FFI ffi;
+ const _CheckboxMenuButton(
+ {Key? key,
+ required this.value,
+ required this.onChanged,
+ required this.child,
+ required this.ffi})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return CheckboxMenuButton(
+ key: key,
+ value: value,
+ child: child,
+ onChanged: onChanged != null
+ ? (bool? value) {
+ _menuDismissCallback(ffi);
+ onChanged?.call(value);
+ }
+ : null,
+ );
+ }
+}
+
+class _RadioMenuButton extends StatelessWidget {
+ final T value;
+ final T? groupValue;
+ final ValueChanged? onChanged;
+ final Widget? child;
+ final FFI ffi;
+ const _RadioMenuButton(
+ {Key? key,
+ required this.value,
+ required this.groupValue,
+ required this.onChanged,
+ required this.child,
+ required this.ffi})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return RadioMenuButton(
+ value: value,
+ groupValue: groupValue,
+ child: child,
+ onChanged: onChanged != null
+ ? (T? value) {
+ _menuDismissCallback(ffi);
+ onChanged?.call(value);
+ }
+ : null,
+ );
+ }
}
class _DraggableShowHide extends StatefulWidget {
@@ -1751,7 +1972,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
child: Icon(
Icons.drag_indicator,
size: 20,
- color: Colors.grey,
+ color: Colors.grey[800],
),
feedback: widget,
onDragStarted: (() {
@@ -1804,7 +2025,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
child: Container(
decoration: BoxDecoration(
color: Colors.white,
- border: Border.all(color: MyTheme.border),
+ borderRadius: BorderRadius.vertical(
+ bottom: Radius.circular(5),
+ ),
),
child: SizedBox(
height: 20,
@@ -1821,3 +2044,15 @@ class KeyboardModeMenu {
KeyboardModeMenu({required this.key, required this.menu});
}
+
+_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
+
+Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
+ return Listener(
+ onPointerHover: (PointerHoverEvent e) =>
+ ffi.inputModel.lastMousePos = e.position,
+ child: MouseRegion(
+ child: child,
+ ),
+ );
+}
diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart
index 5c37900f2..ee3aaaf2c 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -234,7 +234,7 @@ class DesktopTab extends StatelessWidget {
Key? key,
required this.controller,
this.showLogo = true,
- this.showTitle = true,
+ this.showTitle = false,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
@@ -548,13 +548,20 @@ class WindowActionPanelState extends State
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
- // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden,
- // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully.
- // e.g.: saving window position.
+ // macOS specific workaround, the window is not hiding when in fullscreen.
+ if (Platform.isMacOS && await windowManager.isFullScreen()) {
+ await windowManager.setFullScreen(false);
+ await Future.delayed(Duration(seconds: 1));
+ }
await windowManager.hide();
} else {
// it's safe to hide the subwindow
- await WindowController.fromWindowId(kWindowId!).hide();
+ final controller = WindowController.fromWindowId(kWindowId!);
+ if (Platform.isMacOS && await controller.isFullScreen()) {
+ await controller.setFullscreen(false);
+ await Future.delayed(Duration(seconds: 1));
+ }
+ await controller.hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart
index 475b4cb86..38e4d917b 100644
--- a/flutter/lib/desktop/widgets/titlebar_widget.dart
+++ b/flutter/lib/desktop/widgets/titlebar_widget.dart
@@ -24,47 +24,8 @@ class DesktopTitleBar extends StatelessWidget {
Expanded(
child: child ?? Offstage(),
)
- // const WindowButtons()
],
),
);
}
-}
-
-// final buttonColors = WindowButtonColors(
-// iconNormal: const Color(0xFF805306),
-// mouseOver: const Color(0xFFF6A00C),
-// mouseDown: const Color(0xFF805306),
-// iconMouseOver: const Color(0xFF805306),
-// iconMouseDown: const Color(0xFFFFD500));
-//
-// final closeButtonColors = WindowButtonColors(
-// mouseOver: const Color(0xFFD32F2F),
-// mouseDown: const Color(0xFFB71C1C),
-// iconNormal: const Color(0xFF805306),
-// iconMouseOver: Colors.white);
-//
-// class WindowButtons extends StatelessWidget {
-// const WindowButtons({Key? key}) : super(key: key);
-//
-// @override
-// Widget build(BuildContext context) {
-// return Row(
-// children: [
-// MinimizeWindowButton(colors: buttonColors, onPressed: () {
-// windowManager.minimize();
-// },),
-// MaximizeWindowButton(colors: buttonColors, onPressed: () async {
-// if (await windowManager.isMaximized()) {
-// windowManager.restore();
-// } else {
-// windowManager.maximize();
-// }
-// },),
-// CloseWindowButton(colors: closeButtonColors, onPressed: () {
-// windowManager.close();
-// },),
-// ],
-// );
-// }
-// }
+}
\ No newline at end of file
diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart
index c4b07b375..951d63faf 100644
--- a/flutter/lib/mobile/pages/remote_page.dart
+++ b/flutter/lib/mobile/pages/remote_page.dart
@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
import 'package:flutter_hbb/models/chat_model.dart';
+import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
@@ -17,6 +18,7 @@ import '../../common/widgets/remote_input.dart';
import '../../models/input_model.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
+import '../../utils/image.dart';
import '../widgets/dialog.dart';
import '../widgets/gestures.dart';
@@ -32,17 +34,16 @@ class RemotePage extends StatefulWidget {
}
class _RemotePageState extends State {
- Timer? _interval;
Timer? _timer;
bool _showBar = !isWebDesktop;
- double _bottom = 0;
+ bool _showGestureHelp = false;
String _value = '';
double _scale = 1;
double _mouseScrollIntegral = 0; // mouse scroll speed controller
Orientation? _currentOrientation;
- var _more = true;
- var _fn = false;
+ final keyboardVisibilityController = KeyboardVisibilityController();
+ late final StreamSubscription keyboardSubscription;
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
@@ -57,14 +58,14 @@ class _RemotePageState extends State {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
- _interval =
- Timer.periodic(Duration(milliseconds: 30), (timer) => interval());
});
Wakelock.enable();
_physicalFocusNode.requestFocus();
gFFI.ffiModel.updateEventListener(widget.id);
gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id);
+ keyboardSubscription =
+ keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
}
@override
@@ -75,47 +76,26 @@ class _RemotePageState extends State {
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
gFFI.close();
- _interval?.cancel();
_timer?.cancel();
gFFI.dialogManager.dismissAll();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
Wakelock.disable();
+ keyboardSubscription.cancel();
super.dispose();
}
- void resetTool() {
- inputModel.resetModifiers();
- }
-
- bool isKeyboardShown() {
- return _bottom >= 100;
- }
-
- // crash on web before widget initiated.
- void intervalUnsafe() {
- var v = MediaQuery.of(context).viewInsets.bottom;
- if (v != _bottom) {
- resetTool();
- setState(() {
- _bottom = v;
- if (v < 100) {
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
- overlays: []);
- // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
- if (gFFI.chatModel.chatWindowOverlayEntry == null &&
- gFFI.ffiModel.pi.version.isNotEmpty) {
- gFFI.invokeMethod("enable_soft_keyboard", false);
- }
- }
- });
+ void onSoftKeyboardChanged(bool visible) {
+ if (!visible) {
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
+ // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
+ if (gFFI.chatModel.chatWindowOverlayEntry == null &&
+ gFFI.ffiModel.pi.version.isNotEmpty) {
+ gFFI.invokeMethod("enable_soft_keyboard", false);
+ }
}
- }
-
- void interval() {
- try {
- intervalUnsafe();
- } catch (e) {}
+ // update for Scaffold
+ setState(() {});
}
// handle mobile virtual keyboard
@@ -218,8 +198,9 @@ class _RemotePageState extends State {
@override
Widget build(BuildContext context) {
final pi = Provider.of(context).pi;
- final hideKeyboard = isKeyboardShown() && _showEdit;
- final showActionButton = !_showBar || hideKeyboard;
+ final keyboardIsVisible =
+ keyboardVisibilityController.isVisible && _showEdit;
+ final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
return WillPopScope(
@@ -228,29 +209,40 @@ class _RemotePageState extends State {
return false;
},
child: getRawPointerAndKeyBody(Scaffold(
- // resizeToAvoidBottomInset: true,
+ // workaround for https://github.com/rustdesk/rustdesk/issues/3131
+ floatingActionButtonLocation: keyboardIsVisible
+ ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
+ : null,
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
- mini: !hideKeyboard,
+ mini: !keyboardIsVisible,
child: Icon(
- hideKeyboard ? Icons.expand_more : Icons.expand_less),
+ (keyboardIsVisible || _showGestureHelp)
+ ? Icons.expand_more
+ : Icons.expand_less,
+ color: Colors.white,
+ ),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
- if (hideKeyboard) {
+ if (keyboardIsVisible) {
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
+ } else if (_showGestureHelp) {
+ _showGestureHelp = false;
} else {
_showBar = !_showBar;
}
});
}),
- bottomNavigationBar: _showBar && pi.displays.isNotEmpty
- ? getBottomAppBar(keyboard)
- : null,
+ bottomNavigationBar: _showGestureHelp
+ ? getGestureHelp()
+ : (_showBar && pi.displays.isNotEmpty
+ ? getBottomAppBar(keyboard)
+ : null),
body: Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
@@ -340,7 +332,8 @@ class _RemotePageState extends State {
icon: Icon(gFFI.ffiModel.touchMode
? Icons.touch_app
: Icons.mouse),
- onPressed: changeTouchMode,
+ onPressed: () => setState(
+ () => _showGestureHelp = !_showGestureHelp),
),
]) +
(isWeb
@@ -492,6 +485,7 @@ class _RemotePageState extends State {
}
Widget getBodyForMobile() {
+ final keyboardIsVisible = keyboardVisibilityController.isVisible;
return Container(
color: MyTheme.canvasColor,
child: Stack(children: () {
@@ -502,7 +496,7 @@ class _RemotePageState extends State {
right: 10,
child: QualityMonitor(gFFI.qualityMonitorModel),
),
- getHelpTools(),
+ KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)),
SizedBox(
width: 0,
height: 0,
@@ -575,9 +569,10 @@ class _RemotePageState extends State {
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
}
if (perms['keyboard'] != false) {
- more.add(PopupMenuItem(
- child: Text(translate('Physical Keyboard Input Mode')),
- value: 'input-mode'));
+ // * Currently mobile does not enable map mode
+ // more.add(PopupMenuItem(
+ // child: Text(translate('Physical Keyboard Input Mode')),
+ // value: 'input-mode'));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
more.add(PopupMenuItem(
child: Text('${translate('Insert')} Ctrl + Alt + Del'),
@@ -632,8 +627,9 @@ class _RemotePageState extends State {
);
if (value == 'cad') {
bind.sessionCtrlAltDel(id: widget.id);
- } else if (value == 'input-mode') {
- changePhysicalKeyboardInputMode();
+ // * Currently mobile does not enable map mode
+ // } else if (value == 'input-mode') {
+ // changePhysicalKeyboardInputMode();
} else if (value == 'lock') {
bind.sessionLockScreen(id: widget.id);
} else if (value == 'block-input') {
@@ -670,94 +666,110 @@ class _RemotePageState extends State {
}();
}
- void changeTouchMode() {
- setState(() => _showEdit = false);
- showModalBottomSheet(
- // backgroundColor: MyTheme.grayBg,
- isScrollControlled: true,
- context: context,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(5))),
- builder: (context) => DraggableScrollableSheet(
- expand: false,
- builder: (context, scrollController) {
- return SingleChildScrollView(
- controller: ScrollController(),
- padding: EdgeInsets.symmetric(vertical: 10),
- child: GestureHelp(
- touchMode: gFFI.ffiModel.touchMode,
- onTouchModeChange: (t) {
- gFFI.ffiModel.toggleTouchMode();
- final v = gFFI.ffiModel.touchMode ? 'Y' : '';
- bind.sessionPeerOption(
- id: widget.id, name: "touch", value: v);
- }));
- }));
+ /// aka changeTouchMode
+ BottomAppBar getGestureHelp() {
+ return BottomAppBar(
+ child: SingleChildScrollView(
+ controller: ScrollController(),
+ padding: EdgeInsets.symmetric(vertical: 10),
+ child: GestureHelp(
+ touchMode: gFFI.ffiModel.touchMode,
+ onTouchModeChange: (t) {
+ gFFI.ffiModel.toggleTouchMode();
+ final v = gFFI.ffiModel.touchMode ? 'Y' : '';
+ bind.sessionPeerOption(
+ id: widget.id, name: "touch", value: v);
+ })));
}
- void changePhysicalKeyboardInputMode() async {
- var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
- gFFI.dialogManager.show((setState, close) {
- void setMode(String? v) async {
- await bind.sessionPeerOption(
- id: widget.id, name: "keyboard-mode", value: v ?? "");
- setState(() => current = v ?? '');
- Future.delayed(Duration(milliseconds: 300), close);
- }
+ // * Currently mobile does not enable map mode
+ // void changePhysicalKeyboardInputMode() async {
+ // var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
+ // gFFI.dialogManager.show((setState, close) {
+ // void setMode(String? v) async {
+ // await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? "");
+ // setState(() => current = v ?? '');
+ // Future.delayed(Duration(milliseconds: 300), close);
+ // }
+ //
+ // return CustomAlertDialog(
+ // title: Text(translate('Physical Keyboard Input Mode')),
+ // content: Column(mainAxisSize: MainAxisSize.min, children: [
+ // getRadio('Legacy mode', 'legacy', current, setMode,
+ // contentPadding: EdgeInsets.zero),
+ // getRadio('Map mode', 'map', current, setMode,
+ // contentPadding: EdgeInsets.zero),
+ // ]));
+ // }, clickMaskDismiss: true);
+ // }
+}
- return CustomAlertDialog(
- title: Text(translate('Physical Keyboard Input Mode')),
- content: Column(mainAxisSize: MainAxisSize.min, children: [
- getRadio('Legacy mode', 'legacy', current, setMode,
- contentPadding: EdgeInsets.zero),
- getRadio('Map mode', 'map', current, setMode,
- contentPadding: EdgeInsets.zero),
- ]));
- }, clickMaskDismiss: true);
+class KeyHelpTools extends StatefulWidget {
+ /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
+ final bool requestShow;
+
+ KeyHelpTools({required this.requestShow});
+
+ @override
+ State createState() => _KeyHelpToolsState();
+}
+
+class _KeyHelpToolsState extends State {
+ var _more = true;
+ var _fn = false;
+ var _pin = false;
+ final _keyboardVisibilityController = KeyboardVisibilityController();
+
+ InputModel get inputModel => gFFI.inputModel;
+
+ Widget wrap(String text, void Function() onPressed,
+ {bool? active, IconData? icon}) {
+ return TextButton(
+ style: TextButton.styleFrom(
+ minimumSize: Size(0, 0),
+ padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75),
+ //adds padding inside the button
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ //limits the touch area to the button area
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(5.0),
+ ),
+ backgroundColor: active == true ? MyTheme.accent80 : null,
+ ),
+ child: icon != null
+ ? Icon(icon, size: 14, color: Colors.white)
+ : Text(translate(text),
+ style: TextStyle(color: Colors.white, fontSize: 11)),
+ onPressed: onPressed);
}
- Widget getHelpTools() {
- final keyboard = isKeyboardShown();
- if (!keyboard) {
- return SizedBox();
+ @override
+ Widget build(BuildContext context) {
+ final hasModifierOn = inputModel.ctrl ||
+ inputModel.alt ||
+ inputModel.shift ||
+ inputModel.command;
+
+ if (!_pin && !hasModifierOn && !widget.requestShow) {
+ return Offstage();
}
final size = MediaQuery.of(context).size;
- wrap(String text, void Function() onPressed,
- [bool? active, IconData? icon]) {
- return TextButton(
- style: TextButton.styleFrom(
- minimumSize: Size(0, 0),
- padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75),
- //adds padding inside the button
- tapTargetSize: MaterialTapTargetSize.shrinkWrap,
- //limits the touch area to the button area
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(5.0),
- ),
- backgroundColor: active == true ? MyTheme.accent80 : null,
- ),
- child: icon != null
- ? Icon(icon, size: 17, color: Colors.white)
- : Text(translate(text),
- style: TextStyle(color: Colors.white, fontSize: 11)),
- onPressed: onPressed);
- }
final pi = gFFI.ffiModel.pi;
final isMac = pi.platform == kPeerPlatformMacOS;
final modifiers = [
wrap('Ctrl ', () {
setState(() => inputModel.ctrl = !inputModel.ctrl);
- }, inputModel.ctrl),
+ }, active: inputModel.ctrl),
wrap(' Alt ', () {
setState(() => inputModel.alt = !inputModel.alt);
- }, inputModel.alt),
+ }, active: inputModel.alt),
wrap('Shift', () {
setState(() => inputModel.shift = !inputModel.shift);
- }, inputModel.shift),
+ }, active: inputModel.shift),
wrap(isMac ? ' Cmd ' : ' Win ', () {
setState(() => inputModel.command = !inputModel.command);
- }, inputModel.command),
+ }, active: inputModel.command),
];
final keys = [
wrap(
@@ -770,7 +782,14 @@ class _RemotePageState extends State {
}
},
),
- _fn),
+ active: _fn),
+ wrap(
+ '',
+ () => setState(
+ () => _pin = !_pin,
+ ),
+ active: _pin,
+ icon: Icons.push_pin),
wrap(
' ... ',
() => setState(
@@ -781,7 +800,7 @@ class _RemotePageState extends State {
}
},
),
- _more),
+ active: _more),
];
final fn = [
SizedBox(width: 9999),
@@ -806,6 +825,9 @@ class _RemotePageState extends State {
wrap('End', () {
inputModel.inputKey('VK_END');
}),
+ wrap('Ins', () {
+ inputModel.inputKey('VK_INSERT');
+ }),
wrap('Del', () {
inputModel.inputKey('VK_DELETE');
}),
@@ -818,16 +840,16 @@ class _RemotePageState extends State {
SizedBox(width: 9999),
wrap('', () {
inputModel.inputKey('VK_LEFT');
- }, false, Icons.keyboard_arrow_left),
+ }, icon: Icons.keyboard_arrow_left),
wrap('', () {
inputModel.inputKey('VK_UP');
- }, false, Icons.keyboard_arrow_up),
+ }, icon: Icons.keyboard_arrow_up),
wrap('', () {
inputModel.inputKey('VK_DOWN');
- }, false, Icons.keyboard_arrow_down),
+ }, icon: Icons.keyboard_arrow_down),
wrap('', () {
inputModel.inputKey('VK_RIGHT');
- }, false, Icons.keyboard_arrow_right),
+ }, icon: Icons.keyboard_arrow_right),
wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () {
sendPrompt(isMac, 'VK_C');
}),
@@ -842,14 +864,15 @@ class _RemotePageState extends State {
return Container(
color: Color(0xAA000000),
padding: EdgeInsets.only(
- top: keyboard ? 24 : 4, left: 0, right: 0, bottom: 8),
+ top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
child: Wrap(
spacing: space,
runSpacing: space,
children: [SizedBox(width: 9999)] +
- (keyboard
- ? modifiers + keys + (_fn ? fn : []) + (_more ? more : [])
- : modifiers),
+ modifiers +
+ keys +
+ (_fn ? fn : []) +
+ (_more ? more : []),
));
}
}
@@ -893,32 +916,6 @@ class CursorPaint extends StatelessWidget {
}
}
-class ImagePainter extends CustomPainter {
- ImagePainter({
- required this.image,
- required this.x,
- required this.y,
- required this.scale,
- });
-
- ui.Image? image;
- double x;
- double y;
- double scale;
-
- @override
- void paint(Canvas canvas, Size size) {
- if (image == null) return;
- canvas.scale(scale, scale);
- canvas.drawImage(image!, Offset(x, y), Paint());
- }
-
- @override
- bool shouldRepaint(CustomPainter oldDelegate) {
- return oldDelegate != this;
- }
-}
-
void showOptions(
BuildContext context, String id, OverlayDialogManager dialogManager) async {
String quality =
@@ -1134,3 +1131,16 @@ void sendPrompt(bool isMac, String key) {
gFFI.inputModel.ctrl = old;
}
}
+
+class FABLocation extends FloatingActionButtonLocation {
+ FloatingActionButtonLocation location;
+ double offsetX;
+ double offsetY;
+ FABLocation(this.location, this.offsetX, this.offsetY);
+
+ @override
+ Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
+ final offset = location.getOffset(scaffoldGeometry);
+ return Offset(offset.dx + offsetX, offset.dy + offsetY);
+ }
+}
diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart
index 37cc77c8f..bc31ae2c4 100644
--- a/flutter/lib/mobile/widgets/gesture_help.dart
+++ b/flutter/lib/mobile/widgets/gesture_help.dart
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:toggle_switch/toggle_switch.dart';
-import '../../models/model.dart';
-
class GestureIcons {
static const String _family = 'gestureicons';
@@ -79,7 +77,10 @@ class _GestureHelpState extends State {
children: [
ToggleSwitch(
initialLabelIndex: _selectedIndex,
- inactiveBgColor: MyTheme.darkGray,
+ activeFgColor: Colors.white,
+ inactiveFgColor: Colors.white60,
+ activeBgColor: [MyTheme.accent],
+ inactiveBgColor: Theme.of(context).hintColor,
totalSwitches: 2,
minWidth: 150,
fontSize: 15,
@@ -188,7 +189,7 @@ class GestureInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- width: this.width,
+ width: width,
child: Column(
children: [
Icon(
@@ -199,11 +200,14 @@ class GestureInfo extends StatelessWidget {
SizedBox(height: 6),
Text(fromText,
textAlign: TextAlign.center,
- style: TextStyle(fontSize: 9, color: Colors.grey)),
+ style:
+ TextStyle(fontSize: 9, color: Theme.of(context).hintColor)),
SizedBox(height: 3),
Text(toText,
textAlign: TextAlign.center,
- style: TextStyle(fontSize: 12, color: Colors.black))
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).textTheme.bodySmall?.color))
],
));
}
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
index 18d42d143..5817e54fe 100644
--- a/flutter/lib/models/file_model.dart
+++ b/flutter/lib/models/file_model.dart
@@ -75,6 +75,10 @@ class FileModel extends ChangeNotifier {
return isLocal ? _localSortStyle : _remoteSortStyle;
}
+ bool getSortAscending(bool isLocal) {
+ return isLocal ? _localSortAscending : _remoteSortAscending;
+ }
+
FileDirectory _currentLocalDir = FileDirectory();
FileDirectory get currentLocalDir => _currentLocalDir;
diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart
index 8c37f50bd..9a5b06b14 100644
--- a/flutter/lib/models/input_model.dart
+++ b/flutter/lib/models/input_model.dart
@@ -58,9 +58,16 @@ class InputModel {
InputModel(this.parent);
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
- bind.sessionGetKeyboardMode(id: id).then((result) {
- keyboardMode = result.toString();
- });
+ if (!stateGlobal.grabKeyboard) {
+ return KeyEventResult.handled;
+ }
+
+ // * Currently mobile does not enable map mode
+ if (isDesktop) {
+ bind.sessionGetKeyboardMode(id: id).then((result) {
+ keyboardMode = result.toString();
+ });
+ }
final key = e.logicalKey;
if (e is RawKeyDownEvent) {
@@ -93,10 +100,9 @@ class InputModel {
}
}
- if (keyboardMode == 'map') {
+ // * Currently mobile does not enable map mode
+ if (isDesktop && keyboardMode == 'map') {
mapKeyboardMode(e);
- } else if (keyboardMode == 'translate') {
- legacyKeyboardMode(e);
} else {
legacyKeyboardMode(e);
}
@@ -483,10 +489,19 @@ class InputModel {
y /= canvasModel.scale;
x += d.x;
y += d.y;
+
+ if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) {
+ // If left mouse up, no early return.
+ if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
+ return;
+ }
+ }
+
if (type != '') {
x = 0;
y = 0;
}
+
evt['x'] = '${x.round()}';
evt['y'] = '${y.round()}';
var buttons = '';
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index ca99a5bd1..f4efe2f08 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
+import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
@@ -31,6 +33,7 @@ import 'input_model.dart';
import 'platform_model.dart';
typedef HandleMsgBox = Function(Map evt, String id);
+typedef ReconnectHandle = Function(OverlayDialogManager, String, bool);
final _waitForImage = {};
class FfiModel with ChangeNotifier {
@@ -137,6 +140,8 @@ class FfiModel with ChangeNotifier {
handleMsgBox(evt, peerId);
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
+ } else if (name == 'sync_peer_info') {
+ handleSyncPeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
@@ -247,6 +252,8 @@ class FfiModel with ChangeNotifier {
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
}
+ _updateSessionWidthHeight(peerId, display.width, display.height);
+
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
@@ -263,6 +270,7 @@ class FfiModel with ChangeNotifier {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay();
+ handleResolutions(peerId, evt["resolutions"]);
notifyListeners();
}
@@ -296,6 +304,8 @@ class FfiModel with ChangeNotifier {
showWaitUacDialog(id, dialogManager, type);
} else if (type == 'elevation-error') {
showElevationError(id, type, title, text, dialogManager);
+ } else if (type == "relay-hint") {
+ showRelayHintDialog(id, type, title, text, dialogManager);
} else {
var hasRetry = evt['hasRetry'] == 'true';
showMsgBox(id, type, title, text, link, hasRetry, dialogManager);
@@ -306,14 +316,12 @@ class FfiModel with ChangeNotifier {
showMsgBox(String id, String type, String title, String text, String link,
bool hasRetry, OverlayDialogManager dialogManager,
{bool? hasCancel}) {
- msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel);
+ msgBox(id, type, title, text, link, dialogManager,
+ hasCancel: hasCancel, reconnect: reconnect);
_timer?.cancel();
if (hasRetry) {
_timer = Timer(Duration(seconds: _reconnects), () {
- bind.sessionReconnect(id: id);
- clearPermissions();
- dialogManager.showLoading(translate('Connecting...'),
- onCancel: closeConnection);
+ reconnect(dialogManager, id, false);
});
_reconnects *= 2;
} else {
@@ -321,6 +329,51 @@ class FfiModel with ChangeNotifier {
}
}
+ void reconnect(
+ OverlayDialogManager dialogManager, String id, bool forceRelay) {
+ bind.sessionReconnect(id: id, forceRelay: forceRelay);
+ clearPermissions();
+ dialogManager.showLoading(translate('Connecting...'),
+ onCancel: closeConnection);
+ }
+
+ void showRelayHintDialog(String id, String type, String title, String text,
+ OverlayDialogManager dialogManager) {
+ dialogManager.show(tag: '$id-$type', (setState, close) {
+ onClose() {
+ closeConnection();
+ close();
+ }
+
+ final style =
+ ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
+ return CustomAlertDialog(
+ title: null,
+ content: msgboxContent(type, title,
+ "${translate(text)}\n\n${translate('relay_hint_tip')}"),
+ actions: [
+ dialogButton('Close', onPressed: onClose, isOutline: true),
+ dialogButton('Retry',
+ onPressed: () => reconnect(dialogManager, id, false)),
+ dialogButton('Connect via relay',
+ onPressed: () => reconnect(dialogManager, id, true),
+ buttonStyle: style),
+ dialogButton('Always connect via relay', onPressed: () {
+ const option = 'force-always-relay';
+ bind.sessionPeerOption(
+ id: id, name: option, value: bool2option(option, true));
+ reconnect(dialogManager, id, true);
+ }, buttonStyle: style),
+ ],
+ onCancel: onClose,
+ );
+ });
+ }
+
+ _updateSessionWidthHeight(String id, int width, int height) {
+ bind.sessionSetSize(id: id, width: display.width, height: display.height);
+ }
+
/// Handle the peer info event based on [evt].
handlePeerInfo(Map evt, String peerId) async {
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
@@ -371,8 +424,10 @@ class FfiModel with ChangeNotifier {
d.cursorEmbedded = d0['cursor_embedded'] == 1;
_pi.displays.add(d);
}
+ stateGlobal.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay < _pi.displays.length) {
_display = _pi.displays[_pi.currentDisplay];
+ _updateSessionWidthHeight(peerId, display.width, display.height);
}
if (displays.isNotEmpty) {
parent.target?.dialogManager.showLoading(
@@ -383,6 +438,55 @@ class FfiModel with ChangeNotifier {
}
Map features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1;
+ handleResolutions(peerId, evt["resolutions"]);
+ }
+ notifyListeners();
+ }
+
+ handleResolutions(String id, dynamic resolutions) {
+ try {
+ final List dynamicArray = jsonDecode(resolutions as String);
+ List arr = List.empty(growable: true);
+ for (int i = 0; i < dynamicArray.length; i++) {
+ var width = dynamicArray[i]["width"];
+ var height = dynamicArray[i]["height"];
+ if (width is int && width > 0 && height is int && height > 0) {
+ arr.add(Resolution(width, height));
+ }
+ }
+ arr.sort((a, b) {
+ if (b.width != a.width) {
+ return b.width - a.width;
+ } else {
+ return b.height - a.height;
+ }
+ });
+ _pi.resolutions = arr;
+ } catch (e) {
+ debugPrint("Failed to parse resolutions:$e");
+ }
+ }
+
+ /// Handle the peer info synchronization event based on [evt].
+ handleSyncPeerInfo(Map evt, String peerId) async {
+ if (evt['displays'] != null) {
+ List displays = json.decode(evt['displays']);
+ List newDisplays = [];
+ for (int i = 0; i < displays.length; ++i) {
+ Map d0 = displays[i];
+ var d = Display();
+ d.x = d0['x'].toDouble();
+ d.y = d0['y'].toDouble();
+ d.width = d0['width'];
+ d.height = d0['height'];
+ d.cursorEmbedded = d0['cursor_embedded'] == 1;
+ newDisplays.add(d);
+ }
+ _pi.displays = newDisplays;
+ stateGlobal.displaysCount.value = _pi.displays.length;
+ if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
+ _display = _pi.displays[_pi.currentDisplay];
+ }
}
notifyListeners();
}
@@ -417,29 +521,33 @@ class ImageModel with ChangeNotifier {
WeakReference parent;
- final List _callbacksOnFirstImage = [];
+ final List callbacksOnFirstImage = [];
ImageModel(this.parent);
- addCallbackOnFirstImage(Function(String) cb) =>
- _callbacksOnFirstImage.add(cb);
+ addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(Uint8List rgba) {
if (_waitForImage[id]!) {
_waitForImage[id] = false;
parent.target?.dialogManager.dismissAll();
if (isDesktop) {
- for (final cb in _callbacksOnFirstImage) {
+ for (final cb in callbacksOnFirstImage) {
cb(id);
}
}
}
+
final pid = parent.target?.id;
- ui.decodeImageFromPixels(
+ img.decodeImageFromPixels(
rgba,
parent.target?.ffiModel.display.width ?? 0,
parent.target?.ffiModel.display.height ?? 0,
- isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) {
+ isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
+ onPixelsCopied: () {
+ // Unlock the rgba memory from rust codes.
+ platformFFI.nextRgba(id);
+ }).then((image) {
if (parent.target?.id != pid) return;
try {
// my throw exception, because the listener maybe already dispose
@@ -1334,7 +1442,8 @@ class FFI {
void start(String id,
{bool isFileTransfer = false,
bool isPortForward = false,
- String? switchUuid}) {
+ String? switchUuid,
+ bool? forceRelay}) {
assert(!(isFileTransfer && isPortForward), 'more than one connect type');
if (isFileTransfer) {
connType = ConnType.fileTransfer;
@@ -1350,16 +1459,21 @@ class FFI {
}
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
- id: id,
- isFileTransfer: isFileTransfer,
- isPortForward: isPortForward,
- switchUuid: switchUuid ?? "",
- );
+ id: id,
+ isFileTransfer: isFileTransfer,
+ isPortForward: isPortForward,
+ switchUuid: switchUuid ?? "",
+ forceRelay: forceRelay ?? false);
final stream = bind.sessionStart(id: id);
final cb = ffiModel.startEventListener(id);
() async {
+ final useTextureRender = bind.mainUseTextureRender();
+ // Preserved for the rgba data.
await for (final message in stream) {
if (message is EventToUI_Event) {
+ if (message.field0 == "close") {
+ break;
+ }
try {
Map event = json.decode(message.field0);
await cb(event);
@@ -1367,9 +1481,30 @@ class FFI {
debugPrint('json.decode fail1(): $e, ${message.field0}');
}
} else if (message is EventToUI_Rgba) {
- imageModel.onRgba(message.field0);
+ if (useTextureRender) {
+ if (_waitForImage[id]!) {
+ _waitForImage[id] = false;
+ dialogManager.dismissAll();
+ for (final cb in imageModel.callbacksOnFirstImage) {
+ cb(id);
+ }
+ await canvasModel.updateViewStyle();
+ await canvasModel.updateScrollStyle();
+ }
+ } else {
+ // Fetch the image buffer from rust codes.
+ final sz = platformFFI.getRgbaSize(id);
+ if (sz == null || sz == 0) {
+ return;
+ }
+ final rgba = platformFFI.getRgba(id, sz);
+ if (rgba != null) {
+ imageModel.onRgba(rgba);
+ }
+ }
}
}
+ debugPrint('Exit session event loop');
}();
// every instance will bind a stream
this.id = id;
@@ -1390,12 +1525,12 @@ class FFI {
await setCanvasConfig(id, cursorModel.x, cursorModel.y, canvasModel.x,
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
}
- bind.sessionClose(id: id);
imageModel.update(null);
cursorModel.clear();
ffiModel.clear();
canvasModel.clear();
inputModel.resetModifiers();
+ await bind.sessionClose(id: id);
debugPrint('model $id closed');
id = '';
}
@@ -1426,6 +1561,17 @@ class Display {
}
}
+class Resolution {
+ int width = 0;
+ int height = 0;
+ Resolution(this.width, this.height);
+
+ @override
+ String toString() {
+ return 'Resolution($width,$height)';
+ }
+}
+
class Features {
bool privacyMode = false;
}
@@ -1439,6 +1585,7 @@ class PeerInfo {
int currentDisplay = 0;
List displays = [];
Features features = Features();
+ List resolutions = [];
}
const canvasKey = 'canvas';
diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart
index 34a673953..13f5b4587 100644
--- a/flutter/lib/models/native_model.dart
+++ b/flutter/lib/models/native_model.dart
@@ -9,6 +9,7 @@ import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
+import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win32/win32.dart' as win32;
@@ -23,8 +24,15 @@ class RgbaFrame extends Struct {
}
typedef F2 = Pointer Function(Pointer, Pointer);
-typedef F3 = void Function(Pointer, Pointer);
+typedef F3 = Pointer Function(Pointer);
+typedef F4 = Uint64 Function(Pointer);
+typedef F4Dart = int Function(Pointer);
+typedef F5 = Void Function(Pointer);
+typedef F5Dart = void Function(Pointer);
typedef HandleEvent = Future Function(Map evt);
+// pub fn session_register_texture(id: *const char, ptr: usize)
+typedef F6 = Void Function(Pointer, Uint64);
+typedef F6Dart = void Function(Pointer