fix conflicts

This commit is contained in:
Mălina-Ioana Popa 2023-01-24 17:05:08 +01:00
commit a4908882c6
277 changed files with 15032 additions and 6231 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,12 @@ env:
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
VERSION: "1.2.0"
#signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
jobs:
build-for-windows:
@ -41,13 +47,14 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Replace engine with rustdesk custom flutter engine
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-release-flutter.zip
Expand-Archive windows-x64-release-flutter.zip -DestinationPath engine
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Install Rust toolchain
@ -65,12 +72,7 @@ jobs:
- name: Install flutter rust bridge deps
run: |
dart pub global activate ffigen --version 5.0.1
$exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
Push-Location ..
git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
Pop-Location
cargo install flutter_rust_bridge_codegen
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@ -88,12 +90,41 @@ jobs:
- name: Build rustdesk
run: python3 .\build.py --portable --hwcodec --flutter
- name: Rename rustdesk
- name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
# certificatename: '${{ secrets.CERTNAME }}'
folder: './flutter/build/windows/runner/Release/'
recursive: true
- name: Build self-extracted executable
shell: bash
run: |
for name in rustdesk*??-install.exe; do
mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}.exe"
done
pushd ./libs/portable
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe
# - name: Rename rustdesk
# shell: bash
# run: |
# for name in rustdesk*??-install.exe; do
# mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe"
# done
- name: Sign rustdesk self-extracted file
uses: GermanBluefox/code-sign-action@v7
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
# certificatename: '${{ secrets.WINDOWS_PFX_NAME }}'
folder: './SignOutput'
recursive: false
- name: Publish Release
uses: softprops/action-gh-release@v1
@ -101,7 +132,7 @@ jobs:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk-*.exe
./SignOutput/rustdesk-*.exe
build-for-macOS:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
@ -112,13 +143,46 @@ jobs:
job:
- {
target: x86_64-apple-darwin,
os: macos-10.15,
os: macos-latest,
extra-build-args: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
keychain: rustdesk
- name: Check sign and import sign key
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
fileDir: ${{ github.workspace }}
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
popd
- name: Install build runtime
run: |
brew install llvm create-dmg nasm yasm cmake gcc wget ninja
@ -144,10 +208,7 @@ jobs:
- name: Install flutter rust bridge deps
shell: bash
run: |
dart pub global activate ffigen --version 5.0.1
# flutter_rust_bridge
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
cargo install flutter_rust_bridge_codegen
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@ -161,10 +222,6 @@ jobs:
run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
- name: Install cargo bundle tools
run: |
cargo install cargo-bundle
- name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
@ -180,10 +237,24 @@ jobs:
# --hwcodec not supported on macos yet
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm rustdesk-${{ env.VERSION }}.dmg || true
mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-untested-${{ matrix.job.target }}.dmg"
mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg"
done
- name: Publish DMG package
@ -331,27 +402,23 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install ffigen
run: |
dart pub global activate ffigen --version 5.0.1
- name: Install flutter rust bridge deps
shell: bash
run: |
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
cargo install flutter_rust_bridge_codegen
pushd flutter && flutter pub get && popd
- name: Run flutter rust bridge
run: |
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- name: Upload Artifcat
- name: Upload Artifact
uses: actions/upload-artifact@master
with:
name: bridge-artifact
path: |
./src/bridge_generated.rs
./src/bridge_generated.io.rs
./flutter/lib/generated_bridge.dart
./flutter/lib/generated_bridge.freezed.dart
@ -480,6 +547,7 @@ jobs:
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
@ -492,12 +560,14 @@ jobs:
BUILD_TOOLS_VERSION: "30.0.2"
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null
uses: actions/upload-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish apk package
- name: Publish signed apk package
if: env.ANDROID_SIGNING_KEY != null
uses: softprops/action-gh-release@v1
with:
prerelease: true
@ -505,6 +575,15 @@ jobs:
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk
build-rustdesk-lib-linux-amd64:
needs: [generate-bridge-linux, build-vcpkg-deps-linux]
name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
@ -528,6 +607,12 @@ jobs:
os: ubuntu-20.04,
extra-build-features: "flatpak",
}
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-features: "appimage",
}
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps:
- name: Maximize build space
@ -684,6 +769,13 @@ jobs:
use-cross: true,
extra-build-features: "",
}
- {
arch: aarch64,
target: aarch64-unknown-linux-gnu,
os: ubuntu-18.04, # just for naming package, not running host
use-cross: true,
extra-build-features: "appimage",
}
# - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
# - {
# arch: armv7,
@ -862,6 +954,13 @@ jobs:
use-cross: true,
extra-build-features: "",
}
- {
arch: aarch64,
target: aarch64-unknown-linux-gnu,
os: ubuntu-18.04, # just for naming package, not running host
use-cross: true,
extra-build-features: "appimage",
}
# - {
# arch: aarch64,
# target: aarch64-unknown-linux-gnu,
@ -885,7 +984,7 @@ jobs:
- name: Prepare env
run: |
sudo apt update -y
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
mkdir -p ./target/release/
- name: Restore the rustdesk lib file
@ -947,7 +1046,7 @@ jobs:
esac
python3 ./build.py --flutter --hwcodec --skip-cargo
# rpm package
echo -e "start packaging"
echo -e "start packaging fedora package"
pushd /workspace
case ${{ matrix.job.arch }} in
armv7)
@ -964,12 +1063,30 @@ jobs:
for name in rustdesk*??.rpm; do
mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm"
done
# rpm suse package
echo -e "start packaging suse package"
pushd /workspace
case ${{ matrix.job.arch }} in
armv7)
sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec
sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec
;;
aarch64)
sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec
;;
esac
HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb
pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
mkdir -p /opt/artifacts/rpm
for name in rustdesk*??.rpm; do
mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm"
done
- name: Rename rustdesk
shell: bash
run: |
for name in rustdesk*??.deb; do
mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb"
cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb"
done
- name: Publish debian package
@ -980,8 +1097,31 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
- name: Build appimage package
if: ${{ matrix.job.extra-build-features == 'appimage' }}
shell: bash
run: |
# set-up appimage-builder
pushd /tmp
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
popd
# run appimage-builder
pushd appimage
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
- name: Upload Artifcat
- name: Publish appimage package
if: ${{ matrix.job.extra-build-features == 'appimage' }}
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
- name: Upload Artifact
uses: actions/upload-artifact@master
if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }}
with:
@ -1077,6 +1217,12 @@ jobs:
os: ubuntu-18.04,
extra-build-features: "flatpak",
}
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-18.04,
extra-build-features: "appimage",
}
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps:
- name: Checkout source code
@ -1091,7 +1237,7 @@ jobs:
- name: Prepare env
run: |
sudo apt update -y
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
mkdir -p ./target/release/
- name: Restore the rustdesk lib file
@ -1141,15 +1287,30 @@ jobs:
for name in rustdesk*??.rpm; do
mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm"
done
# rpm suse package
pushd /workspace
case ${{ matrix.job.arch }} in
armv7)
sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec
;;
esac
HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb
pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
mkdir -p /opt/artifacts/rpm
for name in rustdesk*??.rpm; do
mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm"
done
- name: Rename rustdesk
shell: bash
run: |
for name in rustdesk*??.deb; do
mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb"
# use cp to duplicate deb files to fit other packages.
cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb"
done
- name: Publish debian package
if: ${{ matrix.job.extra-build-features == '' }}
uses: softprops/action-gh-release@v1
with:
prerelease: true
@ -1157,7 +1318,7 @@ jobs:
files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
- name: Upload Artifcat
- name: Upload Artifact
uses: actions/upload-artifact@master
if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }}
with:
@ -1213,6 +1374,29 @@ jobs:
files: |
res/rustdesk*.zst
- name: Build appimage package
if: ${{ matrix.job.extra-build-features == 'appimage' }}
shell: bash
run: |
# set-up appimage-builder
pushd /tmp
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
popd
# run appimage-builder
pushd appimage
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml
- name: Publish appimage package
if: ${{ matrix.job.extra-build-features == 'appimage' }}
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
- name: Publish fedora28/centos8 package
if: ${{ matrix.job.extra-build-features == '' }}
uses: softprops/action-gh-release@v1

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ cert.pfx
sciter.dll
**pdb
src/bridge_generated.rs
src/bridge_generated.io.rs
*deb
rustdesk
*.cache

538
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ 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"
magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/magnum-opus" }
magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true }
@ -59,11 +59,11 @@ base64 = "0.13"
sysinfo = "0.24"
num_cpus = "1.13"
bytes = { version = "1.2", features = ["serde"] }
default-net = "0.11.0"
default-net = { git = "https://github.com/Kingtous/default-net" }
wol-rs = "0.9.1"
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
flutter_rust_bridge = { version = "1.61.1", optional = true }
errno = "0.2.8"
rdev = { git = "https://github.com/asur4s/rdev" }
rdev = { git = "https://github.com/fufesou/rdev" }
url = { version = "2.1", features = ["serde"] }
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
@ -118,13 +118,15 @@ dbus = "0.9"
dbus-crossroads = "0.5"
gtk = "0.15"
libappindicator = "0.7"
glib = "0.16.5"
backtrace = "0.3"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11"
jni = "0.19"
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
flutter_rust_bridge = "1.61.1"
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"]
@ -142,7 +144,7 @@ winapi = { version = "0.3", features = [ "winnt" ] }
cc = "1.0"
hbb_common = { path = "libs/hbb_common" }
simple_rc = { path = "libs/simple_rc", optional = true }
flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
flutter_rust_bridge_codegen = "1.61.1"
[dev-dependencies]
hound = "3.5"
@ -151,9 +153,9 @@ hound = "3.5"
name = "RustDesk"
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", "libappindicator3-1", "libvdpau1", "libva2"]
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"]
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]
@ -162,4 +164,4 @@ codegen-units = 1
panic = 'abort'
strip = true
#opt-level = 'z' # only have smaller size after strip
rpath = true
rpath = true

View File

@ -39,6 +39,7 @@ Below are the servers you are using for free, it may change along the time. If y
| Germany | Codext | 4 vCPU / 8GB RAM |
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM |
## Dependencies
@ -70,7 +71,7 @@ Please download sciter dynamic library yourself.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
@ -197,7 +198,7 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
## Snapshot

View File

@ -0,0 +1,85 @@
# appimage-builder recipe see https://appimage-builder.readthedocs.io for details
version: 1
script:
- rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.0.deb
- tar -xvf ./data.tar.xz
- mkdir ./AppDir
- mv ./usr ./AppDir/usr
# 32x32 icon
- for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done
# desktop file
# - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop
- rm -rf ./AppDir/usr/share/applications
AppDir:
path: ./AppDir
app_info:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.2.0
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:
arch:
- arm64
allow_unauthenticated: true
sources:
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
include:
- libc6
- libgtk-3-0
- libxcb-randr0
- libxdo3
- libxfixes3
- libxcb-shape0
- libxcb-xfixes0
- libasound2
- libsystemd0
- curl
- libva-drm2
- libva-x11-2
- libvdpau1
- libgstreamer-plugins-base1.0-0
exclude:
- humanity-icon-theme
- hicolor-icon-theme
- adwaita-icon-theme
- ubuntu-mono
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
runtime:
env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
GDK_BACKEND: x11
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: aarch64
update-information: guess

View File

@ -0,0 +1,88 @@
# appimage-builder recipe see https://appimage-builder.readthedocs.io for details
version: 1
script:
- rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.0.deb
- tar -xvf ./data.tar.xz
- mkdir ./AppDir
- mv ./usr ./AppDir/usr
# 32x32 icon
- for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done
# desktop file
# - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop
- rm -rf ./AppDir/usr/share/applications
AppDir:
path: ./AppDir
app_info:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.2.0
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:
arch:
- amd64
allow_unauthenticated: true
sources:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
universe multiverse
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
bionic main
include:
- libc6:amd64
- libgtk-3-0
- libxcb-randr0
- libxdo3
- libxfixes3
- libxcb-shape0
- libxcb-xfixes0
- libasound2
- libsystemd0
- curl
- libva-drm2
- libva-x11-2
- libvdpau1
- libgstreamer-plugins-base1.0-0
exclude:
- humanity-icon-theme
- hicolor-icon-theme
- adwaita-icon-theme
- ubuntu-mono
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
runtime:
env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
GDK_BACKEND: x11
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: x86_64
update-information: guess

View File

@ -8,6 +8,7 @@ import urllib.request
import shutil
import hashlib
import argparse
import sys
windows = platform.platform().startswith('Windows')
osx = platform.platform().startswith(
@ -17,6 +18,14 @@ exe_path = 'target/release/' + hbb_name
flutter_win_target_dir = 'flutter/build/windows/runner/Release/'
skip_cargo = False
def custom_os_system(cmd):
err = os._system(cmd)
if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.")
sys.exit(-1)
# replace prebuilt os.system
os._system = os.system
os.system = custom_os_system
def get_version():
with open("Cargo.toml", encoding="utf-8") as fh:
@ -29,13 +38,15 @@ def get_version():
def parse_rc_features(feature):
available_features = {
'IddDriver': {
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64_pic_en.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5',
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5',
'exclude': ['README.md'],
},
'PrivacyMode': {
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1'
'/TempTopMostWindow_x64_pic_en.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5',
'include': ['WindowInjection.dll'],
}
}
apply_features = {}
@ -88,6 +99,11 @@ def make_parser():
action='store_true',
help='Build rustdesk libs with the flatpak feature enabled'
)
parser.add_argument(
'--appimage',
action='store_true',
help='Build rustdesk libs with the appimage feature enabled'
)
parser.add_argument(
'--skip-cargo',
action='store_true',
@ -133,8 +149,9 @@ def generate_build_script_for_docker():
def download_extract_features(features, res_dir):
proxy = ''
import re
proxy = ''
def req(url):
if not proxy:
return url
@ -145,6 +162,11 @@ def download_extract_features(features, res_dir):
return r
for (feat, feat_info) in features.items():
includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else []
includes = [ re.compile(p) for p in includes ]
excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else []
excludes = [ re.compile(p) for p in excludes ]
print(f'{feat} download begin')
download_filename = feat_info['zip_url'].split('/')[-1]
checksum_md5_response = urllib.request.urlopen(
@ -161,7 +183,22 @@ def download_extract_features(features, res_dir):
zip_file = zipfile.ZipFile(filename)
zip_list = zip_file.namelist()
for f in zip_list:
zip_file.extract(f, res_dir)
file_exclude = False
for p in excludes:
if p.match(f) is not None:
file_exclude = True
break
if file_exclude:
continue
file_include = False if includes else True
for p in includes:
if p.match(f) is not None:
file_include = True
break
if file_include:
print(f'extract file {f}')
zip_file.extract(f, res_dir)
zip_file.close()
os.remove(download_filename)
print(f'{feat} extract end')
@ -204,6 +241,8 @@ def get_features(args):
features.append('flutter')
if args.flatpak:
features.append('flatpak')
if args.appimage:
features.append('appimage')
print("features:", features)
return features
@ -217,7 +256,7 @@ Version: %s
Architecture: amd64
Maintainer: open-trade <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0
Description: A remote control software.
""" % version
@ -243,7 +282,7 @@ def build_flutter_deb(version, features):
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system('mkdir -p tmpdeb/usr/share/applications/')
os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions')
os.system('rm tmpdeb/usr/bin/rustdesk')
os.system('rm tmpdeb/usr/bin/rustdesk || true')
os.system(
'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/')
os.system(
@ -273,7 +312,8 @@ def build_flutter_deb(version, features):
def build_flutter_dmg(version, features):
if not skip_cargo:
os.system(f'cargo build --features {features} --lib --release')
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
# copy dylib
os.system(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
@ -437,6 +477,7 @@ def main():
if pa:
os.system('''
# buggy: rcodesign sign ... path/*, have to sign one by one
# install rcodesign via cargo install apple-codesign
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app
@ -449,12 +490,18 @@ def main():
version, 'rustdesk-%s.dmg' % version)
if pa:
os.system('''
# https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html
# https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html
# https://developer.apple.com/developer-id/
# goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key)
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg
codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg
# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html
rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg
# https://appstoreconnect.apple.com/access/api
# https://gregoryszorc.com/docs/apple-codesign/0.16.0/apple_codesign_rcodesign.html#notarizing-and-stapling
# p8 file is generated when you generate api key, download and put it under ~/.private_keys/
rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg
# verify: spctl -a -t exec -v /Applications/RustDesk.app
'''.format(pa, version))
'''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key')))
else:
print('Not signed')
else:

View File

@ -1,9 +1,16 @@
#[cfg(windows)]
fn build_windows() {
cc::Build::new().file("src/windows.cc").compile("windows");
let file = "src/platform/windows.cc";
cc::Build::new().file(file).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=windows.cc");
println!("cargo:rerun-if-changed={}", file);
}
#[cfg(target_os = "macos")]
fn build_mac() {
let file = "src/platform/macos.mm";
cc::Build::new().file(file).compile("macos");
println!("cargo:rerun-if-changed={}", file);
}
#[cfg(all(windows, feature = "inline"))]
@ -78,26 +85,35 @@ fn install_oboe() {
#[cfg(feature = "flutter")]
fn gen_flutter_rust_bridge() {
use lib_flutter_rust_bridge_codegen::{
config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts,
};
let llvm_path = match std::env::var("LLVM_HOME") {
Ok(path) => Some(vec![path]),
Err(_) => None,
};
// Tell Cargo that if the given file changes, to rerun this build script.
println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
// settings for fbr_codegen
let opts = lib_flutter_rust_bridge_codegen::Opts {
// Options for frb_codegen
let raw_opts = RawOpts {
// Path of input Rust code
rust_input: "src/flutter_ffi.rs".to_string(),
rust_input: vec!["src/flutter_ffi.rs".to_string()],
// Path of output generated Dart code
dart_output: "flutter/lib/generated_bridge.dart".to_string(),
dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()],
// Path of output generated C header
c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
// for other options lets use default
/// Path to the installed LLVM
llvm_path,
// for other options use defaults
..Default::default()
};
// run fbr_codegen
lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap();
// get opts from raw opts
let configs = config_parse(raw_opts);
// generation of rust api for ffi
let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap();
for config in configs.iter() {
frb_codegen(config, &all_symbols).unwrap();
}
}
fn main() {
@ -117,5 +133,8 @@ fn main() {
#[cfg(windows)]
build_windows();
#[cfg(target_os = "macos")]
build_mac();
#[cfg(target_os = "macos")]
println!("cargo:rustc-link-lib=framework=ApplicationServices");
println!("cargo:rerun-if-changed=build.rs");
}

View File

@ -35,7 +35,7 @@ efforts from contributors on the same issue.
- Add tests relevant to the fixed bug or new feature.
For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow).
For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Conduct

View File

@ -1,60 +1,60 @@
<p align="center">
<p dir="rtl" align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a dir="rtl" href="#اسکرین-شات-ها">اسنپ شات</a>
<a dir="rtl" href="#ساختار-پوشه-ها">ساختار</a>
<a dir="rtl" href="#نحوه-ساخت-با-داکر">داکر</a>
<a dir="rtl" href="#ساخت">ساخت</a>
<a dir="rtl" href="#سرورهای-عمومی-رایگان">سرور</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
&#x202b;<b>برای ترجمه این <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang"> RustDesk UI</a> ،README و <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> به زبان مادری شما به کمکتون نیاز داریم
<a href="#تصاویر-محیط-نرمافزار">تصاویر محیط نرم‌افزار</a>
<a href="#ساختار-پوشه-ها">ساختار</a>
<a href="#نحوه-ساخت-با-داکر">داکر</a>
<a href="#ساخت">ساخت</a>
<a href="#سرورهای-عمومی-رایگان">سرور</a>
</p>
<p align="center" dir="auto">[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]</p>
<p dir="rtl" align="center"><b>برای ترجمه این سند (README)، <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang" dir="rtl">رابط کاربری RustDesk</a>، <a href="https://github.com/rustdesk/doc.rustdesk.com" dir="rtl">و مستندات آن</a> به زبان مادری شما به کمکتان نیازمندیم. </b></p>
با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی.
راست‌دسک (RustDesk) نرم‌افزاری برای گارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید.
می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا
[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk).
&#x202b;راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید.
ما از مشارکت همه استقبال می کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید.
[راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[راستدسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[دانلود باینری](https://github.com/rustdesk/rustdesk/releases)
[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases)
## سرورهای عمومی رایگان
سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود.
شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد.
| موقعیت | سرویس دهنده | مشخصات |
| --------- | ------------- | ------------------ |
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Germany | Codext | 4 vCPU / 8GB RAM |
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| کره‌ی جنوبی، سئول | AWS lightsail | 1 vCPU / 0.5GB RAM |
| آلمان | Hetzner | 2 vCPU / 4GB RAM |
| آلمان | Codext | 4 vCPU / 8GB RAM |
| فنلاند، هلسینکی | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| ایالات متحده، اَشبرن | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## وابستگی ها
نسخه‌های دسکتاپ از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند، لطفا کتابخانه پویا sciter را خودتان دانلود کنید.
نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید.
[ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
- [ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll)
- [لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so)
- [مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
نسخه های موبایل از Flutter استفاده می کنند. بعداً نسخه دسکتاپ را از Sciter به Flutter منتقل خواهیم کرد.
نسخه های همراه از Flutter استفاده می کنند. نسخه‌ی رومیزی را هم از Sciter به Flutter منتقل خواهیم کرد.
## مراحل بنیادین برای ساخت
## نیازمندی‌های ساخت
&#x202b;- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید
- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید
&#x202b;- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید:
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- Linux/MacOS: `vcpkg install libvpx libyuv opus`
- run `cargo run`
- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید.
- بسته‌های vcpkg مورد نیاز را نصب کنید:
- ویندوز: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- مک و لینوکس: `vcpkg install libvpx libyuv opus`
- این دستور را اجرا کنید: `cargo run`
## [ساخت](https://rustdesk.com/docs/en/dev/build/)
@ -118,11 +118,11 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
### تغییر Wayland به (X11 (Xorg
راست دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید.
راستدسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید.
## نحوه ساخت با داکر
این مخزن گیت را کلون کنید و کانتینر را به روش زیر بسازید
این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید
```sh
git clone https://github.com/rustdesk/rustdesk
@ -130,13 +130,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
سپس، هر بار که نیاز به ساخت اپلیکیشن داشتید، دستور زیر را اجرا کنید:
سپس، هر بار که نیاز به ساخت ترم‌افزار داشتید، دستور زیر را اجرا کنید:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
توجه داشته باشید که ساخت اول ممکن است قبل از کش شدن وابستگی ها بیشتر طول بکشد، دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `<OPTIONAL-ARGS>` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور:
توجه داشته باشید که نخستین ساخت ممکن است به دلیل محلی نبودن وابستگی‌ها بیشتر طول بکشد. اما دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `<OPTIONAL-ARGS>` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور:
```sh
target/debug/rustdesk
@ -163,7 +163,7 @@ target/release/rustdesk
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
## اسکرین شات ها
## تصاویر محیط نرم‌افزار
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)

View File

@ -2,9 +2,9 @@ An open-source remote desktop application, the open source TeamViewer alternativ
Source code: https://github.com/rustdesk/rustdesk
Doc: https://rustdesk.com/docs/en/manual/mobile/
In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control.
In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control.
In addtion to remote control, you can also transfer files between Android devices and PCs easily with RustDesk.
In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk.
You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 267 KiB

View File

@ -0,0 +1,11 @@
Une application de bureau à distance open source, l'alternative open source à TeamViewer.
Code source : https://github.com/rustdesk/rustdesk
Doc : https://rustdesk.com/docs/en/manual/mobile/
Pour qu'un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher, vous devez autoriser RustDesk à utiliser le service "Accessibilité", RustDesk utilise l'API AccessibilityService pour implémenter la télécommande Addroid.
En plus du contrôle à distance, vous pouvez également transférer facilement des fichiers entre des appareils Android et des PC avec RustDesk.
Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, ou l'auto-hébergement, ou écrire votre propre serveur de rendez-vous/relais. Le serveur auto-hébergé est gratuit et open source : https://github.com/rustdesk/rustdesk-server
Veuillez télécharger et installer la version de bureau à partir de : https://rustdesk.com, vous pourrez alors accéder et contrôler votre bureau à partir de votre mobile, ou contrôler votre mobile à partir du bureau.

View File

@ -0,0 +1 @@
Une application de bureau à distance open source, l'alternative open source à TeamViewer.

1
flutter/.gitignore vendored
View File

@ -54,3 +54,4 @@ lib/generated_bridge.freezed.dart
flutter_export_environment.sh
Flutter-Generated.xcconfig
key.jks
macos/rustdesk.xcodeproj/project.xcworkspace/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 253" width="261" height="253"><path fill="#0ff" d="m1 217c0-5.5 4.5-10 10-10h60c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-60c-5.5 0-10-4.5-10-10z"/><path fill="#487997" d="m89 216c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#c0ff00" d="m3 166c0-5.5 4.5-10 10-10h34c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-34c-5.5 0-10-4.5-10-10z"/><path fill="#00f" d="m63 166c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m140 215c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m190 215c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v28c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m112 166c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m165 165c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m216 164c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m0 115c0-5.5 4.5-10 10-10h61c5.5 0 10 4.5 10 10v23c0 5.5-4.5 10-10 10h-61c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m90 116c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m139 116c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m191 115c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m2 62c0-5.5 4.5-10 10-10h50c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-50c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m79 62c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m131 64c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m182 63c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m0 10c0-5.5 4.5-10 10-10h28c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-28c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m54 11c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m105 12c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m156 13c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path d="m16.7 171.9v1.9q-1.1-0.5-2.1-0.8-1-0.2-1.9-0.2-1.7 0-2.5 0.6-0.9 0.6-0.9 1.8 0 0.9 0.6 1.4 0.6 0.5 2.2 0.8l1.2 0.3q2.2 0.4 3.2 1.4 1.1 1.1 1.1 2.9 0 2.1-1.4 3.2-1.5 1.1-4.2 1.1-1 0-2.2-0.3-1.2-0.2-2.4-0.6v-2.1q1.2 0.7 2.3 1 1.2 0.4 2.3 0.4 1.7 0 2.6-0.7 0.9-0.6 0.9-1.9 0-1.1-0.6-1.7-0.7-0.6-2.2-0.9l-1.2-0.2q-2.2-0.4-3.2-1.4-1-0.9-1-2.6 0-1.9 1.4-3 1.3-1.1 3.7-1.1 1.1 0 2.1 0.1 1.1 0.2 2.2 0.6zm13 7.5v6.6h-1.8v-6.5q0-1.6-0.6-2.4-0.6-0.7-1.8-0.7-1.5 0-2.3 0.9-0.9 0.9-0.9 2.5v6.2h-1.8v-15.2h1.8v6q0.7-1 1.5-1.5 0.9-0.5 2.1-0.5 1.8 0 2.8 1.2 1 1.1 1 3.4zm3.6 6.6v-10.9h1.8v10.9zm0-12.9v-2.3h1.7v2.3zm9.4-2.3h1.7v1.5h-1.7q-0.9 0-1.3 0.4-0.4 0.4-0.4 1.4v1h3v1.4h-3v9.5h-1.8v-9.5h-1.7v-1.4h1.7v-0.8q0-1.8 0.8-2.7 0.9-0.8 2.7-0.8zm2.9 1.2h1.8v3.1h3.7v1.4h-3.7v5.9q0 1.3 0.3 1.7 0.4 0.4 1.5 0.4h1.9v1.5h-1.9q-2.1 0-2.8-0.8-0.8-0.8-0.8-2.8v-5.9h-1.4v-1.4h1.4z"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 253" width="260" height="253"><path fill="#c0ff00" d="m0 167c0-5.5 4.5-10 10-10h90c5.5 0 10 4.5 10 10v23c0 5.5-4.5 10-10 10h-90c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m3 63c0-5.5 4.5-10 10-10h46c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-46c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m1 114c0-5.5 4.5-10 10-10h62c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-62c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m114 166c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m90 117c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v22c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m81 64c0-5.5 4.5-10 10-10h22c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-22c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m54 10c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m2 10c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m2 216c0-5.5 4.5-10 10-10h59c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-59c-5.5 0-10-4.5-10-10z"/><path fill="#487997" d="m89 217c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m141 217c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m191 216c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m166 164c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m215 165c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m138 111c0-5.5 4.5-10 10-10h28c5.5 0 10 4.5 10 10v29c0 5.5-4.5 10-10 10h-28c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m192 112c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v29c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m129 64c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m182 62c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m105 11c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m154 12c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path d="m42.7 172.9v1.9q-1.1-0.5-2.1-0.8-1-0.2-1.9-0.2-1.7 0-2.5 0.6-0.9 0.6-0.9 1.8 0 0.9 0.6 1.4 0.6 0.5 2.2 0.8l1.2 0.3q2.2 0.4 3.2 1.4 1.1 1.1 1.1 2.9 0 2.1-1.4 3.2-1.5 1.1-4.2 1.1-1 0-2.2-0.3-1.2-0.2-2.4-0.6v-2.1q1.2 0.7 2.3 1 1.2 0.4 2.3 0.4 1.7 0 2.6-0.7 0.9-0.6 0.9-1.9 0-1.1-0.6-1.7-0.7-0.6-2.2-0.9l-1.2-0.2q-2.2-0.4-3.2-1.4-1-0.9-1-2.6 0-1.9 1.4-3 1.3-1.1 3.7-1.1 1.1 0 2.1 0.1 1.1 0.2 2.2 0.6zm13 7.5v6.6h-1.8v-6.5q0-1.6-0.6-2.4-0.6-0.7-1.8-0.7-1.5 0-2.3 0.9-0.9 0.9-0.9 2.5v6.2h-1.8v-15.2h1.8v6q0.7-1 1.5-1.5 0.9-0.5 2.1-0.5 1.8 0 2.8 1.2 1 1.1 1 3.4zm3.6 6.6v-10.9h1.8v10.9zm0-12.9v-2.3h1.7v2.3zm9.4-2.3h1.7v1.5h-1.7q-0.9 0-1.3 0.4-0.4 0.4-0.4 1.4v1h3v1.4h-3v9.5h-1.8v-9.5h-1.7v-1.4h1.7v-0.8q0-1.8 0.8-2.7 0.9-0.8 2.7-0.8zm2.9 1.2h1.8v3.1h3.7v1.4h-3.7v5.9q0 1.3 0.3 1.7 0.4 0.4 1.5 0.4h1.9v1.5h-1.9q-2.1 0-2.8-0.8-0.8-0.8-0.8-2.8v-5.9h-1.4v-1.4h1.4z"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,7 +1,7 @@
#!/bin/bash
# Build libyuv / opus / libvpx / oboe for Android
# Required:
# Build libyuv / opus / libvpx / oboe for Android
# Required:
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
# 2. vcpkg initialized
# 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`)
@ -23,7 +23,7 @@ HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/l
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
function build {
ANDROID_ABI=$1
ANDROID_ABI=$1
VCPKG_TARGET=$2
NDK_LLVM_TARGET=$3
LIBVPX_TARGET=$4
@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch
# x86_64-linux-android
# i686-linux-android
# LIBVPX_TARGET :
# arm64-android-gcc
# armv7-android-gcc
# LIBVPX_TARGET :
# arm64-android-gcc
# armv7-android-gcc
# x86_64-android-gcc
# x86-android-gcc
# x86-android-gcc
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
# rm -rf build/libvpx
# rm -rf build/oboe

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -46,7 +46,7 @@ var isWebDesktop = false;
var version = "";
int androidVersion = 0;
/// only avaliable for Windows target
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@ -99,22 +99,28 @@ class IconFont {
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
const ColorThemeExtension({
required this.border,
required this.highlight,
});
final Color? border;
final Color? highlight;
static const light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
highlight: Color(0xFFE5E5E5),
);
static const dark = ColorThemeExtension(
border: Color(0xFF555555),
highlight: Color(0xFF3F3F3F),
);
@override
ThemeExtension<ColorThemeExtension> copyWith({Color? border}) {
ThemeExtension<ColorThemeExtension> copyWith(
{Color? border, Color? highlight}) {
return ColorThemeExtension(
border: border ?? this.border,
highlight: highlight ?? this.highlight,
);
}
@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
}
return ColorThemeExtension(
border: Color.lerp(border, other.border, t),
highlight: Color.lerp(highlight, other.highlight, t),
);
}
}
@ -215,18 +222,15 @@ class MyTheme {
}
static void changeDarkMode(ThemeMode mode) {
final preference = getThemeModePreference();
if (preference != mode) {
Get.changeThemeMode(mode);
if (desktopType == DesktopType.main) {
if (mode == ThemeMode.system) {
bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
} else {
bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
Get.changeThemeMode(mode);
if (desktopType == DesktopType.main) {
bind.mainChangeTheme(dark: currentThemeMode().toShortString());
}
bind.mainChangeTheme(dark: mode.toShortString());
}
}
@ -545,6 +549,10 @@ class OverlayDialogManager {
hideMobileActionsOverlay();
}
}
bool existing(String tag) {
return _dialogs.keys.contains(tag);
}
}
void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
@ -605,8 +613,9 @@ class CustomAlertDialog extends StatelessWidget {
Future.delayed(Duration.zero, () {
if (!focusNode.hasFocus) focusNode.requestFocus();
});
return Focus(
focusNode: focusNode,
FocusScopeNode scopeNode = FocusScopeNode();
return FocusScope(
node: scopeNode,
autofocus: true,
onKey: (node, key) {
if (key.logicalKey == LogicalKeyboardKey.escape) {
@ -618,6 +627,11 @@ class CustomAlertDialog extends StatelessWidget {
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();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
@ -626,8 +640,14 @@ class CustomAlertDialog extends StatelessWidget {
title: title,
contentPadding: EdgeInsets.symmetric(
horizontal: contentPadding ?? 25, vertical: 10),
content:
ConstrainedBox(constraints: contentBoxConstraints, child: content),
content: ConstrainedBox(
constraints: contentBoxConstraints,
child: Theme(
data: ThemeData(
inputDecorationTheme: InputDecorationTheme(
isDense: true, contentPadding: EdgeInsets.all(15)),
),
child: content)),
actions: actions,
),
);
@ -660,24 +680,25 @@ void msgBox(String id, String type, String title, String text, String link,
if (type != "connecting" && type != "success" && !type.contains("nook")) {
hasOk = true;
buttons.insert(0, msgBoxButton(translate('OK'), submit));
buttons.insert(0, dialogButton('OK', onPressed: submit));
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
type != "restarting";
if (hasCancel) {
buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
buttons.insert(
0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
}
// TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
msgBoxButton(translate('Close'), () {
dialogButton('Close', onPressed: () {
dialogManager.dismissAll();
}));
}
if (link.isNotEmpty) {
buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink));
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
}
dialogManager.show(
(setState, close) => CustomAlertDialog(
@ -926,7 +947,8 @@ bool option2bool(String option, String value) {
} else if (option.startsWith("allow-") ||
option == "stop-service" ||
option == "direct-server" ||
option == "stop-rendezvous-service") {
option == "stop-rendezvous-service" ||
option == "force-always-relay") {
res = value == "Y";
} else {
assert(false);
@ -942,7 +964,8 @@ String bool2option(String option, bool b) {
} else if (option.startsWith('allow-') ||
option == "stop-service" ||
option == "direct-server" ||
option == "stop-rendezvous-service") {
option == "stop-rendezvous-service" ||
option == "force-always-relay") {
res = b ? 'Y' : '';
} else {
assert(false);
@ -971,11 +994,13 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
/// Get the image for the current [platform].
Widget getPlatformImage(String platform, {double size = 50}) {
platform = platform.toLowerCase();
if (platform == 'mac os') {
if (platform == kPeerPlatformMacOS) {
platform = 'mac';
} else if (platform != 'linux' && platform != 'android') {
} else if (platform != kPeerPlatformLinux &&
platform != kPeerPlatformAndroid) {
platform = 'win';
} else {
platform = platform.toLowerCase();
}
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
@ -1014,7 +1039,7 @@ class LastWindowPosition {
return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
m["offsetHeight"], m["isMaximized"]);
} catch (e) {
debugPrint(e.toString());
debugPrintStack(label: e.toString());
return null;
}
}
@ -1148,7 +1173,7 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name);
var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) {
debugPrint("window position saved, but cannot be parsed");
debugPrint("no window position saved, ignoring position restoration");
return false;
}
@ -1213,7 +1238,7 @@ Future<void> initUniLinks() async {
}
parseRustdeskUri(initialLink);
} catch (err) {
debugPrint("$err");
debugPrintStack(label: "$err");
}
}
@ -1236,23 +1261,28 @@ StreamSubscription? listenUniLinks() {
/// Returns true if we successfully handle the startup arguments.
bool checkArguments() {
// bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
// check connect args
final connectIndex = bootArgs.indexOf("--connect");
final connectIndex = kBootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
}
String? arg =
bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1];
if (arg != null) {
if (arg.startsWith(kUniLinksPrefix)) {
return parseRustdeskUri(arg);
String? id =
kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
final switchUuidIndex = kBootArgs.indexOf("--switch_uuid");
String? switchUuid = kBootArgs.length < switchUuidIndex + 1
? null
: kBootArgs[switchUuidIndex + 1];
if (id != null) {
if (id.startsWith(kUniLinksPrefix)) {
return parseRustdeskUri(id);
} else {
// remove "--connect xxx" in the `bootArgs` array
bootArgs.removeAt(connectIndex);
bootArgs.removeAt(connectIndex);
kBootArgs.removeAt(connectIndex);
kBootArgs.removeAt(connectIndex);
// fallback to peer id
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(arg);
rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid);
});
return true;
}
@ -1282,19 +1312,34 @@ bool callUniLinksUriHandler(Uri uri) {
// new connection
if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
final peerId = uri.path.substring("/new/".length);
var param = uri.queryParameters;
String? switch_uuid = param["switch_uuid"];
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(peerId);
rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid);
});
return true;
return false;
}
return false;
}
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP);
} else {
await rustDeskWinManager.newRemoteDesktop(id);
}
}
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
void connect(BuildContext context, String id,
connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
@ -1304,12 +1349,20 @@ void connect(BuildContext context, String id,
"more than one connect type");
if (isDesktop) {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP);
if (desktopType == DesktopType.main) {
await connectMainDesktop(
id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
);
} else {
await rustDeskWinManager.newRemoteDesktop(id);
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
});
}
} else {
if (isFileTransfer) {
@ -1340,13 +1393,13 @@ void connect(BuildContext context, String id,
}
}
Future<Map<String, String>> getHttpHeaders() async {
Map<String, String> getHttpHeaders() {
return {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
};
}
// Simple wrapper of built-in types for refrence use.
// Simple wrapper of built-in types for reference use.
class SimpleWrapper<T> {
T value;
SimpleWrapper(this.value);
@ -1382,7 +1435,7 @@ Future<void> reloadAllWindows() async {
/// Indicate the flutter app is running in portable mode.
///
/// [Note]
/// Portable build is only avaliable on Windows.
/// Portable build is only available on Windows.
bool isRunningInPortableMode() {
if (!Platform.isWindows) {
return false;
@ -1391,7 +1444,7 @@ bool isRunningInPortableMode() {
}
/// Window status callback
void onActiveWindowChanged() async {
Future<void> onActiveWindowChanged() async {
print(
"[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
if (rustDeskWinManager.getActiveWindows().isEmpty) {
@ -1402,7 +1455,7 @@ void onActiveWindowChanged() async {
rustDeskWinManager.closeAllSubWindows()
]);
} catch (err) {
debugPrint("$err");
debugPrintStack(label: "$err");
} finally {
await windowManager.setPreventClose(false);
await windowManager.close();
@ -1483,3 +1536,104 @@ Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
bool get kUseCompatibleUiMode =>
Platform.isWindows &&
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
class ServerConfig {
late String idServer;
late String relayServer;
late String apiServer;
late String key;
ServerConfig(
{String? idServer, String? relayServer, String? apiServer, String? key}) {
this.idServer = idServer?.trim() ?? '';
this.relayServer = relayServer?.trim() ?? '';
this.apiServer = apiServer?.trim() ?? '';
this.key = key?.trim() ?? '';
}
/// decode from shared string (from user shared or rustdesk-server generated)
/// also see [encode]
/// throw when decoding failure
ServerConfig.decode(String msg) {
final input = msg.split('').reversed.join('');
final bytes = base64Decode(base64.normalize(input));
final json = jsonDecode(utf8.decode(bytes));
idServer = json['host'] ?? '';
relayServer = json['relay'] ?? '';
apiServer = json['api'] ?? '';
key = json['key'] ?? '';
}
/// encode to shared string
/// also see [ServerConfig.decode]
String encode() {
Map<String, String> config = {};
config['host'] = idServer.trim();
config['relay'] = relayServer.trim();
config['api'] = apiServer.trim();
config['key'] = key.trim();
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
.split('')
.reversed
.join();
}
/// from local options
ServerConfig.fromOptions(Map<String, dynamic> options)
: idServer = options['custom-rendezvous-server'] ?? "",
relayServer = options['relay-server'] ?? "",
apiServer = options['api-server'] ?? "",
key = options['key'] ?? "";
}
Widget dialogButton(String text,
{required VoidCallback? onPressed,
bool isOutline = false,
TextStyle? style}) {
if (isDesktop) {
if (isOutline) {
return OutlinedButton(
onPressed: onPressed,
child: Text(translate(text), style: style),
);
} else {
return ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: onPressed,
child: Text(translate(text), style: style),
);
}
} else {
return TextButton(
onPressed: onPressed,
child: Text(
translate(text),
style: style,
));
}
}
int version_cmp(String v1, String v2) {
return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
}
String getWindowName({WindowType? overrideType}) {
switch (overrideType ?? kWindowType) {
case WindowType.Main:
return "RustDesk";
case WindowType.FileTransfer:
return "File Transfer - RustDesk";
case WindowType.PortForward:
return "Port Forward - RustDesk";
case WindowType.RemoteDesktop:
return "Remote Desktop - RustDesk";
default:
break;
}
return "RustDesk";
}
String getWindowNameWithId(String id, {WindowType? overrideType}) {
return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}";
}

View File

@ -0,0 +1,119 @@
import 'package:flutter_hbb/models/peer_model.dart';
class HttpType {
static const kAuthReqTypeAccount = "account";
static const kAuthReqTypeMobile = "mobile";
static const kAuthReqTypeSMSCode = "sms_code";
static const kAuthReqTypeEmailCode = "email_code";
static const kAuthResTypeToken = "access_token";
static const kAuthResTypeEmailCheck = "email_check";
}
class UserPayload {
String name = '';
String email = '';
String note = '';
int? status;
String grp = '';
bool isAdmin = false;
UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
email = json['email'] ?? '',
note = json['note'] ?? '',
status = json['status'],
grp = json['grp'] ?? '',
isAdmin = json['is_admin'] == true;
}
class PeerPayload {
String id = '';
String info = '';
int? status;
String user = '';
String user_name = '';
String note = '';
PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
info = json['info'] ?? '',
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
return Peer.fromJson({"id": p.id});
}
}
class LoginRequest {
String? username;
String? password;
String? id;
String? uuid;
bool? autoLogin;
String? type;
String? verificationCode;
String? deviceInfo;
LoginRequest(
{this.username,
this.password,
this.id,
this.uuid,
this.autoLogin,
this.type,
this.verificationCode,
this.deviceInfo});
LoginRequest.fromJson(Map<String, dynamic> json) {
username = json['username'];
password = json['password'];
id = json['id'];
uuid = json['uuid'];
autoLogin = json['autoLogin'];
type = json['type'];
verificationCode = json['verificationCode'];
deviceInfo = json['deviceInfo'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['username'] = username ?? '';
data['password'] = password ?? '';
data['id'] = id ?? '';
data['uuid'] = uuid ?? '';
data['autoLogin'] = autoLogin ?? '';
data['type'] = type ?? '';
data['verificationCode'] = verificationCode ?? '';
data['deviceInfo'] = deviceInfo ?? '';
return data;
}
}
class LoginResponse {
String? access_token;
String? type;
UserPayload? user;
LoginResponse({this.access_token, this.type, this.user});
LoginResponse.fromJson(Map<String, dynamic> json) {
access_token = json['access_token'];
type = json['type'];
user = json['user'] != null ? UserPayload.fromJson(json['user']) : null;
}
}
class RequestException implements Exception {
int statusCode;
String cause;
RequestException(this.statusCode, this.cause);
@override
String toString() {
return "RequestException, statusCode: $statusCode, error: $cause";
}
}

View File

@ -1,15 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
import '../../common.dart';
import '../../desktop/pages/desktop_home_page.dart';
import '../../mobile/pages/settings_page.dart';
import 'login.dart';
class AddressBook extends StatefulWidget {
final EdgeInsets? menuPadding;
@ -27,7 +26,6 @@ class _AddressBookState extends State<AddressBook> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb());
}
@override
@ -41,25 +39,12 @@ class _AddressBookState extends State<AddressBook> {
}
});
handleLogin() {
// TODO refactor login dialog for desktop and mobile
if (isDesktop) {
loginDialog().then((success) {
if (success) {
gFFI.abModel.pullAb();
}
});
} else {
showLogin(gFFI.dialogManager);
}
}
Future<Widget> buildBody(BuildContext context) async {
return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
return Center(
child: InkWell(
onTap: handleLogin,
onTap: loginDialog,
child: Text(
translate("Login"),
style: const TextStyle(decoration: TextDecoration.underline),
@ -90,7 +75,7 @@ class _AddressBookState extends State<AddressBook> {
Text(translate(error)),
TextButton(
onPressed: () {
setState(() {});
gFFI.abModel.pullAb();
},
child: Text(translate("Retry")))
],
@ -237,29 +222,32 @@ class _AddressBookState extends State<AddressBook> {
}
void abAddId() async {
var field = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.tags);
var selectedTag = List<dynamic>.empty(growable: true).obs;
final style = TextStyle(fontSize: 14.0);
String? errorMsg;
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
msg = "";
isInProgress = true;
errorMsg = null;
});
field = controller.text.trim();
if (field.isEmpty) {
String id = idController.id;
if (id.isEmpty) {
// pass
} else {
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
field = ids.join(',');
for (final newId in ids) {
if (gFFI.abModel.idContainBy(newId)) {
continue;
}
gFFI.abModel.addId(newId);
if (gFFI.abModel.idContainBy(id)) {
setState(() {
isInProgress = false;
errorMsg = translate('ID already exists');
});
return;
}
gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag);
await gFFI.abModel.pushAb();
this.setState(() {});
// final currentPeers
@ -272,21 +260,70 @@ class _AddressBookState extends State<AddressBook> {
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("whitelist_sep")),
const SizedBox(
height: 8.0,
),
Row(
Column(
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg),
Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Text(
'*',
style: TextStyle(color: Colors.red, fontSize: 14),
),
controller: controller,
focusNode: FocusNode()..requestFocus()),
Text(
'ID',
style: style,
),
],
),
),
TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration(
isDense: true,
border: OutlineInputBorder(),
errorText: errorMsg),
style: style,
),
Align(
alignment: Alignment.centerLeft,
child: Text(
translate('Alias'),
style: style,
),
).marginOnly(top: 8, bottom: 2),
TextField(
controller: aliasController,
decoration: InputDecoration(
border: OutlineInputBorder(),
isDense: true,
),
style: style,
),
Align(
alignment: Alignment.centerLeft,
child: Text(
translate('Tags'),
style: style,
),
).marginOnly(top: 8),
Container(
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
},
showActionMenu: false))
.toList(growable: false),
),
),
],
),
@ -298,8 +335,8 @@ class _AddressBookState extends State<AddressBook> {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@ -365,8 +402,8 @@ class _AddressBookState extends State<AddressBook> {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,

View File

@ -0,0 +1,121 @@
// https://github.com/rodrigobastosv/fancy_password_field
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:get/get.dart';
import 'package:password_strength/password_strength.dart';
abstract class ValidationRule {
String get name;
bool validate(String value);
}
class UppercaseValidationRule extends ValidationRule {
@override
String get name => translate('uppercase');
@override
bool validate(String value) {
return value.contains(RegExp(r'[A-Z]'));
}
}
class LowercaseValidationRule extends ValidationRule {
@override
String get name => translate('lowercase');
@override
bool validate(String value) {
return value.contains(RegExp(r'[a-z]'));
}
}
class DigitValidationRule extends ValidationRule {
@override
String get name => translate('digit');
@override
bool validate(String value) {
return value.contains(RegExp(r'[0-9]'));
}
}
class SpecialCharacterValidationRule extends ValidationRule {
@override
String get name => translate('special character');
@override
bool validate(String value) {
return value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
}
}
class MinCharactersValidationRule extends ValidationRule {
final int _numberOfCharacters;
MinCharactersValidationRule(this._numberOfCharacters);
@override
String get name => translate('length>=$_numberOfCharacters');
@override
bool validate(String value) {
return value.length >= _numberOfCharacters;
}
}
class PasswordStrengthIndicator extends StatelessWidget {
final RxString password;
final double weakMedium = 0.33;
final double mediumStrong = 0.67;
const PasswordStrengthIndicator({Key? key, required this.password})
: super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
var strength = estimatePasswordStrength(password.value);
return Row(
children: [
Expanded(
child: _indicator(
password.isEmpty ? Colors.grey : _getColor(strength))),
Expanded(
child: _indicator(password.isEmpty || strength < weakMedium
? Colors.grey
: _getColor(strength))),
Expanded(
child: _indicator(password.isEmpty || strength < mediumStrong
? Colors.grey
: _getColor(strength))),
Text(password.isEmpty ? '' : translate(_getLabel(strength)))
.marginOnly(left: password.isEmpty ? 0 : 8),
],
);
});
}
Widget _indicator(Color color) {
return Container(
height: 8,
color: color,
);
}
String _getLabel(double strength) {
if (strength < weakMedium) {
return 'Weak';
} else if (strength < mediumStrong) {
return 'Medium';
} else {
return 'Strong';
}
}
Color _getColor(double strength) {
if (strength < weakMedium) {
return Colors.yellow;
} else if (strength < mediumStrong) {
return Colors.blue;
} else {
return Colors.green;
}
}
}

View File

@ -64,8 +64,8 @@ void changeIdDialog() {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@ -111,48 +111,46 @@ void changeWhiteList({Function()? callback}) async {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
await bind.mainSetOption(key: 'whitelist', value: '');
callback?.call();
close();
},
child: Text(translate("Clear"))),
TextButton(
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips =
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(key: 'whitelist', value: '');
callback?.call();
close();
}, isOutline: true),
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
newWhiteList = ips.join(',');
}
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
callback?.call();
close();
},
child: Text(translate("OK"))),
newWhiteList = ips.join(',');
}
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
callback?.call();
close();
},
),
],
onCancel: close,
);
@ -195,14 +193,12 @@ Future<String> changeDirectAccessPort(
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
await bind.mainSetOption(
key: 'direct-access-port', value: controller.text);
close();
},
child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async {
await bind.mainSetOption(
key: 'direct-access-port', value: controller.text);
close();
}),
],
onCancel: close,
);

View File

@ -0,0 +1,676 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
class _IconOP extends StatelessWidget {
final String icon;
final double iconWidth;
final EdgeInsets margin;
const _IconOP(
{Key? key,
required this.icon,
required this.iconWidth,
this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: margin,
child: SvgPicture.asset(
'assets/$icon.svg',
width: iconWidth,
),
);
}
}
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
final double iconWidth;
final Color primaryColor;
final double height;
final Function() onTap;
const ButtonOP({
Key? key,
required this.op,
required this.curOP,
required this.iconWidth,
required this.primaryColor,
required this.height,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(children: [
Container(
height: height,
width: 200,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null,
child: Row(
children: [
SizedBox(
width: 30,
child: _IconOP(
icon: op,
iconWidth: iconWidth,
margin: EdgeInsets.only(right: 5),
)),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Center(
child: Text('${translate("Continue with")} $op')))),
],
))),
),
]);
}
}
class ConfigOP {
final String op;
final double iconWidth;
ConfigOP({required this.op, required this.iconWidth});
}
class WidgetOP extends StatefulWidget {
final ConfigOP config;
final RxString curOP;
final Function(String) cbLogin;
const WidgetOP({
Key? key,
required this.config,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _WidgetOPState();
}
}
class _WidgetOPState extends State<WidgetOP> {
Timer? _updateTimer;
String _stateMsg = '';
String _failedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_updateTimer?.cancel();
}
_beginQueryState() {
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_updateState();
});
}
_updateState() {
bind.mainAccountAuthResult().then((result) {
if (result.isEmpty) {
return;
}
final resultMap = jsonDecode(result);
if (resultMap == null) {
return;
}
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url));
_url = url;
}
if (authBody != null) {
_updateTimer?.cancel();
final String username = authBody['user']['name'];
widget.curOP.value = '';
widget.cbLogin(username);
}
setState(() {
_stateMsg = stateMsg;
_failedMsg = failedMsg;
if (failedMsg.isNotEmpty) {
widget.curOP.value = '';
_updateTimer?.cancel();
}
});
}
});
}
_resetState() {
_stateMsg = '';
_failedMsg = '';
_url = '';
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
iconWidth: widget.config.iconWidth,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
_resetState();
widget.curOP.value = widget.config.op;
await bind.mainAccountAuth(op: widget.config.op);
_beginQueryState();
},
),
Obx(() {
if (widget.curOP.isNotEmpty &&
widget.curOP.value != widget.config.op) {
_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,
),
),
],
));
}),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: const SizedBox(
height: 5.0,
),
),
),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 20),
child: ElevatedButton(
onPressed: () {
widget.curOP.value = '';
_updateTimer?.cancel();
_resetState();
bind.mainAccountAuthCancel();
},
child: Text(
translate('Cancel'),
style: TextStyle(fontSize: 15),
),
),
),
),
),
],
);
}
}
class LoginWidgetOP extends StatelessWidget {
final List<ConfigOP> ops;
final RxString curOP;
final Function(String) cbLogin;
LoginWidgetOP({
Key? key,
required this.ops,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var children = ops
.map((op) => [
WidgetOP(
config: op,
curOP: curOP,
cbLogin: cbLogin,
),
const Divider(
indent: 5,
endIndent: 5,
)
])
.expand((i) => i)
.toList();
if (children.isNotEmpty) {
children.removeLast();
}
return SingleChildScrollView(
child: Container(
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
)));
}
}
class LoginWidgetUserPass extends StatelessWidget {
final TextEditingController username;
final TextEditingController pass;
final String? usernameMsg;
final String? passMsg;
final bool isInProgress;
final RxString curOP;
final RxBool autoLogin;
final Function() onLogin;
final FocusNode? userFocusNode;
const LoginWidgetUserPass({
Key? key,
this.userFocusNode,
required this.username,
required this.pass,
required this.usernameMsg,
required this.passMsg,
required this.isInProgress,
required this.curOP,
required this.autoLogin,
required this.onLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8.0),
DialogTextField(
title: '${translate("Username")}:',
controller: username,
focusNode: userFocusNode,
prefixIcon: Icon(Icons.account_circle_outlined),
errorText: usernameMsg),
DialogTextField(
title: '${translate("Password")}:',
obscureText: true,
controller: pass,
prefixIcon: Icon(Icons.lock_outline),
errorText: passMsg),
Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate("Remember me"),
),
value: autoLogin.value,
onChanged: (v) {
if (v == null) return;
autoLogin.value = v;
},
)),
Offstage(
offstage: !isInProgress,
child: const LinearProgressIndicator()),
const SizedBox(height: 12.0),
FittedBox(
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
height: 38,
width: 200,
child: Obx(() => ElevatedButton(
child: Text(
translate('Login'),
style: TextStyle(fontSize: 16),
),
onPressed:
curOP.value.isEmpty || curOP.value == 'rustdesk'
? () {
onLogin();
}
: null,
)),
),
])),
],
));
}
}
class DialogTextField extends StatelessWidget {
final String title;
final bool obscureText;
final String? errorText;
final String? helperText;
final Widget? prefixIcon;
final TextEditingController controller;
final FocusNode? focusNode;
DialogTextField(
{Key? key,
this.focusNode,
this.obscureText = false,
this.errorText,
this.helperText,
this.prefixIcon,
required this.title,
required this.controller})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: title,
border: const OutlineInputBorder(),
prefixIcon: prefixIcon,
helperText: helperText,
helperMaxLines: 8,
errorText: errorText),
controller: controller,
focusNode: focusNode,
autofocus: true,
obscureText: obscureText,
),
),
],
).paddingSymmetric(vertical: 4.0);
}
}
/// common login dialog for desktop
/// call this directly
Future<bool?> loginDialog() async {
var username = TextEditingController();
var password = TextEditingController();
final userFocusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus());
String? usernameMsg;
String? passwordMsg;
var isInProgress = false;
final autoLogin = true.obs;
final RxString curOP = ''.obs;
final res = await gFFI.dialogManager.show<bool>((setState, close) {
username.addListener(() {
if (usernameMsg != null) {
setState(() => usernameMsg = null);
}
});
password.addListener(() {
if (passwordMsg != null) {
setState(() => passwordMsg = null);
}
});
onDialogCancel() {
isInProgress = false;
close(false);
}
onLogin() async {
// validate
if (username.text.isEmpty) {
setState(() => usernameMsg = translate('Username missed'));
return;
}
if (password.text.isEmpty) {
setState(() => passwordMsg = translate('Password missed'));
return;
}
curOP.value = 'rustdesk';
setState(() => isInProgress = true);
try {
final resp = await gFFI.userModel.login(LoginRequest(
username: username.text,
password: password.text,
id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(),
autoLogin: autoLogin.value,
type: HttpType.kAuthReqTypeAccount));
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
close(true);
return;
}
break;
case HttpType.kAuthResTypeEmailCheck:
setState(() => isInProgress = false);
final res = await verificationCodeDialog(resp.user);
if (res == true) {
close(true);
return;
}
break;
default:
passwordMsg = "Failed, bad response from server";
break;
}
} on RequestException catch (err) {
passwordMsg = translate(err.cause);
debugPrintStack(label: err.toString());
} catch (err) {
passwordMsg = "Unknown Error: $err";
debugPrintStack(label: err.toString());
}
curOP.value = '';
setState(() => isInProgress = false);
}
return CustomAlertDialog(
title: Text(translate('Login')),
contentBoxConstraints: BoxConstraints(minWidth: 400),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 8.0,
),
LoginWidgetUserPass(
username: username,
pass: password,
usernameMsg: usernameMsg,
passMsg: passwordMsg,
isInProgress: isInProgress,
curOP: curOP,
autoLogin: autoLogin,
onLogin: onLogin,
userFocusNode: userFocusNode,
),
const SizedBox(
height: 8.0,
),
Center(
child: Text(
translate('or'),
style: TextStyle(fontSize: 16),
)),
const SizedBox(
height: 8.0,
),
LoginWidgetOP(
ops: [
ConfigOP(op: 'GitHub', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
curOP: curOP,
cbLogin: (String username) {
gFFI.userModel.userName.value = username;
close(true);
},
),
],
),
actions: [dialogButton('Close', onPressed: onDialogCancel)],
onCancel: onDialogCancel,
);
});
if (res != null) {
// update ab and group status
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
}
return res;
}
Future<bool?> verificationCodeDialog(UserPayload? user) async {
var autoLogin = true;
var isInProgress = false;
String? errorText;
final code = TextEditingController();
final focusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
final res = await gFFI.dialogManager.show<bool>((setState, close) {
bool validate() {
return code.text.length >= 6;
}
code.addListener(() {
if (errorText != null) {
setState(() => errorText = null);
}
});
void onVerify() async {
if (!validate()) {
setState(
() => errorText = translate('Too short, at least 6 characters.'));
return;
}
setState(() => isInProgress = true);
try {
final resp = await gFFI.userModel.login(LoginRequest(
verificationCode: code.text,
username: user?.name,
id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(),
autoLogin: autoLogin,
type: HttpType.kAuthReqTypeEmailCode));
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
close(true);
return;
}
break;
default:
errorText = "Failed, bad response from server";
break;
}
} on RequestException catch (err) {
errorText = translate(err.cause);
debugPrintStack(label: err.toString());
} catch (err) {
errorText = "Unknown Error: $err";
debugPrintStack(label: err.toString());
}
setState(() => isInProgress = false);
}
return CustomAlertDialog(
title: Text(translate("Verification code")),
contentBoxConstraints: BoxConstraints(maxWidth: 300),
content: Column(
children: [
Offstage(
offstage: user?.email == null,
child: TextField(
decoration: InputDecoration(
labelText: "Email",
prefixIcon: Icon(Icons.email),
border: InputBorder.none),
readOnly: true,
controller: TextEditingController(text: user?.email),
)),
const SizedBox(height: 8),
DialogTextField(
title: '${translate("Verification code")}:',
controller: code,
errorText: errorText,
focusNode: focusNode,
helperText: translate('verification_tip'),
),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Row(children: [
Expanded(child: Text(translate("Trust this device")))
]),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = !autoLogin);
},
),
Offstage(
offstage: !isInProgress,
child: const LinearProgressIndicator()),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("Verify", onPressed: onVerify),
]);
});
return res;
}

View File

@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:get/get.dart';
import '../../common.dart';
class MyGroup extends StatefulWidget {
final EdgeInsets? menuPadding;
const MyGroup({Key? key, this.menuPadding}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _MyGroupState();
}
}
class _MyGroupState extends State<MyGroup> {
static final RxString selectedUser = ''.obs;
static final RxString searchUserText = ''.obs;
static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) => FutureBuilder<Widget>(
future: buildBody(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return const Offstage();
}
});
Future<Widget> buildBody(BuildContext context) async {
return Obx(() {
if (gFFI.groupModel.userLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (gFFI.groupModel.userLoadError.isNotEmpty) {
return _buildShowError(gFFI.groupModel.userLoadError.value);
}
if (isDesktop) {
return _buildDesktop();
} else {
return _buildMobile();
}
});
}
Widget _buildShowError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(error)),
TextButton(
onPressed: () {
gFFI.groupModel.pull();
},
child: Text(translate("Retry")))
],
));
}
Widget _buildDesktop() {
return Obx(
() => Row(
children: [
Card(
margin: EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(context).scaffoldBackgroundColor)),
child: Container(
width: 200,
height: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
children: [
_buildLeftHeader(),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(2)),
child: _buildUserContacts(),
).marginSymmetric(vertical: 8.0),
)
],
),
),
).marginOnly(right: 8.0),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peersShow.value)),
)
],
),
);
}
Widget _buildMobile() {
return Obx(
() => Column(
children: [
Card(
margin: EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(context).scaffoldBackgroundColor)),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildLeftHeader(),
Container(
width: double.infinity,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(4)),
child: _buildUserContacts(),
).marginSymmetric(vertical: 8.0)
],
),
),
),
Divider(),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peersShow.value)),
)
],
),
);
}
Widget _buildLeftHeader() {
return Row(
children: [
Expanded(
child: TextField(
controller: searchUserController,
onChanged: (value) {
searchUserText.value = value;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
hintText: translate("Search"),
hintStyle:
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
border: InputBorder.none,
isDense: true,
),
)),
],
);
}
Widget _buildUserContacts() {
return Obx(() {
return Column(
children: gFFI.groupModel.users
.where((p0) {
if (searchUserText.isNotEmpty) {
return p0.name.contains(searchUserText.value);
}
return true;
})
.map((e) => _buildUserItem(e.name))
.toList());
});
}
Widget _buildUserItem(String username) {
return InkWell(onTap: () {
if (selectedUser.value != username) {
selectedUser.value = username;
gFFI.groupModel.pullUserPeers(username);
}
}, child: Obx(
() {
bool selected = selectedUser.value == username;
return Container(
decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null,
border: Border(
bottom: BorderSide(
width: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1))),
),
child: Container(
child: Row(
children: [
Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16)
.marginOnly(right: 4),
Expanded(child: Text(username)),
],
).paddingSymmetric(vertical: 4),
),
);
},
)).marginSymmetric(horizontal: 12);
}
}

View File

@ -56,6 +56,9 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildMobile() {
final peer = super.widget.peer;
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
return Card(
margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector(
@ -90,7 +93,7 @@ class _PeerCardState extends State<_PeerCard>
? formatID(peer.id)
: peer.alias)
]),
Text('${peer.username}@${peer.hostname}')
Text(name)
],
).paddingOnly(left: 8.0),
),
@ -145,6 +148,8 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
@ -184,7 +189,7 @@ class _PeerCardState extends State<_PeerCard>
Align(
alignment: Alignment.centerLeft,
child: Text(
'${peer.username}@${peer.hostname}',
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
@ -206,7 +211,8 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerCard(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final name = '${peer.username}@${peer.hostname}';
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
return Card(
color: Colors.transparent,
elevation: 0,
@ -448,7 +454,7 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
/// Only avaliable on Windows.
/// Only available on Windows.
@protected
MenuEntryBase<String> _createShortCutAction(String id) {
return MenuEntryButton<String>(
@ -464,6 +470,12 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
@protected
Future<bool> _isForceAlwaysRelay(String id) async {
return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay'))
.isNotEmpty;
}
@protected
Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
const option = 'force-always-relay';
@ -471,17 +483,12 @@ abstract class BasePeerCard extends StatelessWidget {
switchType: SwitchType.scheckbox,
text: translate('Always connect via relay'),
getter: () async {
return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty;
return await _isForceAlwaysRelay(id);
},
setter: (bool v) async {
String value;
String oldValue = await bind.mainGetPeerOption(id: id, key: option);
if (oldValue.isEmpty) {
value = 'Y';
} else {
value = '';
}
await bind.mainSetPeerOption(id: id, key: option, value: value);
gFFI.abModel.setPeerForceAlwaysRelay(id, v);
await bind.mainSetPeerOption(
id: id, key: option, value: bool2option(option, v));
},
padding: menuPadding,
dismissOnClicked: true,
@ -489,14 +496,14 @@ abstract class BasePeerCard extends StatelessWidget {
}
@protected
MenuEntryBase<String> _renameAction(String id, bool isAddressBook) {
MenuEntryBase<String> _renameAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Rename'),
style: style,
),
proc: () {
_rename(id, isAddressBook);
_rename(id);
},
padding: menuPadding,
dismissOnClicked: true,
@ -586,33 +593,41 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
void _rename(String id, bool isAddressBook) async {
@protected
MenuEntryBase<String> _addToAb(Peer peer) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Add to Address Book'),
style: style,
),
proc: () {
() async {
if (!gFFI.abModel.idContainBy(peer.id)) {
gFFI.abModel.addPeer(peer);
await gFFI.abModel.pushAb();
}
}();
},
padding: menuPadding,
dismissOnClicked: true,
);
}
@protected
Future<String> _getAlias(String id) async =>
await bind.mainGetPeerOption(id: id, key: 'alias');
void _rename(String id) async {
RxBool isInProgress = false.obs;
var name = peer.alias;
String name = await _getAlias(id);
var controller = TextEditingController(text: name);
if (isAddressBook) {
final peer = gFFI.abModel.peers.firstWhereOrNull((p) => id == p.id);
if (peer == null) {
// this should not happen
} else {
name = peer.alias;
}
}
gFFI.dialogManager.show((setState, close) {
submit() async {
isInProgress.value = true;
name = controller.text;
String name = controller.text.trim();
await bind.mainSetPeerAlias(id: id, alias: name);
if (isAddressBook) {
gFFI.abModel.setPeerAlias(id, name);
await gFFI.abModel.pushAb();
}
if (isAddressBook) {
gFFI.abModel.pullAb();
} else {
bind.mainLoadRecentPeers();
bind.mainLoadFavPeers();
}
gFFI.abModel.setPeerAlias(id, name);
_update();
close();
isInProgress.value = false;
}
@ -638,14 +653,17 @@ abstract class BasePeerCard extends StatelessWidget {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
@protected
void _update();
}
class RecentPeerCard extends BasePeerCard {
@ -671,7 +689,7 @@ class RecentPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_renameAction(peer.id));
menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadRecentPeers();
}));
@ -679,8 +697,15 @@ class RecentPeerCard extends BasePeerCard {
menuItems.add(_unrememberPasswordAction(peer.id));
}
menuItems.add(_addFavAction(peer.id));
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
return menuItems;
}
@protected
@override
void _update() => bind.mainLoadRecentPeers();
}
class FavoritePeerCard extends BasePeerCard {
@ -706,7 +731,7 @@ class FavoritePeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_renameAction(peer.id));
menuItems.add(_removeAction(peer.id, () async {
await bind.mainLoadFavPeers();
}));
@ -716,8 +741,15 @@ class FavoritePeerCard extends BasePeerCard {
menuItems.add(_rmFavAction(peer.id, () async {
await bind.mainLoadFavPeers();
}));
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
return menuItems;
}
@protected
@override
void _update() => bind.mainLoadFavPeers();
}
class DiscoveredPeerCard extends BasePeerCard {
@ -744,8 +776,15 @@ class DiscoveredPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_removeAction(peer.id, () async {}));
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
return menuItems;
}
@protected
@override
void _update() => bind.mainLoadLanPeers();
}
class AddressBookPeerCard extends BasePeerCard {
@ -771,7 +810,7 @@ class AddressBookPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id, false));
menuItems.add(_renameAction(peer.id));
menuItems.add(_removeAction(peer.id, () async {}));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
@ -782,6 +821,20 @@ class AddressBookPeerCard extends BasePeerCard {
return menuItems;
}
@protected
@override
Future<bool> _isForceAlwaysRelay(String id) async =>
gFFI.abModel.find(id)?.forceAlwaysRelay ?? false;
@protected
@override
Future<String> _getAlias(String id) async =>
gFFI.abModel.find(id)?.alias ?? '';
@protected
@override
void _update() => gFFI.abModel.pullAb();
@protected
@override
MenuEntryBase<String> _removeAction(
@ -862,8 +915,8 @@ class AddressBookPeerCard extends BasePeerCard {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@ -872,23 +925,61 @@ class AddressBookPeerCard extends BasePeerCard {
}
}
class MyGroupPeerCard extends BasePeerCard {
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
: super(peer: peer, menuPadding: menuPadding, key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
return menuItems;
}
@protected
@override
void _update() => gFFI.groupModel.pull();
}
void _rdpDialog(String id) async {
final portController = TextEditingController(
text: await bind.mainGetPeerOption(id: id, key: 'rdp_port'));
final userController = TextEditingController(
text: await bind.mainGetPeerOption(id: id, key: 'rdp_username'));
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port);
final userController = TextEditingController(text: username);
final passwordController = TextEditingController(
text: await bind.mainGetPeerOption(id: id, key: 'rdp_password'));
RxBool secure = true.obs;
gFFI.dialogManager.show((setState, close) {
submit() async {
String port = portController.text.trim();
String username = userController.text;
String password = passwordController.text;
await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port);
await bind.mainSetPeerOption(
id: id, key: 'rdp_port', value: portController.text.trim());
id: id, key: 'rdp_username', value: username);
await bind.mainSetPeerOption(
id: id, key: 'rdp_username', value: userController.text);
await bind.mainSetPeerOption(
id: id, key: 'rdp_password', value: passwordController.text);
id: id, key: 'rdp_password', value: password);
gFFI.abModel.setRdp(id, port, username);
close();
}
@ -981,8 +1072,8 @@ void _rdpDialog(String id) async {
),
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,

View File

@ -1,36 +1,165 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
const int groupTabIndex = 4;
const String defaultGroupTabname = 'Group';
class StatePeerTab {
final RxInt currentTab = 0.obs;
final RxInt tabHiddenFlag = 0.obs;
final RxList<String> tabNames = [
'Recent Sessions',
'Favorites',
'Discovered',
'Address Book',
defaultGroupTabname,
].obs;
StatePeerTab._() {
tabHiddenFlag.value = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0);
var tabs = _notHiddenTabs();
currentTab.value =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabs.contains(currentTab.value)) {
currentTab.value = 0;
}
}
static final StatePeerTab instance = StatePeerTab._();
check() {
var tabs = _notHiddenTabs();
if (filterGroupCard()) {
if (currentTab.value == groupTabIndex) {
currentTab.value =
tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: currentTab.value.toString());
}
} else {
if (gFFI.userModel.isAdmin.isFalse &&
gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = defaultGroupTabname;
}
if (tabs.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
currentTab.value = groupTabIndex;
}
}
}
List<int> currentTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i) && !_isTabFilter(i)) {
v.add(i);
}
}
return v;
}
bool filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
bool _isTabHidden(int tabindex) {
return tabHiddenFlag & (1 << tabindex) != 0;
}
bool _isTabFilter(int tabIndex) {
if (tabIndex == groupTabIndex) {
return filterGroupCard();
}
return false;
}
List<int> _notHiddenTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i)) {
v.add(i);
}
}
return v;
}
}
final statePeerTab = StatePeerTab.instance;
class PeerTabPage extends StatefulWidget {
final List<String> tabs;
final List<Widget> children;
const PeerTabPage({required this.tabs, required this.children, Key? key})
: super(key: key);
const PeerTabPage({Key? key}) : super(key: key);
@override
State<PeerTabPage> createState() => _PeerTabPageState();
}
class _TabEntry {
final Widget widget;
final Function() load;
_TabEntry(this.widget, this.load);
}
EdgeInsets? _menuPadding() {
return isDesktop ? kDesktopMenuPadding : null;
}
class _PeerTabPageState extends State<PeerTabPage>
with SingleTickerProviderStateMixin {
final RxInt _tabIndex = 0.obs;
final List<_TabEntry> entries = [
_TabEntry(
RecentPeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadRecentPeers),
_TabEntry(
FavoritePeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadFavPeers),
_TabEntry(
DiscoveredPeersView(
menuPadding: _menuPadding(),
),
bind.mainDiscover),
_TabEntry(
AddressBook(
menuPadding: _menuPadding(),
),
() => {}),
_TabEntry(
MyGroup(
menuPadding: _menuPadding(),
),
() => {}),
];
@override
void initState() {
setPeer();
super.initState();
}
setPeer() {
final index = bind.getLocalFlutterConfig(k: 'peer-tab-index');
if (index != '') {
_tabIndex.value = int.parse(index);
}
adjustTab();
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
if (uiType != '') {
@ -38,26 +167,13 @@ class _PeerTabPageState extends State<PeerTabPage>
? PeerUiType.list
: PeerUiType.grid;
}
super.initState();
}
// hard code for now
Future<void> _handleTabSelection(int index) async {
_tabIndex.value = index;
await bind.setLocalFlutterConfig(k: 'peer-tab-index', v: index.toString());
switch (index) {
case 0:
bind.mainLoadRecentPeers();
break;
case 1:
bind.mainLoadFavPeers();
break;
case 2:
bind.mainDiscover();
break;
case 3:
/// AddressBook initState will refresh ab state
break;
Future<void> handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
statePeerTab.currentTab.value = tabIndex;
entries[tabIndex].load();
}
}
@ -80,8 +196,9 @@ class _PeerTabPageState extends State<PeerTabPage>
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: _createSwitchBar(context)),
const SizedBox(width: 10),
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
@ -97,45 +214,81 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: ScrollController(),
children: super.widget.tabs.asMap().entries.map((t) {
return Obx(() => InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: _tabIndex.value == t.key
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
return Obx(() {
var tabs = statePeerTab.currentTabs();
return ListView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
controller: ScrollController(),
children: tabs.map((t) {
return InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: statePeerTab.currentTab.value == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
child: Align(
alignment: Alignment.center,
child: Text(
translatedTabname(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: statePeerTab.currentTab.value == t
? textColor
: textColor
?..withOpacity(0.5)),
),
child: Align(
alignment: Alignment.center,
child: Text(
t.value,
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color:
_tabIndex.value == t.key ? textColor : textColor
?..withOpacity(0.5)),
),
)),
onTap: () async => await _handleTabSelection(t.key),
));
}).toList());
)),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
},
);
}).toList());
});
}
translatedTabname(int index) {
if (index < statePeerTab.tabNames.length) {
final name = statePeerTab.tabNames[index];
if (index == groupTabIndex) {
if (name == defaultGroupTabname) {
return translate(name);
} else {
return name;
}
} else {
return translate(name);
}
}
assert(false);
return index.toString();
}
Widget _createPeersView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
return Expanded(
child: Obx(() => widget
.children[_tabIndex.value]) //: (to) => _tabIndex.value = to)
.marginSymmetric(vertical: verticalMargin),
);
child: Obx(() {
var tabs = statePeerTab.currentTabs();
if (tabs.isEmpty) {
return visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
} else {
if (tabs.contains(statePeerTab.currentTab.value)) {
return entries[statePeerTab.currentTab.value].widget;
} else {
statePeerTab.currentTab.value = tabs[0];
return entries[statePeerTab.currentTab.value].widget;
}
}
}).marginSymmetric(vertical: verticalMargin));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
@ -167,6 +320,72 @@ class _PeerTabPageState extends State<PeerTabPage>
.toList(),
);
}
adjustTab() {
var tabs = statePeerTab.currentTabs();
if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
statePeerTab.currentTab.value = tabs[0];
}
}
Widget visibleContextMenuListener(Widget child) {
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
if (e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return visibleContextMenu(cancelFunc);
},
target: e.position,
);
}
},
child: child);
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
return Obx(() {
final List<MenuEntryBase> menu = List.empty(growable: true);
for (int i = 0; i < statePeerTab.tabNames.length; i++) {
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
continue;
}
int bitMask = 1 << i;
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translatedTabname(i),
getter: () async {
return statePeerTab.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
if (show) {
statePeerTab.tabHiddenFlag.value &= ~bitMask;
} else {
statePeerTab.tabHiddenFlag.value |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card',
v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
cancelFunc();
adjustTab();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
});
}
}
class PeerSearchBar extends StatefulWidget {

View File

@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView {
return true;
}
}
class MyGroupPeerView extends BasePeersView {
MyGroupPeerView(
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required List<Peer> initPeers})
: super(
key: key,
name: 'my group peer',
loadEvent: 'load_my_group_peers',
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer,
menuPadding: menuPadding,
),
initPeers: initPeers,
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../models/input_model.dart';
@ -9,11 +10,12 @@ class RawKeyFocusScope extends StatelessWidget {
final InputModel inputModel;
final Widget child;
RawKeyFocusScope(
{this.focusNode,
this.onFocusChange,
required this.inputModel,
required this.child});
RawKeyFocusScope({
this.focusNode,
this.onFocusChange,
required this.inputModel,
required this.child,
});
@override
Widget build(BuildContext context) {
@ -24,7 +26,8 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true,
focusNode: focusNode,
onFocusChange: onFocusChange,
onKey: inputModel.handleRawKeyEvent,
onKey:
stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null,
child: child));
}
}
@ -35,11 +38,15 @@ class RawPointerMouseRegion extends StatelessWidget {
final MouseCursor? cursor;
final PointerEnterEventListener? onEnter;
final PointerExitEventListener? onExit;
final PointerDownEventListener? onPointerDown;
final PointerUpEventListener? onPointerUp;
RawPointerMouseRegion(
{this.onEnter,
this.onExit,
this.cursor,
this.onPointerDown,
this.onPointerUp,
required this.inputModel,
required this.child});
@ -47,8 +54,14 @@ class RawPointerMouseRegion extends StatelessWidget {
Widget build(BuildContext context) {
return Listener(
onPointerHover: inputModel.onPointHoverImage,
onPointerDown: inputModel.onPointDownImage,
onPointerUp: inputModel.onPointUpImage,
onPointerDown: (evt) {
onPointerDown?.call(evt);
inputModel.onPointDownImage(evt);
},
onPointerUp: (evt) {
onPointerUp?.call(evt);
inputModel.onPointUpImage(evt);
},
onPointerMove: inputModel.onPointMoveImage,
onPointerSignal: inputModel.onPointerSignalImage,
/*

View File

@ -5,15 +5,23 @@ import 'package:flutter_hbb/common.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
const String kAppTypeMain = "main";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide";
const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kUniLinksPrefix = "rustdesk://";
const String kActionNewConnection = "connection/new/";
@ -97,6 +105,8 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom';
const kIgnoreDpi = true;
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
/// see [LogicalKeyboardKey.keyLabel]
const Map<int, String> logicalKeyMap = <int, String>{

View File

@ -6,7 +6,6 @@ import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:get/get.dart';
@ -16,7 +15,6 @@ import 'package:window_manager/window_manager.dart';
import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../models/platform_model.dart';
import '../widgets/button.dart';
@ -46,7 +44,7 @@ class _ConnectionPageState extends State<ConnectionPage>
var svcStatusCode = 0.obs;
var svcIsUsingPublicServer = true.obs;
bool isWindowMinisized = false;
bool isWindowMinimized = false;
@override
void initState() {
@ -82,13 +80,13 @@ class _ConnectionPageState extends State<ConnectionPage>
void onWindowEvent(String eventName) {
super.onWindowEvent(eventName);
if (eventName == 'minimize') {
isWindowMinisized = true;
isWindowMinimized = true;
} else if (eventName == 'maximize' || eventName == 'restore') {
if (isWindowMinisized && Platform.isWindows) {
// windows can't update when minisized.
if (isWindowMinimized && Platform.isWindows) {
// windows can't update when minimized.
Get.forceAppUpdate();
}
isWindowMinisized = false;
isWindowMinimized = false;
}
}
@ -113,7 +111,7 @@ class _ConnectionPageState extends State<ConnectionPage>
delegate: SliverChildListDelegate([
Row(
children: [
_buildRemoteIDTextField(context),
Flexible(child: _buildRemoteIDTextField(context)),
],
).marginOnly(top: 22),
SizedBox(height: 12),
@ -121,28 +119,7 @@ class _ConnectionPageState extends State<ConnectionPage>
])),
SliverFillRemaining(
hasScrollBody: false,
child: PeerTabPage(
tabs: [
translate('Recent Sessions'),
translate('Favorites'),
translate('Discovered'),
translate('Address Book')
],
children: [
RecentPeersView(
menuPadding: kDesktopMenuPadding,
),
FavoritePeersView(
menuPadding: kDesktopMenuPadding,
),
DiscoveredPeersView(
menuPadding: kDesktopMenuPadding,
),
const AddressBook(
menuPadding: kDesktopMenuPadding,
),
],
).paddingOnly(right: 12.0),
child: PeerTabPage().paddingOnly(right: 12.0),
)
],
).paddingOnly(left: 12.0),
@ -193,6 +170,7 @@ class _ConnectionPageState extends State<ConnectionPage>
Expanded(
child: Obx(
() => TextField(
maxLength: 90,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
@ -200,12 +178,13 @@ class _ConnectionPageState extends State<ConnectionPage>
style: const TextStyle(
fontFamily: 'WorkSans',
fontSize: 22,
height: 1,
height: 1.25,
),
maxLines: 1,
cursorColor:
Theme.of(context).textTheme.titleLarge?.color,
decoration: InputDecoration(
counterText: '',
hintText: _idInputFocused.value
? null
: translate('Enter Remote ID'),
@ -258,9 +237,8 @@ class _ConnectionPageState extends State<ConnectionPage>
),
),
);
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 600), child: w));
return Container(
constraints: const BoxConstraints(maxWidth: 600), child: w);
}
Widget buildStatus() {

View File

@ -6,6 +6,7 @@ import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart' hide MenuItem;
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/custom_password.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
@ -14,11 +15,8 @@ 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/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/tray_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_size/window_size.dart' as window_size;
@ -34,7 +32,7 @@ class DesktopHomePage extends StatefulWidget {
const borderColor = Color(0xFF2F65BA);
class _DesktopHomePageState extends State<DesktopHomePage>
with TrayListener, AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin {
final _leftPaneScrollController = ScrollController();
@override
@ -45,6 +43,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var svcStopped = false.obs;
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
Timer? _updateTimer;
@override
@ -304,15 +303,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
Widget buildHelpCards() {
if (Platform.isWindows) {
if (!bind.mainIsInstalled()) {
return buildInstallCard(
"", "install_tip", "Install", bind.mainGotoInstall);
} else if (bind.mainIsInstalledLowerVersion()) {
return buildInstallCard("Status", "Your installation is lower version.",
"Click to upgrade", bind.mainUpdateMe);
}
}
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@ -325,7 +315,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});
}
if (Platform.isMacOS) {
if (Platform.isWindows) {
if (!bind.mainIsInstalled()) {
return buildInstallCard(
"", "install_tip", "Install", bind.mainGotoInstall);
} else if (bind.mainIsInstalledLowerVersion()) {
return buildInstallCard("Status", "Your installation is lower version.",
"Click to upgrade", bind.mainUpdateMe);
}
} else if (Platform.isMacOS) {
if (!bind.mainIsCanScreenRecording(prompt: false)) {
return buildInstallCard("Permissions", "config_screen", "Configure",
() async {
@ -338,6 +336,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsProcessTrusted(prompt: true);
watchIsProcessTrust = true;
}, help: 'Help', link: translate("doc_mac_permission"));
} else if (!bind.mainIsCanInputMonitoring(prompt: false)) {
return buildInstallCard("Permissions", "config_input", "Configure",
() async {
bind.mainIsCanInputMonitoring(prompt: true);
watchIsInputMonitoring = true;
}, help: 'Help', link: translate("doc_mac_permission"));
} else if (!svcStopped.value &&
bind.mainIsInstalled() &&
!bind.mainIsInstalledDaemon(prompt: false)) {
@ -345,8 +349,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsInstalledDaemon(prompt: true);
});
}
} else if (Platform.isLinux) {
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
"Warning", translate("wayland_experiment_tip"), "", () async {},
help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required');
} else if (bind.mainIsLoginWayland()) {
return buildInstallCard("Warning",
"Login screen using Wayland is not supported", "", () async {},
help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen');
}
}
if (bind.mainIsInstalledLowerVersion()) {}
return Container();
}
@ -428,39 +443,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
@override
void onTrayIconMouseDown() {
windowManager.show();
}
@override
void onTrayIconRightMouseDown() {
// linux does not support popup menu manually.
// linux will handle popup action ifself.
if (Platform.isMacOS || Platform.isWindows) {
trayManager.popUpContextMenu();
}
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case kTrayItemQuitKey:
windowManager.close();
break;
case kTrayItemShowKey:
windowManager.show();
windowManager.focus();
break;
default:
break;
}
}
@override
void initState() {
super.initState();
bind.mainStartGrabKeyboard();
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID();
final url = await bind.mainGetSoftwareUpdateUrl();
@ -490,19 +475,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
setState(() {});
}
}
if (watchIsInputMonitoring) {
if (bind.mainIsCanInputMonitoring(prompt: false)) {
watchIsInputMonitoring = false;
setState(() {});
}
}
});
Get.put<RxBool>(svcStopped, tag: 'stop-service');
// disable this tray because we use tray function provided by rust now
// initTray();
trayManager.addListener(this);
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == "main_window_on_top") {
if (call.method == kWindowMainWindowOnTop) {
window_on_top(null);
} else if (call.method == "get_window_info") {
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return "";
@ -526,9 +514,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventShow) {
rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowEventHide) {
rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowConnect) {
await connectMainDesktop(
call.arguments['id'],
isFileTransfer: call.arguments['isFileTransfer'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
);
}
});
_uniLinksSubscription = listenUniLinks();
@ -536,10 +531,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override
void dispose() {
// destoryTray();
// fix: disable unregister to prevent from receiving events from other windows
// rustDeskWinManager.unregisterActiveWindowListener(onActiveWindowChanged);
trayManager.removeListener(this);
_uniLinksSubscription?.cancel();
Get.delete<RxBool>(tag: 'stop-service');
_updateTimer?.cancel();
@ -553,6 +544,14 @@ void setPasswordDialog() async {
final p1 = TextEditingController(text: pw);
var errMsg0 = "";
var errMsg1 = "";
final RxString rxPass = pw.trim().obs;
final rules = [
DigitValidationRule(),
UppercaseValidationRule(),
LowercaseValidationRule(),
// SpecialCharacterValidationRule(),
MinCharactersValidationRule(8),
];
gFFI.dialogManager.show((setState, close) {
submit() {
@ -561,15 +560,20 @@ void setPasswordDialog() async {
errMsg1 = "";
});
final pass = p0.text.trim();
if (pass.length < 6 && pass.isNotEmpty) {
setState(() {
errMsg0 = translate("Too short, at least 6 characters.");
});
return;
if (pass.isNotEmpty) {
for (var r in rules) {
if (!r.validate(pass)) {
setState(() {
errMsg0 = '${translate('Prompt')}: ${r.name}';
});
return;
}
}
}
if (p1.text.trim() != pass) {
setState(() {
errMsg1 = translate("The confirmation is not identical.");
errMsg1 =
'${translate('Prompt')}: ${translate("The confirmation is not identical.")}';
});
return;
}
@ -589,23 +593,40 @@ void setPasswordDialog() async {
),
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text(
"${translate('Password')}:",
textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
labelText: translate('Password'),
border: const OutlineInputBorder(),
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
controller: p0,
focusNode: FocusNode()..requestFocus(),
onChanged: (value) {
rxPass.value = value.trim();
},
),
),
],
),
Row(
children: [
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
],
).marginSymmetric(vertical: 8),
const SizedBox(
height: 8.0,
),
Row(
children: [
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
controller: p0,
focusNode: FocusNode()..requestFocus(),
labelText: translate('Confirmation'),
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
controller: p1,
),
),
],
@ -613,32 +634,30 @@ void setPasswordDialog() async {
const SizedBox(
height: 8.0,
),
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Confirmation')}:")
.marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
controller: p1,
),
),
],
),
Obx(() => Wrap(
runSpacing: 8,
spacing: 4,
children: rules.map((e) {
var checked = e.validate(rxPass.value.trim());
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(),
))
],
),
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,

View File

@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:get/get.dart';
@ -18,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
const double _kTabWidth = 235;
const double _kTabHeight = 42;
@ -63,7 +63,7 @@ class DesktopSettingPage extends StatefulWidget {
DesktopTabPage.onAddSetting(initialPage: page);
}
} catch (e) {
debugPrint('$e');
debugPrintStack(label: '$e');
}
}
}
@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
scrollController: controller,
child: PageView(
controller: controller,
physics: NeverScrollableScrollPhysics(),
children: const [
_General(),
_Safety(),
@ -273,6 +274,15 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs'),
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
if (Platform.isLinux)
Tooltip(
message: translate('software_render_tip'),
child: _OptionCheckBox(
context,
"Always use software rendering",
'allow-always-software-render',
),
)
]);
}
@ -932,6 +942,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
return false;
}
}
final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
if (old.isNotEmpty && old != idServer) {
await gFFI.userModel.logOut();
}
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
@ -954,23 +968,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
TextEditingController mytext = TextEditingController();
String? aNullableString = '';
aNullableString = value?.text;
mytext.text = aNullableString.toString();
if (mytext.text.isNotEmpty) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
Map<String, dynamic> config = jsonDecode(mytext.text);
if (config.containsKey('IdServer')) {
String id = config['IdServer'] ?? '';
String relay = config['RelayServer'] ?? '';
String api = config['ApiServer'] ?? '';
String key = config['Key'] ?? '';
idController.text = id;
relayController.text = relay;
apiController.text = api;
keyController.text = key;
Future<bool> success = set(id, relay, api, key);
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
idController.text = sc.idServer;
relayController.text = sc.relayServer;
apiController.text = sc.apiServer;
keyController.text = sc.key;
Future<bool> success =
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
success.then((value) {
if (value) {
showToast(
@ -992,12 +1000,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
export() {
Map<String, String> config = {};
config['IdServer'] = idController.text.trim();
config['RelayServer'] = relayController.text.trim();
config['ApiServer'] = apiController.text.trim();
config['Key'] = keyController.text.trim();
Clipboard.setData(ClipboardData(text: jsonEncode(config)));
final text = ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text)
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
@ -1059,21 +1070,13 @@ class _AccountState extends State<_Account> {
}
Widget accountAction() {
return _futureBuilder(future: () async {
return await gFFI.userModel.getUserName();
}(), hasData: (_) {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog().then((success) {
if (success) {
gFFI.abModel.pullAb();
}
})
: gFFI.userModel.logOut()
}));
});
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
: gFFI.userModel.logOut()
}));
}
}
@ -1103,29 +1106,31 @@ class _AboutState extends State<_About> {
child: SingleChildScrollView(
controller: scrollController,
physics: NeverScrollableScrollPhysics(),
child: _Card(title: 'About RustDesk', children: [
child: _Card(title: '${translate('About')} RustDesk', children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
Text('Version: $version').marginSymmetric(vertical: 4.0),
Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0),
Text('${translate('Version')}: $version')
.marginSymmetric(vertical: 4.0),
Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com/privacy');
},
child: const Text(
'Privacy Statement',
child: Text(
translate('Privacy Statement'),
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com');
},
child: const Text(
'Website',
child: Text(
translate('Website'),
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
Container(
@ -1142,8 +1147,8 @@ class _AboutState extends State<_About> {
'Copyright © 2022 Purslane Ltd.\n$license',
style: const TextStyle(color: Colors.white),
),
const Text(
'Made with heart in this chaotic world!',
Text(
translate('Slogan_tip'),
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white),
@ -1227,7 +1232,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
ref.value = option;
if (reverse) option = !option;
String value = bool2option(key, option);
bind.mainSetOption(key: key, value: value);
await bind.mainSetOption(key: key, value: value);
update?.call();
}
}
@ -1431,7 +1436,7 @@ Widget _lock(
_LabeledTextField(
BuildContext context,
String lable,
String label,
TextEditingController controller,
String errorText,
bool enabled,
@ -1442,7 +1447,7 @@ _LabeledTextField(
Expanded(
flex: 4,
child: Text(
'${translate(lable)}:',
'${translate(label)}:',
textAlign: TextAlign.right,
style: TextStyle(color: _disabledTextColor(context, enabled)),
),
@ -1455,6 +1460,8 @@ _LabeledTextField(
enabled: enabled,
obscureText: secure,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 15),
errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle(
color: _disabledTextColor(context, enabled),
@ -1664,8 +1671,8 @@ void changeSocks5Proxy() async {
),
),
actions: [
TextButton(onPressed: close, child: Text(translate('Cancel'))),
TextButton(onPressed: submit, child: Text(translate('OK'))),
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: close,

View File

@ -31,7 +31,7 @@ class DesktopTabPage extends StatefulWidget {
initialPage: initialPage,
)));
} catch (e) {
debugPrint('$e');
debugPrintStack(label: '$e');
}
}
}

View File

@ -7,6 +7,7 @@ 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/tabbar_widget.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:get/get.dart';
@ -32,6 +33,18 @@ enum LocationStatus {
fileSearchBar
}
/// The status of currently focused scope of the mouse
enum MouseFocusScope {
/// Mouse is in local field.
local,
/// Mouse is in remote field.
remote,
/// Mouse is not in local field, remote neither.
none
}
class FileManagerPage extends StatefulWidget {
const FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id;
@ -55,6 +68,11 @@ class _FileManagerPageState extends State<FileManagerPage>
final _searchTextRemote = "".obs;
final _breadCrumbScrollerLocal = ScrollController();
final _breadCrumbScrollerRemote = ScrollController();
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal");
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
final _listSearchBufferLocal = TimeoutStringBuffer();
final _listSearchBufferRemote = TimeoutStringBuffer();
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime =
@ -93,6 +111,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Wakelock.enable();
}
debugPrint("File manager page init success with id ${widget.id}");
model.onDirChanged = breadCrumbScrollToEnd;
// register location listener
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
@ -100,17 +119,18 @@ class _FileManagerPageState extends State<FileManagerPage>
@override
void dispose() {
model.onClose();
_ffi.close();
_ffi.dialogManager.dismissAll();
if (!Platform.isLinux) {
Wakelock.disable();
}
Get.delete<FFI>(tag: 'ft_${widget.id}');
_locationNodeLocal.removeListener(onLocalLocationFocusChanged);
_locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
_locationNodeLocal.dispose();
_locationNodeRemote.dispose();
model.onClose().whenComplete(() {
_ffi.close();
_ffi.dialogManager.dismissAll();
if (!Platform.isLinux) {
Wakelock.disable();
}
Get.delete<FFI>(tag: 'ft_${widget.id}');
_locationNodeLocal.removeListener(onLocalLocationFocusChanged);
_locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
_locationNodeLocal.dispose();
_locationNodeRemote.dispose();
});
super.dispose();
}
@ -195,6 +215,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}
Widget body({bool isLocal = false}) {
final scrollController = ScrollController();
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
margin: const EdgeInsets.all(16.0),
@ -215,8 +236,8 @@ class _FileManagerPageState extends State<FileManagerPage>
children: [
Expanded(
child: SingleChildScrollView(
controller: ScrollController(),
child: _buildDataTable(context, isLocal),
controller: scrollController,
child: _buildDataTable(context, isLocal, scrollController),
),
)
],
@ -226,7 +247,9 @@ class _FileManagerPageState extends State<FileManagerPage>
);
}
Widget _buildDataTable(BuildContext context, bool isLocal) {
Widget _buildDataTable(
BuildContext context, bool isLocal, ScrollController scrollController) {
const rowHeight = 25.0;
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
final sortIndex = (SortBy style) {
@ -244,130 +267,219 @@ class _FileManagerPageState extends State<FileManagerPage>
final sortAscending =
isLocal ? model.localSortAscending : model.remoteSortAscending;
return ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
return DataTable(
key: ValueKey(isLocal ? 0 : 1),
showCheckboxColumn: false,
dataRowHeight: 25,
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,
return MouseRegion(
onEnter: (evt) {
_mouseFocusScope.value =
isLocal ? MouseFocusScope.local : MouseFocusScope.remote;
if (isLocal) {
_keyboardNodeLocal.requestFocus();
} else {
_keyboardNodeRemote.requestFocus();
}
},
onExit: (evt) {
_mouseFocusScope.value = MouseFocusScope.none;
},
child: ListSearchActionListener(
node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote,
buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote,
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) {
final index = entries.indexOf(selectedEntries.items.first);
if (index < 0) {
return;
}
skipCount = index + 1;
}
var searchResult = entries
.skip(skipCount)
.where((element) => element.name.startsWith(buffer));
if (searchResult.isEmpty) {
// cannot find next, lets restart search from head
searchResult =
entries.where((element) => element.name.startsWith(buffer));
}
if (searchResult.isEmpty) {
setState(() {
getSelectedItems(isLocal).clear();
});
return;
}
_jumpToEntry(
isLocal, searchResult.first, scrollController, rowHeight, buffer);
},
onSearch: (buffer) {
debugPrint("searching for $buffer");
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
entries.where((element) => element.name.startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
setState(() {
getSelectedItems(isLocal).clear();
});
return;
}
_jumpToEntry(
isLocal, searchResult.first, scrollController, rowHeight, buffer);
},
child: ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
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))
.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);
?.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);
},
),
DataCell(FittedBox(
child: Tooltip(
// handle double click
if (_checkDoubleClick(entry)) {
openDirectory(entry.path, isLocal: isLocal);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
),
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: lastModifiedStr,
message: sizeStr,
child: Text(
lastModifiedStr,
sizeStr,
overflow: TextOverflow.ellipsis,
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),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
fontSize: 10, color: MyTheme.darkGray),
))),
]);
}).toList(growable: false),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
),
),
);
}
void _jumpToEntry(bool isLocal, Entry entry,
ScrollController scrollController, double rowHeight, String buffer) {
final entries = model.getCurrentDir(isLocal).entries;
final index = entries.indexOf(entry);
if (index == -1) {
debugPrint("entry is not valid: ${entry.path}");
}
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
entries.where((element) => element.name.startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
return;
}
final offset = min(
max(scrollController.position.minScrollExtent,
entries.indexOf(searchResult.first) * rowHeight),
scrollController.position.maxScrollExtent);
scrollController.jumpTo(offset);
setState(() {
selectedEntries.add(isLocal, searchResult.first);
debugPrint("focused on ${searchResult.first.name}");
});
}
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
Entry entry, bool isLocal) {
final isCtrlDown = RawKeyboard.instance.keysPressed
@ -456,7 +568,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Wrap(
children: [
Text(
'${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '),
'${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '),
Text(
'${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '),
Offstage(
@ -487,8 +599,8 @@ class _FileManagerPageState extends State<FileManagerPage>
icon: const Icon(Icons.restart_alt_rounded)),
),
IconButton(
icon: const Icon(Icons.delete_forever_outlined),
splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.close),
splashRadius: 1,
onPressed: () {
model.jobTable.removeAt(index);
model.cancelJob(item.id);
@ -636,7 +748,6 @@ class _FileManagerPageState extends State<FileManagerPage>
}),
IconButton(
onPressed: () {
breadCrumbScrollToEnd(isLocal);
model.refresh(isLocal: isLocal);
},
splashRadius: kDesktopIconButtonSplashRadius,
@ -691,14 +802,9 @@ class _FileManagerPageState extends State<FileManagerPage>
],
),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: cancel,
child: Text(translate("Cancel"))),
ElevatedButton(
style: flatButtonStyle,
onPressed: submit,
child: Text(translate("OK")))
dialogButton("Cancel",
onPressed: cancel, isOutline: true),
dialogButton("OK", onPressed: submit)
],
onSubmit: submit,
onCancel: cancel,
@ -800,7 +906,8 @@ class _FileManagerPageState extends State<FileManagerPage>
onPointerSignal: (e) {
if (e is PointerScrollEvent) {
final sc = getBreadCrumbScrollController(isLocal);
sc.jumpTo(sc.offset + e.scrollDelta.dy / 4);
final scale = Platform.isWindows ? 2 : 4;
sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
}
},
child: BreadCrumb(
@ -824,7 +931,7 @@ class _FileManagerPageState extends State<FileManagerPage>
final x = offset.dx;
final y = offset.dy + size.height + 1;
final isPeerWindows = isWindows(isLocal);
final isPeerWindows = model.getCurrentIsWindows(isLocal);
final List<MenuEntryBase> menuItems = [
MenuEntryButton(
childBuilder: (TextStyle? style) => isPeerWindows
@ -870,6 +977,8 @@ class _FileManagerPageState extends State<FileManagerPage>
},
dismissOnClicked: true));
}
} catch (e) {
debugPrint("buildBread fetchDirectory err=$e");
} finally {
if (!isLocal) {
_ffi.dialogManager.dismissByTag(loadingTag);
@ -912,7 +1021,8 @@ class _FileManagerPageState extends State<FileManagerPage>
bool isLocal, void Function(List<String>) onPressed) {
final path = model.getCurrentDir(isLocal).path;
final breadCrumbList = List<BreadCrumbItem>.empty(growable: true);
if (isWindows(isLocal) && path == '/') {
final isWindows = model.getCurrentIsWindows(isLocal);
if (isWindows && path == '/') {
breadCrumbList.add(BreadCrumbItem(
content: TextButton(
child: buildWindowsThisPC(),
@ -921,7 +1031,7 @@ class _FileManagerPageState extends State<FileManagerPage>
onPressed: () => onPressed(['/']))
.marginSymmetric(horizontal: 4)));
} else {
final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal));
final list = PathUtil.split(path, isWindows);
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
content: TextButton(
child: Text(e.value),
@ -933,14 +1043,6 @@ class _FileManagerPageState extends State<FileManagerPage>
return breadCrumbList;
}
bool isWindows(bool isLocal) {
if (isLocal) {
return Platform.isWindows;
} else {
return _ffi.ffiModel.pi.platform.toLowerCase() == "windows";
}
}
breadCrumbScrollToEnd(bool isLocal) {
Future.delayed(Duration(milliseconds: 200), () {
final breadCrumbScroller = getBreadCrumbScrollController(isLocal);
@ -999,9 +1101,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}
openDirectory(String path, {bool isLocal = false}) {
model.openDirectory(path, isLocal: isLocal).then((_) {
breadCrumbScrollToEnd(isLocal);
});
model.openDirectory(path, isLocal: isLocal);
}
void handleDragDone(DropDoneDetails details, bool isLocal) {
@ -1022,4 +1122,14 @@ class _FileManagerPageState extends State<FileManagerPage>
}
model.sendFiles(items, isRemote: false);
}
void refocusKeyboardListener(bool isLocal) {
Future.delayed(Duration.zero, () {
if (isLocal) {
_keyboardNodeLocal.requestFocus();
} else {
_keyboardNodeRemote.requestFocus();
}
});
}
}

View File

@ -31,6 +31,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
_FileManagerTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
tabController.onSelected = (_, id) {
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
};
tabController.add(TabInfo(
key: params['id'],
label: params['id'],

View File

@ -127,8 +127,8 @@ class _PortForwardPageState extends State<PortForwardPage>
}
buildTunnel(BuildContext context) {
text(String lable) => Expanded(
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
text(String label) => Expanded(
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
@ -241,8 +241,8 @@ class _PortForwardPageState extends State<PortForwardPage>
}
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
text(String lable) => Expanded(
child: Text(lable, style: const TextStyle(fontSize: 20))
text(String label) => Expanded(
child: Text(label, style: const TextStyle(fontSize: 20))
.marginOnly(left: _kTextLeftMargin));
return Container(
@ -285,11 +285,11 @@ class _PortForwardPageState extends State<PortForwardPage>
}
buildRdp(BuildContext context) {
text1(String lable) => Expanded(
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
text2(String lable) => Expanded(
text1(String label) => Expanded(
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
text2(String label) => Expanded(
child: Text(
lable,
label,
style: const TextStyle(fontSize: 20),
).marginOnly(left: _kTextLeftMargin));
return Theme(

View File

@ -31,6 +31,10 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
isRDP = params['isRDP'];
tabController =
Get.put(DesktopTabController(tabType: DesktopTabType.portForward));
tabController.onSelected = (_, id) {
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
};
tabController.add(TabInfo(
key: params['id'],
label: params['id'],

View File

@ -2,9 +2,12 @@ import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
as custom_cursor_manager;
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
@ -20,6 +23,7 @@ import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../widgets/remote_menubar.dart';
import '../widgets/kb_layout_type_chooser.dart';
bool _isCustomCursorInited = false;
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
@ -29,10 +33,12 @@ class RemotePage extends StatefulWidget {
Key? key,
required this.id,
required this.menubarState,
this.switchUuid,
}) : super(key: key);
final String id;
final MenubarState menubarState;
final String? switchUuid;
final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
@ -46,17 +52,17 @@ class RemotePage extends StatefulWidget {
}
class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, MultiWindowListener {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
final _cursorOverImage = false.obs;
late RxBool _showRemoteCursor;
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
final FocusNode _rawKeyFocusNode = FocusNode();
var _imageFocused = false;
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
Function(bool)? _onEnterOrLeaveImage4Menubar;
@ -92,7 +98,14 @@ class _RemotePageState extends State<RemotePage>
_initStates(widget.id);
_ffi = FFI();
Get.put(_ffi, tag: widget.id);
_ffi.start(widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
});
_ffi.start(
widget.id,
switchUuid: widget.switchUuid,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
_ffi.dialogManager
@ -101,7 +114,6 @@ class _RemotePageState extends State<RemotePage>
if (!Platform.isLinux) {
Wakelock.enable();
}
_rawKeyFocusNode.requestFocus();
_ffi.ffiModel.updateEventListener(widget.id);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// Session option should be set after models.dart/FFI.start
@ -109,22 +121,59 @@ class _RemotePageState extends State<RemotePage>
id: widget.id, arg: 'show-remote-cursor');
_zoomCursor.value =
bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor');
if (!_isCustomCursorInited) {
customCursorController.registerNeedUpdateCursorCallback(
(String? lastKey, String? currentKey) async {
if (_firstEnterImage.value) {
_firstEnterImage.value = false;
return true;
}
return lastKey == null || lastKey != currentKey;
});
_isCustomCursorInited = true;
DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback(
// (String? lastKey, String? currentKey) async {
// if (_firstEnterImage.value) {
// _firstEnterImage.value = false;
// return true;
// }
// return lastKey == null || lastKey != currentKey;
// });
// _isCustomCursorInited = true;
// }
}
@override
void onWindowBlur() {
super.onWindowBlur();
// On windows, we use `focus` way to handle keyboard better.
// Now on Linux, there's some rdev issues which will break the input.
// We disable the `focus` way for non-Windows temporarily.
if (Platform.isWindows) {
_isWindowBlur = true;
// unfocus the primary-focus when the whole window is lost focus,
// and let OS to handle events instead.
_rawKeyFocusNode.unfocus();
}
}
@override
void onWindowFocus() {
super.onWindowFocus();
// See [onWindowBlur].
if (Platform.isWindows) {
_isWindowBlur = false;
}
}
@override
void onWindowRestore() {
super.onWindowRestore();
// On windows, we use `onWindowRestore` way to handle window restore from
// a minimized state.
if (Platform.isWindows) {
_isWindowBlur = false;
}
}
@override
void dispose() {
debugPrint("REMOTE PAGE dispose ${widget.id}");
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(id: widget.id, enter: false);
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose();
@ -153,8 +202,23 @@ class _RemotePageState extends State<RemotePage>
color: Colors.black,
child: RawKeyFocusScope(
focusNode: _rawKeyFocusNode,
onFocusChange: (bool v) {
_imageFocused = v;
onFocusChange: (bool imageFocused) {
debugPrint(
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
// See [onWindowBlur].
if (Platform.isWindows) {
if (_isWindowBlur) {
imageFocused = false;
Future.delayed(Duration.zero, () {
_rawKeyFocusNode.unfocus();
});
}
if (imageFocused) {
_ffi.inputModel.enterOrLeave(true);
} else {
_ffi.inputModel.enterOrLeave(false);
}
}
},
inputModel: _ffi.inputModel,
child: getBodyForDesktop(context)));
@ -181,9 +245,6 @@ class _RemotePageState extends State<RemotePage>
}
void enterView(PointerEnterEvent evt) {
if (!_imageFocused) {
_rawKeyFocusNode.requestFocus();
}
_cursorOverImage.value = true;
_firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Menubar != null) {
@ -193,7 +254,13 @@ class _RemotePageState extends State<RemotePage>
//
}
}
_ffi.inputModel.enterOrLeave(true);
// See [onWindowBlur].
if (!Platform.isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
bind.sessionEnterOrLeave(id: widget.id, enter: true);
}
}
void leaveView(PointerExitEvent evt) {
@ -206,7 +273,10 @@ class _RemotePageState extends State<RemotePage>
//
}
}
_ffi.inputModel.enterOrLeave(false);
// See [onWindowBlur].
if (!Platform.isWindows) {
bind.sessionEnterOrLeave(id: widget.id, enter: false);
}
}
Widget getBodyForDesktop(BuildContext context) {
@ -228,6 +298,21 @@ class _RemotePageState extends State<RemotePage>
listenerBuilder: (child) => RawPointerMouseRegion(
onEnter: enterView,
onExit: leaveView,
onPointerDown: (event) {
// A double check for blur status.
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
// Sometimes the system does not send the necessary focus event to flutter. We should manually
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
// ensure the grab-key thread is running when our users are clicking the remote canvas.
if (_isWindowBlur) {
debugPrint(
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
_isWindowBlur = false;
}
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
},
inputModel: _ffi.inputModel,
child: child,
),
@ -235,9 +320,9 @@ class _RemotePageState extends State<RemotePage>
}))
];
if (!_ffi.canvasModel.cursorEmbeded) {
paints.add(Obx(() => Visibility(
visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue,
if (!_ffi.canvasModel.cursorEmbedded) {
paints.add(Obx(() => Offstage(
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
child: CursorPaint(
id: widget.id,
zoomCursor: _zoomCursor,
@ -302,7 +387,7 @@ class _ImagePaintState extends State<ImagePaint> {
mouseRegion({child}) => Obx(() => MouseRegion(
cursor: cursorOverImage.isTrue
? c.cursorEmbeded
? c.cursorEmbedded
? SystemMouseCursors.none
: keyboardEnabled.isTrue
? (() {
@ -322,35 +407,36 @@ class _ImagePaintState extends State<ImagePaint> {
onHover: (evt) {},
child: child));
if (c.scrollStyle == ScrollStyle.scrollbar) {
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
final imageHeight = c.getDisplayHeight() * s;
final imageSize = Size(imageWidth, imageHeight);
final imageWidget = CustomPaint(
size: Size(imageWidth, imageHeight),
size: imageSize,
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final percentX = _horizontal.hasClients
? _horizontal.position.extentBefore /
(_horizontal.position.extentBefore +
_horizontal.position.extentInside +
_horizontal.position.extentAfter)
: 0.0;
final percentY = _vertical.hasClients
? _vertical.position.extentBefore /
(_vertical.position.extentBefore +
_vertical.position.extentInside +
_vertical.position.extentAfter)
: 0.0;
c.setScrollPercent(percentX, percentY);
return false;
},
child: mouseRegion(
child: _buildCrossScrollbar(context, _buildListener(imageWidget),
Size(imageWidth, imageHeight))),
);
onNotification: (notification) {
final percentX = _horizontal.hasClients
? _horizontal.position.extentBefore /
(_horizontal.position.extentBefore +
_horizontal.position.extentInside +
_horizontal.position.extentAfter)
: 0.0;
final percentY = _vertical.hasClients
? _vertical.position.extentBefore /
(_vertical.position.extentBefore +
_vertical.position.extentInside +
_vertical.position.extentAfter)
: 0.0;
c.setScrollPercent(percentX, percentY);
return false;
},
child: mouseRegion(
child: Obx(() => _buildCrossScrollbarFromLayout(
context, _buildListener(imageWidget), c.size, imageSize)),
));
} else {
final imageWidget = CustomPaint(
size: Size(c.size.width, c.size.height),
@ -366,15 +452,23 @@ class _ImagePaintState extends State<ImagePaint> {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale, zoomCursor.value);
cursor.addKey(key);
return FlutterCustomMemoryImageCursor(
pixbuf: cache.data,
key: key,
hotx: cache.hotx,
hoty: cache.hoty,
imageWidth: (cache.width * cache.scale).toInt(),
imageHeight: (cache.height * cache.scale).toInt(),
);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always
// be executed after this.
custom_cursor_manager.CursorManager.instance
.registerCursor(custom_cursor_manager.CursorData()
..buffer = cache.data!
..height = (cache.height * cache.scale).toInt()
..width = (cache.width * cache.scale).toInt()
..hotX = cache.hotx
..hotY = cache.hoty
..name = key);
cursor.addKey(key);
}
return FlutterCustomMemoryImageCursor(key: key);
}
}
@ -477,24 +571,6 @@ class _ImagePaintState extends State<ImagePaint> {
return widget;
}
Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) {
var layoutSize = MediaQuery.of(context).size;
// If minimized, w or h may be negative here.
final w = layoutSize.width - kWindowBorderWidth * 2;
final h =
layoutSize.height - kWindowBorderWidth * 2 - kDesktopRemoteTabBarHeight;
layoutSize = Size(
w < 0 ? 0 : w,
h < 0 ? 0 : h,
);
bool overflow =
layoutSize.width < size.width || layoutSize.height < size.height;
return overflow
? Obx(() =>
_buildCrossScrollbarFromLayout(context, child, layoutSize, size))
: _buildCrossScrollbarFromLayout(context, child, layoutSize, size);
}
Widget _buildListener(Widget child) {
if (listenerBuilder != null) {
return listenerBuilder!(child);
@ -529,7 +605,8 @@ class CursorPaint extends StatelessWidget {
double cx = c.x;
double cy = c.y;
if (c.scrollStyle == ScrollStyle.scrollbar) {
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
c.scrollStyle == ScrollStyle.scrollbar) {
final d = c.parent.target!.ffiModel.display;
final imageWidth = d.width * c.scale;
final imageHeight = d.height * c.scale;
@ -538,7 +615,7 @@ class CursorPaint extends StatelessWidget {
}
double x = (m.x - hotx) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cy;
double scale = 1.0;
if (zoomCursor.isTrue) {
x = m.x - hotx + cx / c.scale;

View File

@ -39,8 +39,7 @@ class ConnectionTabPage extends StatefulWidget {
class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabController = Get.put(DesktopTabController(
tabType: DesktopTabType.remoteScreen,
onSelected: (_, id) => bind.setCurSessionId(id: id)));
tabType: DesktopTabType.remoteScreen));
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
@ -54,6 +53,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final peerId = params['id'];
if (peerId != null) {
ConnectionTypeState.init(peerId);
tabController.onSelected = (_, id) {
bind.setCurSessionId(id: id);
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
};
tabController.add(TabInfo(
key: peerId,
label: peerId,
@ -64,6 +68,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(peerId),
id: peerId,
menubarState: _menubarState,
switchUuid: params['switch_uuid'],
),
));
_update_remote_count();
@ -75,6 +80,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
@ -84,6 +90,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
if (call.method == "new_remote_desktop") {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
window_on_top(windowId());
ConnectionTypeState.init(id);
tabController.add(TabInfo(
@ -96,6 +103,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(id),
id: id,
menubarState: _menubarState,
switchUuid: switchUuid,
),
));
} else if (call.method == "onDestroy") {
@ -257,7 +265,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
),
]);
if (!ffi.canvasModel.cursorEmbeded) {
if (!ffi.canvasModel.cursorEmbedded) {
menu.add(MenuEntryDivider<String>());
menu.add(() {
final state = ShowRemoteCursorState.find(key);
@ -308,7 +316,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
dismissOnClicked: true,
));
if (pi.platform == 'Linux' || pi.sasEnabled) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',

View File

@ -30,7 +30,12 @@ class _DesktopServerPageState extends State<DesktopServerPage>
void initState() {
gFFI.ffiModel.updateEventListener("");
windowManager.addListener(this);
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.onRemoved = (_, id) {
onRemoveId(id);
};
tabController.onSelected = (_, id) {
windowManager.setTitle(getWindowNameWithId(id));
};
super.initState();
}
@ -238,7 +243,7 @@ Widget buildConnectionCard(Client client) {
key: ValueKey(client.id),
children: [
_CmHeader(client: client),
client.isFileTransfer || client.disconnected
client.type_() != ClientType.remote || client.disconnected
? Offstage()
: _PrivilegeBoard(client: client),
Expanded(
@ -376,7 +381,7 @@ class _CmHeaderState extends State<_CmHeader>
),
),
Offstage(
offstage: !client.authorized || client.isFileTransfer,
offstage: !client.authorized || client.type_() != ClientType.remote,
child: IconButton(
onPressed: () => checkClickTime(
client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)),
@ -510,10 +515,21 @@ class _CmControlPanel extends StatelessWidget {
buildAuthorized(BuildContext context) {
final bool canElevate = bind.cmCanElevate();
final model = Provider.of<ServerModel>(context);
final showElevation = canElevate && model.showElevation;
final showElevation = canElevate &&
model.showElevation &&
client.type_() == ClientType.remote;
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Offstage(
offstage: !client.fromSwitch,
child: buildButton(context,
color: Colors.purple,
onClick: () => handleSwitchBack(context),
icon: Icon(Icons.reply, color: Colors.white),
text: "Switch Sides",
textColor: Colors.white),
),
Offstage(
offstage: !showElevation,
child: buildButton(context, color: Colors.green[700], onClick: () {
@ -560,7 +576,9 @@ class _CmControlPanel extends StatelessWidget {
buildUnAuthorized(BuildContext context) {
final bool canElevate = bind.cmCanElevate();
final model = Provider.of<ServerModel>(context);
final showElevation = canElevate && model.showElevation;
final showElevation = canElevate &&
model.showElevation &&
client.type_() == ClientType.remote;
final showAccept = model.approveMode != 'password';
return Column(
mainAxisAlignment: MainAxisAlignment.end,
@ -670,6 +688,10 @@ class _CmControlPanel extends StatelessWidget {
windowManager.close();
}
}
void handleSwitchBack(BuildContext context) {
bind.cmSwitchBack(connId: client.id);
}
}
void checkClickTime(int id, Function() callback) async {

View File

@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
class DesktopRemoteScreen extends StatelessWidget {
final Map<String, dynamic> params;
const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key);
DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) {
if (!bind.mainStartGrabKeyboard()) {
stateGlobal.grabKeyboard = true;
}
}
@override
Widget build(BuildContext context) {

View File

@ -0,0 +1,225 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import '../../common.dart';
typedef KBChosenCallback = Future<bool> Function(String);
const double _kImageMarginVertical = 6.0;
const double _kImageMarginHorizontal = 10.0;
const double _kImageBoarderWidth = 4.0;
const double _kImagePaddingWidth = 4.0;
const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2);
const double _kBorderRadius = 6.0;
const String _kKBLayoutTypeISO = 'ISO';
const String _kKBLayoutTypeNotISO = 'Not ISO';
const _kKBLayoutImageMap = {
_kKBLayoutTypeISO: 'kb_layout_iso',
_kKBLayoutTypeNotISO: 'kb_layout_not_iso',
};
class _KBImage extends StatelessWidget {
final String kbLayoutType;
final double imageWidth;
final RxString chosenType;
const _KBImage({
Key? key,
required this.kbLayoutType,
required this.imageWidth,
required this.chosenType,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_kBorderRadius),
border: Border.all(
color: chosenType.value == kbLayoutType
? _kImageBorderColor
: Colors.transparent,
width: _kImageBoarderWidth,
),
),
margin: EdgeInsets.symmetric(
horizontal: _kImageMarginHorizontal,
vertical: _kImageMarginVertical,
),
padding: EdgeInsets.all(_kImagePaddingWidth),
child: SvgPicture.asset(
'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg',
width: imageWidth -
_kImageMarginHorizontal * 2 -
_kImagePaddingWidth * 2 -
_kImageBoarderWidth * 2,
),
);
});
}
}
class _KBChooser extends StatelessWidget {
final String kbLayoutType;
final double imageWidth;
final RxString chosenType;
final KBChosenCallback cb;
const _KBChooser({
Key? key,
required this.kbLayoutType,
required this.imageWidth,
required this.chosenType,
required this.cb,
}) : super(key: key);
@override
Widget build(BuildContext context) {
onChanged(String? v) async {
if (v != null) {
if (await cb(v)) {
chosenType.value = v;
}
}
}
return Column(
children: [
TextButton(
onPressed: () {
onChanged(kbLayoutType);
},
child: _KBImage(
kbLayoutType: kbLayoutType,
imageWidth: imageWidth,
chosenType: chosenType,
),
style: TextButton.styleFrom(padding: EdgeInsets.zero),
),
TextButton(
child: Row(
children: [
Obx(() => Radio(
splashRadius: 0,
value: kbLayoutType,
groupValue: chosenType.value,
onChanged: onChanged,
)),
Text(kbLayoutType),
],
),
onPressed: () {
onChanged(kbLayoutType);
},
),
],
);
}
}
class KBLayoutTypeChooser extends StatelessWidget {
final RxString chosenType;
final double width;
final double height;
final double dividerWidth;
final KBChosenCallback cb;
KBLayoutTypeChooser({
Key? key,
required this.chosenType,
required this.width,
required this.height,
required this.dividerWidth,
required this.cb,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final imageWidth = width / 2 - dividerWidth;
return SizedBox(
width: width,
height: height,
child: Center(
child: Row(
children: [
_KBChooser(
kbLayoutType: _kKBLayoutTypeISO,
imageWidth: imageWidth,
chosenType: chosenType,
cb: cb,
),
VerticalDivider(
width: dividerWidth * 2,
),
_KBChooser(
kbLayoutType: _kKBLayoutTypeNotISO,
imageWidth: imageWidth,
chosenType: chosenType,
cb: cb,
),
],
),
),
);
}
}
RxString KBLayoutType = ''.obs;
String getLocalPlatformForKBLayoutType(String peerPlatform) {
String localPlatform = '';
if (peerPlatform != kPeerPlatformMacOS) {
return localPlatform;
}
if (Platform.isWindows) {
localPlatform = kPeerPlatformWindows;
} else if (Platform.isLinux) {
localPlatform = kPeerPlatformLinux;
}
// to-do: web desktop support ?
return localPlatform;
}
showKBLayoutTypeChooserIfNeeded(
String peerPlatform,
OverlayDialogManager dialogManager,
) async {
final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform);
if (localPlatform == '') {
return;
}
KBLayoutType.value = bind.getLocalKbLayoutType();
if (KBLayoutType.value == _kKBLayoutTypeISO ||
KBLayoutType.value == _kKBLayoutTypeNotISO) {
return;
}
showKBLayoutTypeChooser(localPlatform, dialogManager);
}
showKBLayoutTypeChooser(
String localPlatform,
OverlayDialogManager dialogManager,
) {
dialogManager.show((setState, close) {
return CustomAlertDialog(
title:
Text('${translate('Select local keyboard type')} ($localPlatform)'),
content: KBLayoutTypeChooser(
chosenType: KBLayoutType,
width: 360,
height: 200,
dividerWidth: 4.0,
cb: (String v) async {
await bind.setLocalKbLayoutType(kbLayoutType: v);
KBLayoutType.value = bind.getLocalKbLayoutType();
return v == KBLayoutType.value;
}),
actions: [dialogButton('Close', onPressed: close)],
onCancel: close,
);
});
}

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
class ListSearchActionListener extends StatelessWidget {
final FocusNode node;
final TimeoutStringBuffer buffer;
final Widget child;
final Function(String) onNext;
final Function(String) onSearch;
const ListSearchActionListener(
{super.key,
required this.node,
required this.buffer,
required this.child,
required this.onNext,
required this.onSearch});
@mustCallSuper
@override
Widget build(BuildContext context) {
return KeyboardListener(
autofocus: true,
onKeyEvent: (kv) {
final ch = kv.character;
if (ch == null) {
return;
}
final action = buffer.input(ch);
switch (action) {
case ListSearchAction.search:
onSearch(buffer.buffer);
break;
case ListSearchAction.next:
onNext(buffer.buffer);
break;
}
},
focusNode: node,
child: child);
}
}
enum ListSearchAction { search, next }
class TimeoutStringBuffer {
var _buffer = "";
late DateTime _duration;
static int timeoutMilliSec = 1500;
String get buffer => _buffer;
TimeoutStringBuffer() {
_duration = DateTime.now();
}
ListSearchAction input(String ch) {
final curr = DateTime.now();
try {
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {
_buffer = ch;
return ListSearchAction.search;
} else {
if (ch == _buffer) {
return ListSearchAction.next;
} else {
_buffer += ch;
return ListSearchAction.search;
}
}
} finally {
_duration = curr;
}
}
}

View File

@ -1,521 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
class _IconOP extends StatelessWidget {
final String icon;
final double iconWidth;
const _IconOP({Key? key, required this.icon, required this.iconWidth})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: SvgPicture.asset(
'assets/$icon.svg',
width: iconWidth,
),
);
}
}
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
final double iconWidth;
final Color primaryColor;
final double height;
final Function() onTap;
const ButtonOP({
Key? key,
required this.op,
required this.curOP,
required this.iconWidth,
required this.primaryColor,
required this.height,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(
child: Container(
height: height,
padding: kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed:
curOP.value.isEmpty || curOP.value == op ? onTap : null,
child: Stack(children: [
Center(child: Text('${translate("Continue with")} $op')),
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 120,
child: _IconOP(
icon: op,
iconWidth: iconWidth,
)),
),
]),
)),
),
)
]);
}
}
class ConfigOP {
final String op;
final double iconWidth;
ConfigOP({required this.op, required this.iconWidth});
}
class WidgetOP extends StatefulWidget {
final ConfigOP config;
final RxString curOP;
final Function(String) cbLogin;
const WidgetOP({
Key? key,
required this.config,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _WidgetOPState();
}
}
class _WidgetOPState extends State<WidgetOP> {
Timer? _updateTimer;
String _stateMsg = '';
String _FailedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_updateTimer?.cancel();
}
_beginQueryState() {
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_updateState();
});
}
_updateState() {
bind.mainAccountAuthResult().then((result) {
if (result.isEmpty) {
return;
}
final resultMap = jsonDecode(result);
if (resultMap == null) {
return;
}
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _FailedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url));
_url = url;
}
if (authBody != null) {
_updateTimer?.cancel();
final String username = authBody['user']['name'];
widget.curOP.value = '';
widget.cbLogin(username);
}
setState(() {
_stateMsg = stateMsg;
_FailedMsg = failedMsg;
if (failedMsg.isNotEmpty) {
widget.curOP.value = '';
_updateTimer?.cancel();
}
});
}
});
}
_resetState() {
_stateMsg = '';
_FailedMsg = '';
_url = '';
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
iconWidth: widget.config.iconWidth,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
_resetState();
widget.curOP.value = widget.config.op;
await bind.mainAccountAuth(op: widget.config.op);
_beginQueryState();
},
),
Obx(() {
if (widget.curOP.isNotEmpty &&
widget.curOP.value != widget.config.op) {
_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,
),
),
],
));
}),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: const SizedBox(
height: 5.0,
),
),
),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 20),
child: ElevatedButton(
onPressed: () {
widget.curOP.value = '';
_updateTimer?.cancel();
_resetState();
bind.mainAccountAuthCancel();
},
child: Text(
translate('Cancel'),
style: TextStyle(fontSize: 15),
),
),
),
),
),
],
);
}
}
class LoginWidgetOP extends StatelessWidget {
final List<ConfigOP> ops;
final RxString curOP;
final Function(String) cbLogin;
LoginWidgetOP({
Key? key,
required this.ops,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var children = ops
.map((op) => [
WidgetOP(
config: op,
curOP: curOP,
cbLogin: cbLogin,
),
const Divider(
indent: 5,
endIndent: 5,
)
])
.expand((i) => i)
.toList();
if (children.isNotEmpty) {
children.removeLast();
}
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
));
}
}
class LoginWidgetUserPass extends StatelessWidget {
final String username;
final String pass;
final String usernameMsg;
final String passMsg;
final bool isInProgress;
final RxString curOP;
final Function(String, String) onLogin;
const LoginWidgetUserPass({
Key? key,
required this.username,
required this.pass,
required this.usernameMsg,
required this.passMsg,
required this.isInProgress,
required this.curOP,
required this.onLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: pass);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
Container(
padding: kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: usernameMsg.isNotEmpty ? usernameMsg : null),
controller: userController,
focusNode: FocusNode()..requestFocus(),
),
),
],
),
),
const SizedBox(
height: 8.0,
),
Container(
padding: kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text('${translate("Password")}:')
.marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: passMsg.isNotEmpty ? passMsg : null),
controller: pwdController,
),
),
],
),
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator()),
const SizedBox(
height: 12.0,
),
Row(children: [
Expanded(
child: Container(
height: 38,
padding: kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: curOP.value.isEmpty || curOP.value == 'rustdesk'
? null
: ElevatedButton.styleFrom(
primary: Colors.grey,
),
child: Text(
translate('Login'),
style: TextStyle(fontSize: 16),
),
onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk'
? () {
onLogin(userController.text, pwdController.text);
}
: null,
)),
),
),
]),
],
);
}
}
/// common login dialog for desktop
/// call this directly
Future<bool> loginDialog() async {
String username = '';
var usernameMsg = '';
String pass = '';
var passMsg = '';
var isInProgress = false;
var completer = Completer<bool>();
final RxString curOP = ''.obs;
gFFI.dialogManager.show((setState, close) {
cancel() {
isInProgress = false;
completer.complete(false);
close();
}
onLogin(String username0, String pass0) async {
setState(() {
usernameMsg = '';
passMsg = '';
isInProgress = true;
});
cancel() {
curOP.value = '';
if (isInProgress) {
setState(() {
isInProgress = false;
});
}
}
curOP.value = 'rustdesk';
username = username0;
pass = pass0;
if (username.isEmpty) {
usernameMsg = translate('Username missed');
cancel();
return;
}
if (pass.isEmpty) {
passMsg = translate('Password missed');
cancel();
return;
}
try {
final resp = await gFFI.userModel.login(username, pass);
if (resp.containsKey('error')) {
passMsg = resp['error'];
cancel();
return;
}
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
debugPrint('$resp');
completer.complete(true);
} catch (err) {
debugPrint(err.toString());
cancel();
return;
}
close();
}
return CustomAlertDialog(
title: Text(translate('Login')),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
LoginWidgetUserPass(
username: username,
pass: pass,
usernameMsg: usernameMsg,
passMsg: passMsg,
isInProgress: isInProgress,
curOP: curOP,
onLogin: onLogin,
),
const SizedBox(
height: 8.0,
),
Center(
child: Text(
translate('or'),
style: TextStyle(fontSize: 16),
)),
const SizedBox(
height: 8.0,
),
LoginWidgetOP(
ops: [
ConfigOP(op: 'Github', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
curOP: curOP,
cbLogin: (String username) {
gFFI.userModel.userName.value = username;
completer.complete(true);
close();
},
),
],
),
),
actions: [msgBoxButton(translate('Close'), cancel)],
onCancel: cancel,
);
});
return completer.future;
}

View File

@ -118,6 +118,15 @@ abstract class MenuEntryBase<T> {
this.enabled,
});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
enabledStyle(BuildContext context) => TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
disabledStyle() => TextStyle(
color: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
}
class MenuEntryDivider<T> extends MenuEntryBase<T> {
@ -189,54 +198,76 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
mod_menu.PopupMenuEntry<T> _buildMenuItem(
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
Widget getTextChild() {
final enabledTextChild = Text(
opt.text,
style: enabledStyle(context),
);
final disabledTextChild = Text(
opt.text,
style: disabledStyle(),
);
if (opt.enabled == null) {
return enabledTextChild;
} else {
return Obx(
() => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild);
}
}
final child = Container(
padding: padding,
alignment: AlignmentDirectional.centerStart,
constraints:
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
child: Row(
children: [
getTextChild(),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Transform.scale(
scale: MenuConfig.iconScale,
child: Obx(() => opt.value == curOption.value
? IconButton(
padding:
const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0),
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
onPressed: () {},
icon: Icon(
Icons.check,
color: (opt.enabled ?? true.obs).isTrue
? conf.commonColor
: Colors.grey,
))
: const SizedBox.shrink()),
))),
],
),
);
onPressed() {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(opt.value);
}
return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: Container(
width: conf.boxWidth,
child: TextButton(
child: Container(
padding: padding,
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(
minHeight: conf.height, maxHeight: conf.height),
child: Row(
children: [
Text(
opt.text,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Transform.scale(
scale: MenuConfig.iconScale,
child: Obx(() => opt.value == curOption.value
? IconButton(
padding: const EdgeInsets.fromLTRB(
8.0, 0.0, 8.0, 0.0),
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
onPressed: () {},
icon: Icon(
Icons.check,
color: conf.commonColor,
))
: const SizedBox.shrink()),
))),
],
),
),
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(opt.value);
},
)),
width: conf.boxWidth,
child: opt.enabled == null
? TextButton(
child: child,
onPressed: onPressed,
)
: Obx(() => TextButton(
child: child,
onPressed: opt.enabled!.isTrue ? onPressed : null,
)),
),
);
}
@ -567,12 +598,9 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
const SizedBox(width: MenuConfig.midPadding),
Obx(() => Text(
text,
style: TextStyle(
color: super.enabled!.value
? Theme.of(context).textTheme.titleLarge?.color
: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
style: super.enabled!.value
? enabledStyle(context)
: disabledStyle(),
)),
Expanded(
child: Align(
@ -605,14 +633,6 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
final enabledStyle = TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
const disabledStyle = TextStyle(
color: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
super.enabled ??= true.obs;
return Obx(() => Container(
width: conf.boxWidth,
@ -631,7 +651,7 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
constraints:
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
child: childBuilder(
super.enabled!.value ? enabledStyle : disabledStyle),
super.enabled!.value ? enabledStyle(context) : disabledStyle()),
),
)));
}

View File

@ -26,7 +26,7 @@ class RefreshWrapperState extends State<RefreshWrapper> {
}
rebuild() {
debugPrint("=====Global State Rebuild (win-${windowId ?? 'main'})=====");
debugPrint("=====Global State Rebuild (win-${kWindowId ?? 'main'})=====");
if (Get.context != null) {
(context as Element).visitChildren(_rebuildElement);
}

View File

@ -8,9 +8,10 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart' as rxdart;
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:window_size/window_size.dart' as window_size;
@ -21,6 +22,7 @@ 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 {
final kStoreKey = 'remoteMenubarState';
@ -118,10 +120,11 @@ class RemoteMenubar extends StatefulWidget {
}
class _RemoteMenubarState extends State<RemoteMenubar> {
final Rx<Color> _hideColor = Colors.white12.obs;
final _rxHideReplay = rxdart.ReplaySubject<int>();
late Debouncer<int> _debouncerHide;
bool _isCursorOverImage = false;
window_size.Screen? _screen;
final _fractionX = 0.5.obs;
final _dragging = false.obs;
int get windowId => stateGlobal.windowId;
@ -138,23 +141,26 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
initState() {
super.initState();
_debouncerHide = Debouncer<int>(
Duration(milliseconds: 5000),
onChanged: _debouncerHideProc,
initialValue: 0,
);
widget.onEnterOrLeaveImageSetter((enter) {
if (enter) {
_rxHideReplay.add(0);
_debouncerHide.value = 0;
_isCursorOverImage = true;
} else {
_isCursorOverImage = false;
}
});
}
_rxHideReplay
.throttleTime(const Duration(milliseconds: 5000),
trailing: true, leading: false)
.listen((int v) {
if (!pin && show.isTrue && _isCursorOverImage) {
show.value = false;
}
});
_debouncerHideProc(int v) {
if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) {
show.value = false;
}
}
@override
@ -166,42 +172,38 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
@override
Widget build(BuildContext context) {
// No need to use future builder here.
_updateScreen();
return Align(
alignment: Alignment.topCenter,
child: Obx(
() => show.value ? _buildMenubar(context) : _buildShowHide(context)),
child: Obx(() => show.value
? _buildMenubar(context)
: _buildDraggableShowHide(context)),
);
}
Widget _buildShowHide(BuildContext context) {
return Obx(() => Tooltip(
message: translate(show.value ? 'Hide Menubar' : 'Show Menubar'),
child: SizedBox(
width: 100,
height: 13,
child: TextButton(
onHover: (bool v) {
_hideColor.value = v ? Colors.white60 : Colors.white24;
},
onPressed: () {
show.value = !show.value;
_hideColor.value = Colors.white24;
if (show.isTrue) {
_updateScreen();
}
},
child: Obx(() => Container(
decoration: BoxDecoration(
color: _hideColor.value,
border: Border.all(color: MyTheme.border),
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
).marginOnly(bottom: 8.0)),
))));
Widget _buildDraggableShowHide(BuildContext context) {
return Obx(() {
if (show.isTrue && _dragging.isFalse) {
_debouncerHide.value = 1;
}
return Align(
alignment: FractionalOffset(_fractionX.value, 0),
child: Offstage(
offstage: _dragging.isTrue,
child: _DraggableShowHide(
dragging: _dragging,
fractionX: _fractionX,
show: show,
),
),
);
});
}
_updateScreen() async {
final v = await DesktopMultiWindow.invokeMethod(0, 'get_window_info', '');
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
final String valueStr = v;
if (valueStr.isEmpty) {
_screen = null;
@ -253,13 +255,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: MyTheme.border),
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: menubarItems,
)),
_buildShowHide(context),
_buildDraggableShowHide(context),
]));
}
@ -364,8 +365,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
bind.sessionSwitchDisplay(id: widget.id, value: i);
pi.currentDisplay = i;
display.value = i;
}
},
)
@ -510,6 +509,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
List<MenuEntryBase<String>> _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<MenuEntryBase<String>> displayMenu = [];
displayMenu.addAll([
@ -571,7 +571,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
),
]);
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
final auditServer = bind.sessionGetAuditServerSync(id: widget.id);
final auditServer =
bind.sessionGetAuditServerSync(id: widget.id, typ: "conn");
if (auditServer.isNotEmpty) {
displayMenu.add(
MenuEntryButton<String>(
@ -589,7 +590,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
@ -604,9 +605,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
}
if (perms['restart'] != false &&
(pi.platform == 'Linux' ||
pi.platform == 'Windows' ||
pi.platform == 'Mac OS')) {
(pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Restart Remote Device'),
@ -633,7 +634,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
dismissOnClicked: true,
));
if (pi.platform == 'Windows') {
if (pi.platform == kPeerPlatformWindows) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
@ -651,6 +652,20 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
dismissOnClicked: true,
));
}
if (false &&
pi.platform != kPeerPlatformAndroid &&
version_cmp(peer_version, '1.2.0') >= 0) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Switch Sides'),
style: style,
),
proc: () =>
showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager),
padding: padding,
dismissOnClicked: true,
));
}
}
if (pi.version.isNotEmpty) {
@ -699,12 +714,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (_screen == null) {
return false;
}
double scale = _screen!.scaleFactor;
double selfWidth = _screen!.frame.width;
double selfHeight = _screen!.frame.height;
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
double selfWidth = _screen!.visibleFrame.width;
double selfHeight = _screen!.visibleFrame.height;
if (isFullscreen) {
selfWidth = _screen!.visibleFrame.width;
selfHeight = _screen!.visibleFrame.height;
selfWidth = _screen!.frame.width;
selfHeight = _screen!.frame.height;
}
final canvasModel = widget.ffi.canvasModel;
@ -721,6 +736,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
List<MenuEntryBase<String>> _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 = [
MenuEntryRadios<String>(
text: translate('Ratio'),
@ -809,7 +825,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
if (newValue == kRemoteImageQualityCustom) {
final btnClose = msgBoxButton(translate('Close'), () async {
final btnClose = dialogButton('Close', onPressed: () async {
await setCustomValues();
widget.ffi.dialogManager.dismissAll();
});
@ -829,15 +845,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
qualityInitValue = qualityMaxValue;
}
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
final qualityRxReplay = rxdart.ReplaySubject<double>();
qualityRxReplay
.throttleTime(const Duration(milliseconds: 1000),
trailing: true, leading: false)
.listen((double v) {
() async {
await setCustomValues(quality: v);
}();
});
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(quality: v);
},
initialValue: qualityInitValue,
);
final qualitySlider = Obx(() => Row(
children: [
Slider(
@ -847,7 +861,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
divisions: 90,
onChanged: (double value) {
qualitySliderValue.value = value;
qualityRxReplay.add(value);
debouncerQuality.value = value;
},
),
SizedBox(
@ -867,15 +881,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
fpsInitValue = 30;
}
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
final fpsRxReplay = rxdart.ReplaySubject<double>();
fpsRxReplay
.throttleTime(const Duration(milliseconds: 1000),
trailing: true, leading: false)
.listen((double v) {
() async {
await setCustomValues(fps: v);
}();
});
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(fps: v);
},
initialValue: qualityInitValue,
);
bool? direct;
try {
direct = ConnectionTypeState.find(widget.id).direct.value ==
@ -884,9 +896,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final fpsSlider = Offstage(
offstage:
(await bind.mainIsUsingPublicServer() && direct != true) ||
(await bind.versionToNumber(
v: widget.ffi.ffiModel.pi.version) <
await bind.versionToNumber(v: '1.2.0')),
version_cmp(peer_version, '1.2.0') < 0,
child: Row(
children: [
Obx((() => Slider(
@ -896,7 +906,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
divisions: 22,
onChanged: (double value) {
fpsSliderValue.value = value;
fpsRxReplay.add(value);
debouncerFps.value = value;
},
))),
SizedBox(
@ -940,11 +950,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('ScrollAuto'),
value: kRemoteScrollStyleAuto,
dismissOnClicked: true,
enabled: widget.ffi.canvasModel.imageOverflow,
),
MenuEntryRadioOption(
text: translate('Scrollbar'),
value: kRemoteScrollStyleBar,
dismissOnClicked: true,
enabled: widget.ffi.canvasModel.imageOverflow,
),
],
curOptionGetter: () async =>
@ -958,75 +970,77 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
dismissOnClicked: true,
));
displayMenu.insert(3, MenuEntryDivider<String>());
}
if (_isWindowCanBeAdjusted(remoteCount)) {
displayMenu.insert(
0,
MenuEntryDivider<String>(),
);
displayMenu.insert(
0,
MenuEntryButton<String>(
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;
if (_isWindowCanBeAdjusted(remoteCount)) {
displayMenu.insert(
0,
MenuEntryDivider<String>(),
);
displayMenu.insert(
0,
MenuEntryButton<String>(
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.windowBorderWidth * 2) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() +
canvasModel.tabBarHeight +
canvasModel.windowBorderWidth * 2) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
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;
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));
}
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,
),
);
}();
},
padding: padding,
dismissOnClicked: true,
),
);
}
}
/// Show Codec Preference
@ -1038,7 +1052,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final h265 = codecsJson['h265'] ?? false;
codecs.add(h264);
codecs.add(h265);
} finally {}
} catch (e) {
debugPrint("Show Codec Preference err=$e");
}
if (codecs.length == 2 && (codecs[0] || codecs[1])) {
displayMenu.add(MenuEntryRadios<String>(
text: translate('Codec Preference'),
@ -1088,7 +1104,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
/// Show remote cursor
if (!widget.ffi.canvasModel.cursorEmbeded) {
if (!widget.ffi.canvasModel.cursorEmbedded) {
displayMenu.add(() {
final state = ShowRemoteCursorState.find(widget.id);
return MenuEntrySwitch2<String>(
@ -1155,7 +1171,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
if (Platform.isWindows &&
pi.platform == 'Windows' &&
pi.platform == kPeerPlatformWindows &&
perms['file'] != false) {
displayMenu.add(_createSwitchMenuEntry(
'Allow file copy and paste', 'enable-file-transfer', padding, true));
@ -1168,7 +1184,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
displayMenu.add(_createSwitchMenuEntry(
'Lock after session end', 'lock-after-session-end', padding, true));
if (pi.platform == 'Windows') {
if (pi.features.privacyMode) {
displayMenu.add(MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Privacy mode'),
@ -1188,22 +1204,85 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
List<MenuEntryBase<String>> _getKeyboardMenu() {
final keyboardMenu = [
final List<MenuEntryBase<String>> keyboardMenu = [
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'),
MenuEntryRadioOption(text: translate('Map mode'), value: 'map'),
],
curOptionGetter: () async =>
await bind.sessionGetKeyboardName(id: widget.id),
optionsGetter: () {
List<MenuEntryRadioOption> list = [];
List<String> modes = ["legacy"];
if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) {
modes.add("map");
}
for (String mode in modes) {
if (mode == "legacy") {
list.add(MenuEntryRadioOption(
text: translate('Legacy mode'), value: 'legacy'));
} else if (mode == "map") {
list.add(MenuEntryRadioOption(
text: translate('Map mode'), value: 'map'));
}
}
return list;
},
curOptionGetter: () async {
return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetKeyboardMode(
id: widget.id, keyboardMode: newValue);
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
},
)
];
final localPlatform =
getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform);
if (localPlatform != '') {
keyboardMenu.add(MenuEntryDivider());
keyboardMenu.add(
MenuEntryButton<String>(
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>[
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);
}
showKBLayoutTypeChooser(
localPlatform, widget.ffi.dialogManager);
},
),
),
))
],
)),
proc: () {},
padding: EdgeInsets.zero,
dismissOnClicked: false,
),
);
}
return keyboardMenu;
}
@ -1261,16 +1340,8 @@ void showSetOSPassword(
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: close,
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: submit,
child: Text(translate('OK')),
),
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@ -1334,19 +1405,125 @@ void showAuditDialog(String id, dialogManager) async {
focusNode: focusNode,
)),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: close,
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: submit,
child: Text(translate('OK')),
),
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit)
],
onSubmit: submit,
onCancel: close,
);
});
}
void showConfirmSwitchSidesDialog(
String id, OverlayDialogManager dialogManager) async {
dialogManager.show((setState, close) {
submit() async {
await bind.sessionSwitchSides(id: id);
closeConnection(id: id);
}
return CustomAlertDialog(
title: Text(translate('Switch Sides')),
content: Column(
children: [
Text(translate('Please confirm if you want to share your desktop?')),
],
),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
class _DraggableShowHide extends StatefulWidget {
final RxDouble fractionX;
final RxBool dragging;
final RxBool show;
const _DraggableShowHide({
Key? key,
required this.fractionX,
required this.dragging,
required this.show,
}) : super(key: key);
@override
State<_DraggableShowHide> createState() => _DraggableShowHideState();
}
class _DraggableShowHideState extends State<_DraggableShowHide> {
Offset position = Offset.zero;
Size size = Size.zero;
Widget _buildDraggable(BuildContext context) {
return Draggable(
axis: Axis.horizontal,
child: Icon(
Icons.drag_indicator,
size: 20,
color: Colors.grey,
),
feedback: widget,
onDragStarted: (() {
final RenderObject? renderObj = context.findRenderObject();
if (renderObj != null) {
final RenderBox renderBox = renderObj as RenderBox;
size = renderBox.size;
position = renderBox.localToGlobal(Offset.zero);
}
widget.dragging.value = true;
}),
onDragEnd: (details) {
final mediaSize = MediaQueryData.fromWindow(ui.window).size;
widget.fractionX.value +=
(details.offset.dx - position.dx) / (mediaSize.width - size.width);
if (widget.fractionX.value < 0.35) {
widget.fractionX.value = 0.35;
}
if (widget.fractionX.value > 0.65) {
widget.fractionX.value = 0.65;
}
widget.dragging.value = false;
},
);
}
@override
Widget build(BuildContext context) {
final ButtonStyle buttonStyle = ButtonStyle(
minimumSize: MaterialStateProperty.all(const Size(0, 0)),
padding: MaterialStateProperty.all(EdgeInsets.zero),
);
final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
TextButton(
onPressed: () => setState(() {
widget.show.value = !widget.show.value;
}),
child: Obx((() => Icon(
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
size: 20,
))),
),
],
);
return TextButtonTheme(
data: TextButtonThemeData(style: buttonStyle),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: MyTheme.border),
),
child: SizedBox(
height: 20,
child: child,
),
),
);
}
}

View File

@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget {
return _buildBlock(
child: Obx(() => PageView(
controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(),
children: state.value.tabs
.map((tab) => tab.page)
.toList(growable: false))));
@ -373,7 +374,7 @@ class DesktopTab extends StatelessWidget {
width: 78,
)),
Offstage(
offstage: kUseCompatibleUiMode,
offstage: kUseCompatibleUiMode || Platform.isMacOS,
child: Row(children: [
Offstage(
offstage: !showLogo,
@ -485,7 +486,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
}
});
} else {
final wc = WindowController.fromWindowId(windowId!);
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (widget.isMaximized.value != maximized) {
@ -526,13 +527,19 @@ class WindowActionPanelState extends State<WindowActionPanel>
void onWindowClose() async {
// hide window on close
if (widget.isMainWindow) {
await rustDeskWinManager.unregisterActiveWindow(0);
// `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.
await windowManager.hide();
rustDeskWinManager.unregisterActiveWindow(0);
} else {
widget.onClose?.call();
await WindowController.fromWindowId(windowId!).hide();
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": windowId!});
// it's safe to hide the subwindow
await WindowController.fromWindowId(kWindowId!).hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
}
super.onWindowClose();
}
@ -548,7 +555,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
child: Row(
children: [
Offstage(
offstage: !widget.showMinimize,
offstage: !widget.showMinimize || Platform.isMacOS,
child: ActionIcon(
message: 'Minimize',
icon: IconFont.min,
@ -556,13 +563,13 @@ class WindowActionPanelState extends State<WindowActionPanel>
if (widget.isMainWindow) {
windowManager.minimize();
} else {
WindowController.fromWindowId(windowId!).minimize();
WindowController.fromWindowId(kWindowId!).minimize();
}
},
isClose: false,
)),
Offstage(
offstage: !widget.showMaximize,
offstage: !widget.showMaximize || Platform.isMacOS,
child: Obx(() => ActionIcon(
message:
widget.isMaximized.value ? "Restore" : "Maximize",
@ -573,7 +580,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
isClose: false,
))),
Offstage(
offstage: !widget.showClose,
offstage: !widget.showClose || Platform.isMacOS,
child: ActionIcon(
message: 'Close',
icon: IconFont.close,
@ -586,7 +593,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
if (widget.isMainWindow) {
await windowManager.close();
} else {
await WindowController.fromWindowId(windowId!)
await WindowController.fromWindowId(kWindowId!)
.close();
}
});
@ -615,7 +622,7 @@ void startDragging(bool isMainWindow) {
if (isMainWindow) {
windowManager.startDragging();
} else {
WindowController.fromWindowId(windowId!).startDragging();
WindowController.fromWindowId(kWindowId!).startDragging();
}
}
@ -631,7 +638,7 @@ Future<bool> toggleMaximize(bool isMainWindow) async {
return true;
}
} else {
final wc = WindowController.fromWindowId(windowId!);
final wc = WindowController.fromWindowId(kWindowId!);
if (await wc.isMaximized()) {
wc.unmaximize();
return false;
@ -680,8 +687,8 @@ Future<bool> closeConfirmDialog() async {
]),
// confirm checkbox
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@ -824,7 +831,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Text(
translate(widget.label.value),
widget.label.value,
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
@ -899,7 +906,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
children: [
_buildTabContent(),
Obx((() => _CloseButton(
visiable: hover.value && widget.closable,
visible: hover.value && widget.closable,
tabSelected: isSelected,
onClose: () => widget.onClose(),
)))
@ -931,13 +938,13 @@ class _TabState extends State<_Tab> with RestorationMixin {
}
class _CloseButton extends StatelessWidget {
final bool visiable;
final bool visible;
final bool tabSelected;
final Function onClose;
const _CloseButton({
Key? key,
required this.visiable,
required this.visible,
required this.tabSelected,
required this.onClose,
}) : super(key: key);
@ -947,7 +954,7 @@ class _CloseButton extends StatelessWidget {
return SizedBox(
width: _kIconSize,
child: Offstage(
offstage: !visiable,
offstage: !visible,
child: InkWell(
customBorder: const RoundedRectangleBorder(),
onTap: () => onClose(),
@ -1033,8 +1040,8 @@ class AddButton extends StatelessWidget {
return ActionIcon(
message: 'New Connection',
icon: IconFont.add,
onTap: () =>
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
onTap: () => rustDeskWinManager.call(
WindowType.Main, kWindowMainWindowOnTop, ""),
isClose: false);
}
}

View File

@ -26,13 +26,15 @@ import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'models/platform_model.dart';
int? windowId;
late List<String> bootArgs;
/// Basic window and launch properties.
int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args");
bootArgs = List.from(args);
kBootArgs = List.from(args);
if (!isDesktop) {
runMobileApp();
@ -40,10 +42,10 @@ Future<void> main(List<String> args) async {
}
// main window
if (args.isNotEmpty && args.first == 'multi_window') {
windowId = int.parse(args[1]);
stateGlobal.setWindowId(windowId!);
kWindowId = int.parse(args[1]);
stateGlobal.setWindowId(kWindowId!);
if (!Platform.isMacOS) {
WindowController.fromWindowId(windowId!).showTitleBar(false);
WindowController.fromWindowId(kWindowId!).showTitleBar(false);
}
final argument = args[2].isEmpty
? <String, dynamic>{}
@ -51,15 +53,16 @@ Future<void> main(List<String> args) async {
int type = argument['type'] ?? -1;
// to-do: No need to parse window id ?
// Because stateGlobal.windowId is a global value.
argument['windowId'] = windowId;
WindowType wType = type.windowType;
switch (wType) {
argument['windowId'] = kWindowId;
kWindowType = type.windowType;
final windowName = getWindowName();
switch (kWindowType) {
case WindowType.RemoteDesktop:
desktopType = DesktopType.remote;
runMultiWindow(
argument,
kAppTypeDesktopRemote,
'RustDesk - Remote Desktop',
windowName,
);
break;
case WindowType.FileTransfer:
@ -67,7 +70,7 @@ Future<void> main(List<String> args) async {
runMultiWindow(
argument,
kAppTypeDesktopFileTransfer,
'RustDesk - File Transfer',
windowName,
);
break;
case WindowType.PortForward:
@ -75,7 +78,7 @@ Future<void> main(List<String> args) async {
runMultiWindow(
argument,
kAppTypeDesktopPortForward,
'RustDesk - Port Forward',
windowName,
);
break;
default:
@ -117,6 +120,7 @@ void runMainApp(bool startService) async {
// await windowManager.ensureInitialized();
gFFI.serverModel.startService();
}
gFFI.userModel.refreshCurrentUser();
runApp(App());
// restore the location of the main window before window hide or show
await restoreWindowPosition(WindowType.Main);
@ -134,6 +138,7 @@ void runMainApp(bool startService) async {
windowManager.waitUntilReadyToShow(windowOptions, () async {
windowManager.setOpacity(1);
});
windowManager.setTitle(getWindowName());
}
void runMobileApp() async {
@ -149,7 +154,7 @@ void runMultiWindow(
) async {
await initEnv(appType);
// set prevent close to true, we handle close event manually
WindowController.fromWindowId(windowId!).setPreventClose(true);
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
late Widget widget;
switch (appType) {
case kAppTypeDesktopRemote:
@ -178,23 +183,26 @@ void runMultiWindow(
);
// we do not hide titlebar on win7 because of the frame overflow.
if (kUseCompatibleUiMode) {
WindowController.fromWindowId(windowId!).showTitleBar(true);
WindowController.fromWindowId(kWindowId!).showTitleBar(true);
}
switch (appType) {
case kAppTypeDesktopRemote:
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId!);
windowId: kWindowId!);
break;
case kAppTypeDesktopFileTransfer:
await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!);
await restoreWindowPosition(WindowType.FileTransfer,
windowId: kWindowId!);
break;
case kAppTypeDesktopPortForward:
await restoreWindowPosition(WindowType.PortForward, windowId: windowId!);
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
break;
default:
// no such appType
exit(0);
}
// show window from hidden status
WindowController.fromWindowId(kWindowId!).show();
}
void runConnectionManagerScreen(bool hide) async {

View File

@ -7,9 +7,8 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/address_book.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
@ -75,20 +74,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
])),
SliverFillRemaining(
hasScrollBody: false,
child: PeerTabPage(
tabs: [
translate('Recent Sessions'),
translate('Favorites'),
translate('Discovered'),
translate('Address Book')
],
children: [
RecentPeersView(),
FavoritePeersView(),
DiscoveredPeersView(),
const AddressBook(),
],
),
child: PeerTabPage(),
)
],
).marginOnly(top: 2, left: 10, right: 10);
@ -271,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager);
loginDialog();
} else {
gFFI.userModel.logOut();
}

View File

@ -32,15 +32,17 @@ class _FileManagerPageState extends State<FileManagerPage> {
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
gFFI.ffiModel.updateEventListener(widget.id);
model.onDirChanged = (_) => breadCrumbScrollToEnd();
Wakelock.enable();
}
@override
void dispose() {
model.onClose();
gFFI.close();
gFFI.dialogManager.dismissAll();
Wakelock.disable();
model.onClose().whenComplete(() {
gFFI.close();
gFFI.dialogManager.dismissAll();
Wakelock.disable();
});
super.dispose();
}
@ -136,7 +138,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
child: Row(
children: [
Icon(
model.currentShowHidden
model.getCurrentShowHidden()
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: Theme.of(context).iconTheme.color),
@ -172,22 +174,18 @@ class _FileManagerPageState extends State<FileManagerPage> {
],
),
actions: [
TextButton(
style: flatButtonStyle,
dialogButton("Cancel",
onPressed: () => close(false),
child: Text(translate("Cancel"))),
ElevatedButton(
style: flatButtonStyle,
onPressed: () {
if (name.value.text.isNotEmpty) {
model.createDir(PathUtil.join(
model.currentDir.path,
name.value.text,
model.currentIsWindows));
close();
}
},
child: Text(translate("OK")))
isOutline: true),
dialogButton("OK", onPressed: () {
if (name.value.text.isNotEmpty) {
model.createDir(PathUtil.join(
model.currentDir.path,
name.value.text,
model.getCurrentIsWindows()));
close();
}
})
]));
} else if (v == "hidden") {
model.toggleShowHidden();
@ -309,7 +307,6 @@ class _FileManagerPageState extends State<FileManagerPage> {
}
if (entries[index].isDirectory || entries[index].isDrive) {
model.openDirectory(entries[index].path);
breadCrumbScrollToEnd();
} else {
// Perform file-related tasks.
}
@ -333,10 +330,12 @@ class _FileManagerPageState extends State<FileManagerPage> {
breadCrumbScrollToEnd() {
Future.delayed(Duration(milliseconds: 200), () {
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
if (_breadCrumbScroller.hasClients) {
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
}
});
}
@ -350,12 +349,12 @@ class _FileManagerPageState extends State<FileManagerPage> {
if (model.currentHome.startsWith(list[0])) {
// absolute path
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
path = PathUtil.join(path, item, model.getCurrentIsWindows());
}
} else {
path += model.currentHome;
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
path = PathUtil.join(path, item, model.getCurrentIsWindows());
}
}
model.openDirectory(path);
@ -477,7 +476,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
return BottomSheetBody(
leading: Icon(Icons.check),
title: "${translate("Successful")}!",
text: "",
text: model.jobProgress.display(),
onCanceled: () => model.jobReset(),
);
case JobState.error:
@ -499,7 +498,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
List<BreadCrumbItem> getPathBreadCrumbItems(
void Function() onHome, void Function(List<String>) onPressed) {
final path = model.currentShortPath;
final list = PathUtil.split(path, model.currentIsWindows);
final list = PathUtil.split(path, model.getCurrentIsWindows());
final breadCrumbList = [
BreadCrumbItem(
content: IconButton(

View File

@ -518,7 +518,7 @@ class _RemotePageState extends State<RemotePage> {
),
),
];
if (!gFFI.canvasModel.cursorEmbeded) {
if (!gFFI.canvasModel.cursorEmbedded) {
paints.add(CursorPaint());
}
return paints;
@ -527,7 +527,7 @@ class _RemotePageState extends State<RemotePage> {
Widget getBodyForDesktopWithListener(bool keyboard) {
var paints = <Widget>[ImagePaint()];
if (!gFFI.canvasModel.cursorEmbeded) {
if (!gFFI.canvasModel.cursorEmbedded) {
final cursor = bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-remote-cursor');
if (keyboard || cursor) {
@ -574,14 +574,14 @@ class _RemotePageState extends State<RemotePage> {
more.add(PopupMenuItem<String>(
child: Text(translate('Physical Keyboard Input Mode')),
value: 'input-mode'));
if (pi.platform == 'Linux' || pi.sasEnabled) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
more.add(PopupMenuItem<String>(
child: Text('${translate('Insert')} Ctrl + Alt + Del'),
value: 'cad'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Insert Lock')), value: 'lock'));
if (pi.platform == 'Windows' &&
if (pi.platform == kPeerPlatformWindows &&
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
true) {
more.add(PopupMenuItem<String>(
@ -591,9 +591,9 @@ class _RemotePageState extends State<RemotePage> {
}
}
if (perms["restart"] != false &&
(pi.platform == "Linux" ||
pi.platform == "Windows" ||
pi.platform == "Mac OS")) {
(pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) {
more.add(PopupMenuItem<String>(
child: Text(translate('Restart Remote Device')), value: 'restart'));
}
@ -692,10 +692,11 @@ class _RemotePageState extends State<RemotePage> {
}
void changePhysicalKeyboardInputMode() async {
var current = await bind.sessionGetKeyboardName(id: widget.id);
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, keyboardMode: v ?? '');
await bind.sessionPeerOption(
id: widget.id, name: "keyboard-mode", value: v ?? "");
setState(() => current = v ?? '');
Future.delayed(Duration(milliseconds: 300), close);
}
@ -739,7 +740,7 @@ class _RemotePageState extends State<RemotePage> {
}
final pi = gFFI.ffiModel.pi;
final isMac = pi.platform == "Mac OS";
final isMac = pi.platform == kPeerPlatformMacOS;
final modifiers = <Widget>[
wrap('Ctrl ', () {
setState(() => inputModel.ctrl = !inputModel.ctrl);
@ -977,7 +978,9 @@ void showOptions(
final h265 = codecsJson['h265'] ?? false;
codecs.add(h264);
codecs.add(h265);
} finally {}
} catch (e) {
debugPrint("Show Codec Preference err=$e");
}
}
dialogManager.show((setState, close) {
@ -992,7 +995,7 @@ void showOptions(
}
more.add(getToggle(
id, setState, 'lock-after-session-end', 'Lock after session end'));
if (pi.platform == 'Windows') {
if (pi.platform == kPeerPlatformWindows) {
more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
}
}
@ -1055,7 +1058,7 @@ void showOptions(
final toggles = [
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
];
if (!gFFI.canvasModel.cursorEmbeded) {
if (!gFFI.canvasModel.cursorEmbedded) {
toggles.insert(0,
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
}
@ -1095,15 +1098,9 @@ void showSetOSPassword(
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton(
'OK',
onPressed: () {
var text = controller.text.trim();
bind.sessionPeerOption(id: id, name: "os-password", value: text);
@ -1114,7 +1111,6 @@ void showSetOSPassword(
}
close();
},
child: Text(translate('OK')),
),
]);
});

View File

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:zxing2/qrcode.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
class ScanPage extends StatefulWidget {
@override
_ScanPageState createState() => _ScanPageState();
State<ScanPage> createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
@ -42,9 +41,9 @@ class _ScanPageState extends State<ScanPage> {
icon: Icon(Icons.image_search),
iconSize: 32.0,
onPressed: () async {
final ImagePicker _picker = ImagePicker();
final ImagePicker picker = ImagePicker();
final XFile? file =
await _picker.pickImage(source: ImageSource.gallery);
await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
var image = img.decodeNamedImage(
File(file.path).readAsBytesSync(), file.path)!;
@ -139,155 +138,12 @@ class _ScanPageState extends State<ScanPage> {
return;
}
try {
Map<String, dynamic> values = json.decode(data.substring(7));
var host = values['host'] != null ? values['host'] as String : '';
var key = values['key'] != null ? values['key'] as String : '';
var api = values['api'] != null ? values['api'] as String : '';
final sc = ServerConfig.decode(data.substring(7));
Timer(Duration(milliseconds: 60), () {
showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager);
showServerSettingsWithValue(sc, gFFI.dialogManager);
});
} catch (e) {
showToast('Invalid QR code');
}
}
}
void showServerSettingsWithValue(String id, String relay, String key,
String api, OverlayDialogManager dialogManager) async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
String id0 = oldOptions['custom-rendezvous-server'] ?? "";
String relay0 = oldOptions['relay-server'] ?? "";
String api0 = oldOptions['api-server'] ?? "";
String key0 = oldOptions['key'] ?? "";
var isInProgress = false;
final idController = TextEditingController(text: id);
final relayController = TextEditingController(text: relay);
final apiController = TextEditingController(text: api);
String? idServerMsg;
String? relayServerMsg;
String? apiServerMsg;
dialogManager.show((setState, close) {
Future<bool> validate() async {
if (idController.text != id) {
final res = await validateAsync(idController.text);
setState(() => idServerMsg = res);
if (idServerMsg != null) return false;
id = idController.text;
}
if (relayController.text != relay) {
relayServerMsg = await validateAsync(relayController.text);
if (relayServerMsg != null) return false;
relay = relayController.text;
}
if (apiController.text != relay) {
apiServerMsg = await validateAsync(apiController.text);
if (apiServerMsg != null) return false;
api = apiController.text;
}
return true;
}
return CustomAlertDialog(
title: Text(translate('ID/Relay Server')),
content: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: idController,
decoration: InputDecoration(
labelText: translate('ID Server'),
errorText: idServerMsg),
)
] +
(isAndroid
? [
TextFormField(
controller: relayController,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg),
)
]
: []) +
[
TextFormField(
controller: apiController,
decoration: InputDecoration(
labelText: translate('API Server'),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (v) {
if (v != null && v.length > 0) {
if (!(v.startsWith('http://') ||
v.startsWith("https://"))) {
return translate("invalid_http");
}
}
return apiServerMsg;
},
),
TextFormField(
initialValue: key,
decoration: InputDecoration(
labelText: 'Key',
),
onChanged: (String? value) {
if (value != null) key = value.trim();
},
),
Offstage(
offstage: !isInProgress,
child: LinearProgressIndicator())
])),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () async {
setState(() {
idServerMsg = null;
relayServerMsg = null;
apiServerMsg = null;
isInProgress = true;
});
if (await validate()) {
if (id != id0) {
bind.mainSetOption(key: "custom-rendezvous-server", value: id);
}
if (relay != relay0) {
bind.mainSetOption(key: "relay-server", value: relay);
}
if (key != key0) bind.mainSetOption(key: "key", value: key);
if (api != api0) {
bind.mainSetOption(key: "api-server", value: api);
}
close();
}
setState(() {
isInProgress = false;
});
},
child: Text(translate('OK')),
),
],
);
});
}
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:provider/provider.dart';
@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape {
}
class _ServerPageState extends State<ServerPage> {
Timer? _updateTimer;
@override
void initState() {
super.initState();
_updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
await gFFI.serverModel.fetchID();
});
gFFI.serverModel.checkAndroidPermission();
}
@override
void dispose() {
_updateTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
checkService();
@ -564,7 +577,7 @@ void androidChannelInit() {
}
}
} catch (e) {
debugPrint("MethodCallHandler err:$e");
debugPrintStack(label: "MethodCallHandler err:$e");
}
return "";
});

View File

@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
@ -272,13 +273,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
content: Text(translate(
"android_open_battery_optimizations_tip")),
actions: [
TextButton(
onPressed: () => close(),
child: Text(translate("Cancel"))),
ElevatedButton(
onPressed: () => close(true),
child:
Text(translate("Open System Setting"))),
dialogButton("Cancel",
onPressed: () => close(), isOutline: true),
dialogButton(
"Open System Setting",
onPressed: () => close(true),
),
],
));
if (res == true) {
@ -300,7 +300,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
leading: Icon(Icons.person),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager);
loginDialog();
} else {
gFFI.userModel.logOut();
}
@ -391,17 +391,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
void showServerSettings(OverlayDialogManager dialogManager) async {
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
String id = options['custom-rendezvous-server'] ?? "";
String relay = options['relay-server'] ?? "";
String api = options['api-server'] ?? "";
String key = options['key'] ?? "";
showServerSettingsWithValue(id, relay, key, api, dialogManager);
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
}
void showLanguageSettings(OverlayDialogManager dialogManager) async {
try {
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
var lang = await bind.mainGetLocalOption(key: "lang");
var lang = bind.mainGetLocalOption(key: "lang");
dialogManager.show((setState, close) {
setLang(v) {
if (lang != v) {
@ -486,78 +482,6 @@ void showAbout(OverlayDialogManager dialogManager) {
}, clickMaskDismiss: true, backDismiss: true);
}
void showLogin(OverlayDialogManager dialogManager) {
final passwordController = TextEditingController();
final nameController = TextEditingController();
var loading = false;
var error = '';
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('Login')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
autofocus: true,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: translate('Username'),
),
controller: nameController,
),
PasswordWidget(controller: passwordController, autoFocus: false),
]),
actions: (loading
? <Widget>[CircularProgressIndicator()]
: (error != ""
? <Widget>[
Text(translate(error),
style: TextStyle(color: Colors.red))
]
: <Widget>[])) +
<Widget>[
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () {
close();
setState(() {
loading = false;
});
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () async {
final name = nameController.text.trim();
final pass = passwordController.text.trim();
if (name != "" && pass != "") {
setState(() {
loading = true;
});
final resp = await gFFI.userModel.login(name, pass);
setState(() {
loading = false;
});
if (resp.containsKey('error')) {
error = resp['error'];
return;
}
gFFI.abModel.pullAb();
}
close();
},
child: Text(translate('OK')),
),
],
);
});
}
class ScanButton extends StatelessWidget {
@override
Widget build(BuildContext context) {

View File

@ -1,5 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/button.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../models/model.dart';
@ -31,10 +34,8 @@ void showRestartRemoteDevice(
content: Text(
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
actions: [
TextButton(
onPressed: () => close(), child: Text(translate("Cancel"))),
ElevatedButton(
onPressed: () => close(true), child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: () => close(), isOutline: true),
dialogButton("OK", onPressed: () => close(true)),
],
));
if (res == true) bind.sessionRestartRemoteDevice(id: id);
@ -94,15 +95,15 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
),
])),
actions: [
TextButton(
style: flatButtonStyle,
dialogButton(
'Cancel',
onPressed: () {
close();
},
child: Text(translate('Cancel')),
isOutline: true,
),
TextButton(
style: flatButtonStyle,
dialogButton(
'OK',
onPressed: (validateLength && validateSame)
? () async {
close();
@ -116,7 +117,6 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
}
}
: null,
child: Text(translate('OK')),
),
],
);
@ -196,16 +196,8 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: cancel,
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: submit,
child: Text(translate('OK')),
),
dialogButton('Cancel', onPressed: cancel, isOutline: true),
dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: cancel,
@ -218,24 +210,375 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) {
title: Text(translate('Wrong Password')),
content: Text(translate('Do you want to enter again?')),
actions: [
TextButton(
style: flatButtonStyle,
dialogButton(
'Cancel',
onPressed: () {
close();
closeConnection();
},
child: Text(translate('Cancel')),
isOutline: true,
),
TextButton(
style: flatButtonStyle,
dialogButton(
'Retry',
onPressed: () {
enterPasswordDialog(id, dialogManager);
},
child: Text(translate('Retry')),
),
]));
}
void showServerSettingsWithValue(
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
final oldCfg = ServerConfig.fromOptions(oldOptions);
var isInProgress = false;
final idCtrl = TextEditingController(text: serverConfig.idServer);
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
final apiCtrl = TextEditingController(text: serverConfig.apiServer);
final keyCtrl = TextEditingController(text: serverConfig.key);
String? idServerMsg;
String? relayServerMsg;
String? apiServerMsg;
dialogManager.show((setState, close) {
Future<bool> validate() async {
if (idCtrl.text != oldCfg.idServer) {
final res = await validateAsync(idCtrl.text);
setState(() => idServerMsg = res);
if (idServerMsg != null) return false;
}
if (relayCtrl.text != oldCfg.relayServer) {
relayServerMsg = await validateAsync(relayCtrl.text);
if (relayServerMsg != null) return false;
}
if (apiCtrl.text != oldCfg.apiServer) {
if (apiServerMsg != null) return false;
}
return true;
}
return CustomAlertDialog(
title: Text(translate('ID/Relay Server')),
content: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: idCtrl,
decoration: InputDecoration(
labelText: translate('ID Server'),
errorText: idServerMsg),
)
] +
(isAndroid
? [
TextFormField(
controller: relayCtrl,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg),
)
]
: []) +
[
TextFormField(
controller: apiCtrl,
decoration: InputDecoration(
labelText: translate('API Server'),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (v) {
if (v != null && v.isNotEmpty) {
if (!(v.startsWith('http://') ||
v.startsWith("https://"))) {
return translate("invalid_http");
}
}
return apiServerMsg;
},
),
TextFormField(
controller: keyCtrl,
decoration: InputDecoration(
labelText: 'Key',
),
),
Offstage(
offstage: !isInProgress,
child: LinearProgressIndicator())
])),
actions: [
dialogButton('Cancel', onPressed: () {
close();
}, isOutline: true),
dialogButton(
'OK',
onPressed: () async {
setState(() {
idServerMsg = null;
relayServerMsg = null;
apiServerMsg = null;
isInProgress = true;
});
if (await validate()) {
if (idCtrl.text != oldCfg.idServer) {
if (oldCfg.idServer.isNotEmpty) {
await gFFI.userModel.logOut();
}
bind.mainSetOption(
key: "custom-rendezvous-server", value: idCtrl.text);
}
if (relayCtrl.text != oldCfg.relayServer) {
bind.mainSetOption(key: "relay-server", value: relayCtrl.text);
}
if (keyCtrl.text != oldCfg.key) {
bind.mainSetOption(key: "key", value: keyCtrl.text);
}
if (apiCtrl.text != oldCfg.apiServer) {
bind.mainSetOption(key: "api-server", value: apiCtrl.text);
}
close();
showToast(translate('Successful'));
}
setState(() {
isInProgress = false;
});
},
),
],
);
});
}
void showWaitUacDialog(String id, OverlayDialogManager dialogManager) {
dialogManager.dismissAll();
dialogManager.show(
tag: '$id-wait-uac',
(setState, close) => CustomAlertDialog(
title: Text(translate('Wait')),
content: Text(translate('wait_accept_uac_tip')).marginAll(10),
));
}
void _showRequestElevationDialog(
String id, OverlayDialogManager dialogManager) {
RxString groupValue = ''.obs;
RxString errUser = ''.obs;
RxString errPwd = ''.obs;
TextEditingController userController = TextEditingController();
TextEditingController pwdController = TextEditingController();
void onRadioChanged(String? value) {
if (value != null) {
groupValue.value = value;
}
}
const minTextStyle = TextStyle(fontSize: 14);
var content = Obx(() => Column(children: [
Row(
children: [
Radio(
value: '',
groupValue: groupValue.value,
onChanged: onRadioChanged),
Expanded(
child:
Text(translate('Ask the remote user for authentication'))),
],
),
Align(
alignment: Alignment.centerLeft,
child: Text(
translate(
'Choose this if the remote account is administrator'),
style: TextStyle(fontSize: 13))
.marginOnly(left: 40),
).marginOnly(bottom: 15),
Row(
children: [
Radio(
value: 'logon',
groupValue: groupValue.value,
onChanged: onRadioChanged),
Expanded(
child: Text(translate(
'Transmit the username and password of administrator')),
)
],
),
Row(
children: [
Expanded(
flex: 1,
child: Text(
'${translate('Username')}:',
style: minTextStyle,
).marginOnly(right: 10)),
Expanded(
flex: 3,
child: TextField(
controller: userController,
style: minTextStyle,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 15),
hintText: 'eg: admin',
errorText: errUser.isEmpty ? null : errUser.value),
onChanged: (s) {
if (s.isNotEmpty) {
errUser.value = '';
}
},
),
)
],
).marginOnly(left: 40),
Row(
children: [
Expanded(
flex: 1,
child: Text(
'${translate('Password')}:',
style: minTextStyle,
).marginOnly(right: 10)),
Expanded(
flex: 3,
child: TextField(
controller: pwdController,
obscureText: true,
style: minTextStyle,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 15),
errorText: errPwd.isEmpty ? null : errPwd.value),
onChanged: (s) {
if (s.isNotEmpty) {
errPwd.value = '';
}
},
),
),
],
).marginOnly(left: 40),
Align(
alignment: Alignment.centerLeft,
child: Text(translate('still_click_uac_tip'),
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold))
.marginOnly(top: 20)),
]));
dialogManager.dismissAll();
dialogManager.show(tag: '$id-request-elevation', (setState, close) {
void submit() {
if (groupValue.value == 'logon') {
if (userController.text.isEmpty) {
errUser.value = translate('Empty Username');
return;
}
if (pwdController.text.isEmpty) {
errPwd.value = translate('Empty Password');
return;
}
bind.sessionElevateWithLogon(
id: id,
username: userController.text,
password: pwdController.text);
} else {
bind.sessionElevateDirect(id: id);
}
}
return CustomAlertDialog(
title: Text(translate('Request Elevation')),
content: content,
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void showOnBlockDialog(
String id,
String type,
String title,
String text,
OverlayDialogManager dialogManager,
) {
if (dialogManager.existing('$id-wait-uac') ||
dialogManager.existing('$id-request-elevation')) {
return;
}
var content = Column(children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}",
textAlign: TextAlign.left,
style: TextStyle(fontWeight: FontWeight.w400),
).marginSymmetric(vertical: 15),
),
]);
dialogManager.show(tag: '$id-$type', (setState, close) {
void submit() {
close();
_showRequestElevationDialog(id, dialogManager);
}
return CustomAlertDialog(
title: Text(translate(title)),
content: content,
actions: [
dialogButton('Wait', onPressed: () {
close();
}, isOutline: true),
dialogButton('Request Elevation', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void showElevationError(String id, String type, String title, String text,
OverlayDialogManager dialogManager) {
dialogManager.show(tag: '$id-$type', (setState, close) {
void submit() {
close();
_showRequestElevationDialog(id, dialogManager);
}
return CustomAlertDialog(
title: Text(translate(title)),
content: Text(translate(text)),
actions: [
dialogButton('Cancel', onPressed: () {
close();
}, isOutline: true),
dialogButton('Retry', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}
class PasswordWidget extends StatefulWidget {
PasswordWidget({Key? key, required this.controller, this.autoFocus = true})
: super(key: key);
@ -285,7 +628,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
color: Theme.of(context).primaryColorDark,
),
onPressed: () {
// Update the state i.e. toogle the state of passwordVisible variable
// Update the state i.e. toggle the state of passwordVisible variable
setState(() {
_passwordVisible = !_passwordVisible;
});

View File

@ -21,26 +21,30 @@ class AbModel {
AbModel(this.parent);
FFI? get _ffi => parent.target;
Future<dynamic> pullAb() async {
if (_ffi!.userModel.userName.isEmpty) return;
if (gFFI.userModel.userName.isEmpty) return;
abLoading.value = true;
abError.value = "";
final api = "${await bind.mainGetApiServer()}/api/ab/get";
try {
final resp =
await http.post(Uri.parse(api), headers: await getHttpHeaders());
final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
abError = json['error'];
abError.value = json['error'];
} else if (json.containsKey('data')) {
final data = jsonDecode(json['data']);
tags.value = data['tags'];
peers.clear();
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
if (data != null) {
tags.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
}
}
return resp.body;
@ -56,16 +60,27 @@ class AbModel {
return null;
}
void reset() {
Future<void> reset() async {
await bind.mainSetLocalOption(key: "selected-tags", value: '');
tags.clear();
peers.clear();
}
void addId(String id) async {
void addId(String id, String alias, List<dynamic> tags) {
if (idContainBy(id)) {
return;
}
peers.add(Peer.fromJson({"id": id}));
final peer = Peer.fromJson({
'id': id,
'alias': alias,
'tags': tags,
});
peers.add(peer);
}
void addPeer(Peer peer) {
peers.removeWhere((e) => e.id == peer.id);
peers.add(peer);
}
void addTag(String tag) async {
@ -86,7 +101,7 @@ class AbModel {
Future<void> pushAb() async {
abLoading.value = true;
final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = await getHttpHeaders();
var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toJson()).toList();
final body = jsonEncode({
@ -105,6 +120,10 @@ class AbModel {
}
}
Peer? find(String id) {
return peers.firstWhereOrNull((e) => e.id == id);
}
bool idContainBy(String id) {
return peers.where((element) => element.id == id).isNotEmpty;
}
@ -143,18 +162,28 @@ class AbModel {
}
}
void setPeerAlias(String id, String value) {
Future<void> setPeerAlias(String id, String value) async {
final it = peers.where((p0) => p0.id == id);
if (it.isEmpty) {
debugPrint("$id is not exists");
return;
} else {
if (it.isNotEmpty) {
it.first.alias = value;
await pushAb();
}
}
void clear() {
peers.clear();
tags.clear();
Future<void> setPeerForceAlwaysRelay(String id, bool value) async {
final it = peers.where((p0) => p0.id == id);
if (it.isNotEmpty) {
it.first.forceAlwaysRelay = value;
await pushAb();
}
}
Future<void> setRdp(String id, String port, String username) async {
final it = peers.where((p0) => p0.id == id);
if (it.isNotEmpty) {
it.first.rdpPort = port;
it.first.rdpUsername = username;
await pushAb();
}
}
}

View File

@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
@ -42,6 +44,9 @@ class FileModel extends ChangeNotifier {
/// JobTable <jobId, JobProgress>
final _jobTable = List<JobProgress>.empty(growable: true).obs;
/// `isLocal` bool
Function(bool)? onDirChanged;
RxList<JobProgress> get jobTable => _jobTable;
bool get isLocal => _isSelectedLocal;
@ -141,18 +146,14 @@ class FileModel extends ChangeNotifier {
}
}
bool get currentShowHidden =>
_isSelectedLocal ? _localOption.showHidden : _remoteOption.showHidden;
bool getCurrentShowHidden(bool isLocal) {
return isLocal ? _localOption.showHidden : _remoteOption.showHidden;
bool getCurrentShowHidden([bool? isLocal]) {
final isLocal_ = isLocal ?? _isSelectedLocal;
return isLocal_ ? _localOption.showHidden : _remoteOption.showHidden;
}
bool get currentIsWindows =>
_isSelectedLocal ? _localOption.isWindows : _remoteOption.isWindows;
bool getCurrentIsWindows(bool isLocal) {
return isLocal ? _localOption.isWindows : _remoteOption.isWindows;
bool getCurrentIsWindows([bool? isLocal]) {
final isLocal_ = isLocal ?? _isSelectedLocal;
return isLocal_ ? _localOption.isWindows : _remoteOption.isWindows;
}
final _fileFetcher = FileFetcher();
@ -213,7 +214,6 @@ class FileModel extends ChangeNotifier {
}
receiveFileDir(Map<String, dynamic> evt) {
debugPrint("recv file dir:$evt");
if (evt['is_local'] == "false") {
// init remote home, the connection will automatic read remote home when established,
try {
@ -237,7 +237,9 @@ class FileModel extends ChangeNotifier {
debugPrint("init remote home:${fd.path}");
_currentRemoteDir = fd;
}
} finally {}
} catch (e) {
debugPrint("receiveFileDir err=$e");
}
}
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
notifyListeners();
@ -268,6 +270,7 @@ class FileModel extends ChangeNotifier {
}
jobError(Map<String, dynamic> evt) {
final err = evt['err'].toString();
if (!isDesktop) {
if (_jobResultListener.isListening) {
_jobResultListener.complete(evt);
@ -275,12 +278,24 @@ class FileModel extends ChangeNotifier {
}
_selectMode = false;
_jobProgress.clear();
_jobProgress.err = err;
_jobProgress.state = JobState.error;
_jobProgress.fileNum = int.parse(evt['file_num']);
if (err == "skipped") {
_jobProgress.state = JobState.done;
_jobProgress.finishedSize = _jobProgress.totalSize;
}
} else {
int jobIndex = getJob(int.parse(evt['id']));
if (jobIndex != -1) {
final job = jobTable[jobIndex];
job.state = JobState.error;
job.err = err;
job.fileNum = int.parse(evt['file_num']);
if (err == "skipped") {
job.state = JobState.done;
job.finishedSize = job.totalSize;
}
}
}
debugPrint("jobError $evt");
@ -327,13 +342,13 @@ class FileModel extends ChangeNotifier {
_localOption.showHidden = (await bind.sessionGetPeerOption(
id: parent.target?.id ?? "", name: "local_show_hidden"))
.isNotEmpty;
_localOption.isWindows = Platform.isWindows;
_remoteOption.showHidden = (await bind.sessionGetPeerOption(
id: parent.target?.id ?? "", name: "remote_show_hidden"))
.isNotEmpty;
_remoteOption.isWindows = parent.target?.ffiModel.pi.platform == "Windows";
debugPrint("remote platform: ${parent.target?.ffiModel.pi.platform}");
_remoteOption.isWindows =
parent.target?.ffiModel.pi.platform == kPeerPlatformWindows;
await Future.delayed(Duration(milliseconds: 100));
@ -350,14 +365,14 @@ class FileModel extends ChangeNotifier {
if (_currentRemoteDir.path.isEmpty) {
openDirectory(_remoteOption.home, isLocal: false);
}
// load last transfer jobs
await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}');
}
onClose() {
Future<void> onClose() async {
parent.target?.dialogManager.dismissAll();
jobReset();
onDirChanged = null;
// save config
Map<String, String> msgMap = {};
@ -367,7 +382,7 @@ class FileModel extends ChangeNotifier {
msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : "";
final id = parent.target?.id ?? "";
for (final msg in msgMap.entries) {
bind.sessionPeerOption(id: id, name: msg.key, value: msg.value);
await bind.sessionPeerOption(id: id, name: msg.key, value: msg.value);
}
_currentLocalDir.clear();
_currentRemoteDir.clear();
@ -399,14 +414,10 @@ class FileModel extends ChangeNotifier {
if (!isBack) {
pushHistory(isLocal);
}
final showHidden =
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
final isWindows =
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
final showHidden = getCurrentShowHidden(isLocal);
final isWindows = getCurrentIsWindows(isLocal);
// process /C:\ -> C:\ on Windows
if (isLocal
? _localOption.isWindows
: _remoteOption.isWindows && path.length > 1 && path[0] == '/') {
if (isWindows && path.length > 1 && path[0] == '/') {
path = path.substring(1);
if (path[path.length - 1] != '\\') {
path = "$path\\";
@ -421,6 +432,7 @@ class FileModel extends ChangeNotifier {
_currentRemoteDir = fd;
}
notifyListeners();
onDirChanged?.call(isLocal);
} catch (e) {
debugPrint("Failed to openDirectory $path: $e");
}
@ -653,14 +665,8 @@ class FileModel extends ChangeNotifier {
: const SizedBox.shrink()
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: cancel,
child: Text(translate("Cancel"))),
TextButton(
style: flatButtonStyle,
onPressed: submit,
child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: cancel, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: cancel,
@ -712,18 +718,9 @@ class FileModel extends ChangeNotifier {
: const SizedBox.shrink()
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: cancel,
child: Text(translate("Cancel"))),
TextButton(
style: flatButtonStyle,
onPressed: () => close(null),
child: Text(translate("Skip"))),
TextButton(
style: flatButtonStyle,
onPressed: submit,
child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: cancel, isOutline: true),
dialogButton("Skip", onPressed: () => close(null), isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: cancel,
@ -1090,6 +1087,7 @@ class JobProgress {
var remote = "";
var to = "";
var showHidden = false;
var err = "";
clear() {
state = JobState.none;
@ -1101,6 +1099,14 @@ class JobProgress {
fileCount = 0;
remote = "";
to = "";
err = "";
}
String display() {
if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
return state.display();
}
}

View File

@ -0,0 +1,140 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class GroupModel {
final RxBool userLoading = false.obs;
final RxString userLoadError = "".obs;
final RxBool peerLoading = false.obs; //to-do: not used
final RxString peerLoadError = "".obs;
final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<PeerPayload> peerPayloads = RxList.empty(growable: true);
final RxList<Peer> peersShow = RxList.empty(growable: true);
WeakReference<FFI> parent;
GroupModel(this.parent);
Future<void> reset() async {
userLoading.value = false;
userLoadError.value = "";
peerLoading.value = false;
peerLoadError.value = "";
users.clear();
peerPayloads.clear();
peersShow.clear();
}
Future<void> pull() async {
await reset();
if (gFFI.userModel.userName.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
statePeerTab.check();
return;
}
userLoading.value = true;
userLoadError.value = "";
final api = "${await bind.mainGetApiServer()}/api/users";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
var total = 0;
int current = 0;
do {
current += 1;
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
if (gFFI.userModel.isAdmin.isFalse)
'grp': gFFI.userModel.groupName.value,
});
final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
throw json['error'];
} else {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final user in data) {
users.add(UserPayload.fromJson(user));
}
}
}
}
}
} while (current * pageSize < total);
} catch (err) {
debugPrint('$err');
userLoadError.value = err.toString();
} finally {
userLoading.value = false;
statePeerTab.check();
}
}
Future<void> pullUserPeers(String username) async {
peerPayloads.clear();
peersShow.clear();
peerLoading.value = true;
peerLoadError.value = "";
final api = "${await bind.mainGetApiServer()}/api/peers";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
var total = 0;
int current = 0;
do {
current += 1;
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
'grp': gFFI.userModel.groupName.value,
'target_user': username
});
final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
throw json['error'];
} else {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final p in data) {
final peer = PeerPayload.fromJson(p);
peerPayloads.add(peer);
peersShow.add(PeerPayload.toPeer(peer));
}
}
}
}
}
} while (current * pageSize < total);
} catch (err) {
debugPrint('$err');
peerLoadError.value = err.toString();
} finally {
peerLoading.value = false;
}
}
}

View File

@ -17,6 +17,10 @@ import './state_model.dart';
/// Mouse button enum.
enum MouseButtons { left, right, wheel }
const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
const _kMouseEventMove = 'mousemove';
extension ToString on MouseButtons {
String get value {
switch (this) {
@ -46,7 +50,7 @@ class InputModel {
// mouse
final isPhysicalMouse = false.obs;
int _lastMouseDownButtons = 0;
int _lastButtons = 0;
Offset lastMousePos = Offset.zero;
get id => parent.target?.id ?? "";
@ -54,7 +58,7 @@ class InputModel {
InputModel(this.parent);
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
bind.sessionGetKeyboardName(id: id).then((result) {
bind.sessionGetKeyboardMode(id: id).then((result) {
keyboardMode = result.toString();
});
@ -115,8 +119,11 @@ class InputModel {
keyCode = newData.keyCode;
} else if (e.data is RawKeyEventDataLinux) {
RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux;
scanCode = newData.scanCode;
keyCode = newData.keyCode;
// scanCode and keyCode of RawKeyEventDataLinux are incorrect.
// 1. scanCode means keycode
// 2. keyCode means keysym
scanCode = 0;
keyCode = newData.scanCode;
} else if (e.data is RawKeyEventDataAndroid) {
RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid;
scanCode = newData.scanCode + 8;
@ -131,16 +138,33 @@ class InputModel {
} else {
down = false;
}
inputRawKey(e.character ?? "", keyCode, scanCode, down);
inputRawKey(e.character ?? '', keyCode, scanCode, down);
}
/// Send raw Key Event
void inputRawKey(String name, int keyCode, int scanCode, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
bind.sessionHandleFlutterKeyEvent(
id: id,
name: name,
keycode: keyCode,
scancode: scanCode,
lockModes: lockModes,
downOrUp: down);
}
@ -183,20 +207,42 @@ class InputModel {
Map<String, dynamic> getEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
out['type'] = type;
out['x'] = evt.position.dx;
out['y'] = evt.position.dy;
if (alt) out['alt'] = 'true';
if (shift) out['shift'] = 'true';
if (ctrl) out['ctrl'] = 'true';
if (command) out['command'] = 'true';
out['buttons'] = evt
.buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right)
if (evt.buttons != 0) {
_lastMouseDownButtons = evt.buttons;
// Check update event type and set buttons to be sent.
int buttons = _lastButtons;
if (type == _kMouseEventMove) {
// flutter may emit move event if one button is pressed and another button
// is pressing or releasing.
if (evt.buttons != _lastButtons) {
// For simplicity
// Just consider 3 - 1 ((Left + Right buttons) - Left button)
// Do not consider 2 - 1 (Right button - Left button)
// or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons))
// and so on
buttons = evt.buttons - _lastButtons;
if (buttons > 0) {
type = _kMouseEventDown;
} else {
type = _kMouseEventUp;
buttons = -buttons;
}
}
} else {
out['buttons'] = _lastMouseDownButtons;
if (evt.buttons != 0) {
buttons = evt.buttons;
}
}
_lastButtons = evt.buttons;
out['buttons'] = buttons;
out['type'] = type;
return out;
}
@ -260,7 +306,7 @@ class InputModel {
isPhysicalMouse.value = true;
}
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mousemove'));
handleMouse(getEvent(e, _kMouseEventMove));
}
}
@ -325,21 +371,21 @@ class InputModel {
}
}
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mousedown'));
handleMouse(getEvent(e, _kMouseEventDown));
}
}
void onPointUpImage(PointerUpEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mouseup'));
handleMouse(getEvent(e, _kMouseEventUp));
}
}
void onPointMoveImage(PointerMoveEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mousemove'));
handleMouse(getEvent(e, _kMouseEventMove));
}
}
@ -388,13 +434,13 @@ class InputModel {
var type = '';
var isMove = false;
switch (evt['type']) {
case 'mousedown':
case _kMouseEventDown:
type = 'down';
break;
case 'mouseup':
case _kMouseEventUp:
type = 'up';
break;
case 'mousemove':
case _kMouseEventMove:
isMove = true;
break;
default:
@ -440,15 +486,21 @@ class InputModel {
evt['y'] = '${y.round()}';
var buttons = '';
switch (evt['buttons']) {
case 1:
case kPrimaryMouseButton:
buttons = 'left';
break;
case 2:
case kSecondaryMouseButton:
buttons = 'right';
break;
case 4:
case kMiddleMouseButton:
buttons = 'wheel';
break;
case kBackMouseButton:
buttons = 'back';
break;
case kForwardMouseButton:
buttons = 'forward';
break;
}
evt['buttons'] = buttons;
bind.sessionSendMouse(id: id, msg: json.encode(evt));

View File

@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@ -19,8 +20,9 @@ import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:tuple/tuple.dart';
import 'package:image/image.dart' as img2;
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import '../common.dart';
import '../common/shared_state.dart';
@ -59,7 +61,7 @@ class FfiModel with ChangeNotifier {
bool get touchMode => _touchMode;
bool get isPeerAndroid => _pi.platform == 'Android';
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
set inputBlocked(v) {
_inputBlocked = v;
@ -140,7 +142,7 @@ class FfiModel with ChangeNotifier {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
handleSwitchDisplay(evt, peerId);
} else if (name == 'cursor_data') {
await parent.target?.cursorModel.updateCursorData(evt);
} else if (name == 'cursor_id') {
@ -182,13 +184,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt, peerId);
} else if (name == 'new_connection') {
var arg = evt['peer_id'].toString();
if (arg.startsWith(kUniLinksPrefix)) {
parseRustdeskUri(arg);
} else {
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(arg);
});
var uni_links = evt['uni_links'].toString();
if (uni_links.startsWith(kUniLinksPrefix)) {
parseRustdeskUri(uni_links);
}
} else if (name == 'alias') {
handleAliasChanged(evt);
@ -197,6 +195,10 @@ class FfiModel with ChangeNotifier {
parent.target?.serverModel.setShowElevation(show);
} else if (name == 'cancel_msgbox') {
cancelMsgBox(evt, peerId);
} else if (name == 'switch_back') {
final peer_id = evt['peer_id'].toString();
await bind.sessionSwitchSides(id: peer_id);
closeConnection(id: peer_id);
}
};
}
@ -213,7 +215,7 @@ class FfiModel with ChangeNotifier {
}
}
handleSwitchDisplay(Map<String, dynamic> evt) {
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
final oldOrientation = _display.width > _display.height;
var old = _pi.currentDisplay;
_pi.currentDisplay = int.parse(evt['display']);
@ -221,15 +223,26 @@ class FfiModel with ChangeNotifier {
_display.y = double.parse(evt['y']);
_display.width = int.parse(evt['width']);
_display.height = int.parse(evt['height']);
_display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1;
_display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
if (old != _pi.currentDisplay) {
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
}
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
// remote is mobile, and orientation changed
if ((_display.width > _display.height) != oldOrientation) {
gFFI.canvasModel.updateViewStyle();
}
if (_pi.platform == kPeerPlatformLinux ||
_pi.platform == kPeerPlatformWindows ||
_pi.platform == kPeerPlatformMacOS) {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay();
notifyListeners();
}
@ -258,7 +271,13 @@ class FfiModel with ChangeNotifier {
hasCancel: false);
} else if (type == 'wait-remote-accept-nook') {
msgBoxCommon(dialogManager, title, Text(translate(text)),
[msgBoxButton("Cancel", closeConnection)]);
[dialogButton("Cancel", onPressed: closeConnection)]);
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
showOnBlockDialog(id, type, title, text, dialogManager);
} else if (type == 'wait-uac') {
showWaitUacDialog(id, dialogManager);
} else if (type == 'elevation-error') {
showElevationError(id, type, title, text, dialogManager);
} else {
var hasRetry = evt['hasRetry'] == 'true';
showMsgBox(id, type, title, text, link, hasRetry, dialogManager);
@ -331,7 +350,7 @@ class FfiModel with ChangeNotifier {
d.y = d0['y'].toDouble();
d.width = d0['width'];
d.height = d0['height'];
d.cursorEmbeded = d0['cursor_embeded'] == 1;
d.cursorEmbedded = d0['cursor_embedded'] == 1;
_pi.displays.add(d);
}
if (_pi.currentDisplay < _pi.displays.length) {
@ -344,6 +363,8 @@ class FfiModel with ChangeNotifier {
_waitForImage[peerId] = true;
_reconnects = 1;
}
Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1;
}
notifyListeners();
}
@ -378,12 +399,22 @@ class ImageModel with ChangeNotifier {
WeakReference<FFI> parent;
final List<Function(String)> _callbacksOnFirstImage = [];
ImageModel(this.parent);
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) {
cb(id);
}
}
}
final pid = parent.target?.id;
ui.decodeImageFromPixels(
@ -493,7 +524,7 @@ class ViewStyle {
double get scale {
double s = 1.0;
if (style == 'adaptive') {
if (style == kRemoteViewStyleAdaptive) {
final s1 = width / displayWidth;
final s2 = height / displayHeight;
s = s1 < s2 ? s1 : s2;
@ -509,6 +540,7 @@ class CanvasModel with ChangeNotifier {
double _y = 0;
// image scale
double _scale = 1.0;
Size _size = Size.zero;
// the tabbar over the image
// double tabBarHeight = 0.0;
// the window border's width
@ -522,6 +554,8 @@ class CanvasModel with ChangeNotifier {
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
ViewStyle _lastViewStyle = ViewStyle();
final _imageOverflow = false.obs;
WeakReference<FFI> parent;
CanvasModel(this.parent);
@ -529,7 +563,12 @@ class CanvasModel with ChangeNotifier {
double get x => _x;
double get y => _y;
double get scale => _scale;
Size get size => _size;
ScrollStyle get scrollStyle => _scrollStyle;
ViewStyle get viewStyle => _lastViewStyle;
RxBool get imageOverflow => _imageOverflow;
_resetScroll() => setScrollPercent(0.0, 0.0);
setScrollPercent(double x, double y) {
_scrollX = x;
@ -540,28 +579,44 @@ class CanvasModel with ChangeNotifier {
double get scrollY => _scrollY;
updateViewStyle() async {
Size getSize() {
final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here.
double w = size.width - windowBorderWidth * 2;
double h = size.height - tabBarHeight - windowBorderWidth * 2;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
final style = await bind.sessionGetViewStyle(id: id);
if (style == null) {
return;
}
final sizeWidth = size.width;
final sizeHeight = size.height;
_size = getSize();
final displayWidth = getDisplayWidth();
final displayHeight = getDisplayHeight();
final viewStyle = ViewStyle(
style: style,
width: sizeWidth,
height: sizeHeight,
width: size.width,
height: size.height,
displayWidth: displayWidth,
displayHeight: displayHeight,
);
if (_lastViewStyle == viewStyle) {
return;
}
if (_lastViewStyle.style != viewStyle.style) {
_resetScroll();
}
_lastViewStyle = viewStyle;
_scale = viewStyle.scale;
_x = (sizeWidth - displayWidth * _scale) / 2;
_y = (sizeHeight - displayHeight * _scale) / 2;
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / ui.window.devicePixelRatio;
}
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
_imageOverflow.value = _x < 0 || y < 0;
notifyListeners();
}
@ -569,8 +624,7 @@ class CanvasModel with ChangeNotifier {
final style = await bind.sessionGetScrollStyle(id: id);
if (style == kRemoteScrollStyleBar) {
_scrollStyle = ScrollStyle.scrollbar;
_scrollX = 0.0;
_scrollY = 0.0;
_resetScroll();
} else {
_scrollStyle = ScrollStyle.scrollauto;
}
@ -584,8 +638,8 @@ class CanvasModel with ChangeNotifier {
notifyListeners();
}
bool get cursorEmbeded =>
parent.target?.ffiModel.display.cursorEmbeded ?? false;
bool get cursorEmbedded =>
parent.target?.ffiModel.display.cursorEmbedded ?? false;
int getDisplayWidth() {
final defaultWidth = (isDesktop || isWebDesktop)
@ -604,14 +658,6 @@ class CanvasModel with ChangeNotifier {
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
double get tabBarHeight => stateGlobal.tabBarHeight;
Size get size {
final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here.
double w = size.width - windowBorderWidth * 2;
double h = size.height - tabBarHeight - windowBorderWidth * 2;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
moveDesktopMouse(double x, double y) {
// On mobile platforms, move the canvas with the cursor.
final dw = getDisplayWidth() * _scale;
@ -1111,7 +1157,8 @@ class CursorModel with ChangeNotifier {
_clearCache() {
final keys = {...cachedKeys};
for (var k in keys) {
customCursorController.freeCache(k);
debugPrint("deleting cursor with key $k");
CursorManager.instance.deleteCursor(k);
}
}
}
@ -1218,6 +1265,7 @@ class FFI {
late final ChatModel chatModel; // session
late final FileModel fileModel; // session
late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording
@ -1231,8 +1279,9 @@ class FFI {
serverModel = ServerModel(WeakReference(this));
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this));
@ -1240,7 +1289,9 @@ class FFI {
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
void start(String id,
{bool isFileTransfer = false, bool isPortForward = false}) {
{bool isFileTransfer = false,
bool isPortForward = false,
String? switchUuid}) {
assert(!(isFileTransfer && isPortForward), 'more than one connect type');
if (isFileTransfer) {
connType = ConnType.fileTransfer;
@ -1256,19 +1307,23 @@ class FFI {
}
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward);
id: id,
isFileTransfer: isFileTransfer,
isPortForward: isPortForward,
switchUuid: switchUuid ?? "",
);
final stream = bind.sessionStart(id: id);
final cb = ffiModel.startEventListener(id);
() async {
await for (final message in stream) {
if (message is Event) {
if (message is EventToUI_Event) {
try {
Map<String, dynamic> event = json.decode(message.field0);
await cb(event);
} catch (e) {
debugPrint('json.decode fail1(): $e, ${message.field0}');
}
} else if (message is Rgba) {
} else if (message is EventToUI_Rgba) {
imageModel.onRgba(message.field0);
}
}
@ -1316,7 +1371,7 @@ class Display {
double y = 0;
int width = 0;
int height = 0;
bool cursorEmbeded = false;
bool cursorEmbedded = false;
Display() {
width = (isDesktop || isWebDesktop)
@ -1328,6 +1383,10 @@ class Display {
}
}
class Features {
bool privacyMode = false;
}
class PeerInfo {
String version = '';
String username = '';
@ -1336,6 +1395,7 @@ class PeerInfo {
bool sasEnabled = false;
int currentDisplay = 0;
List<Display> displays = [];
Features features = Features();
}
const canvasKey = 'canvas';

View File

@ -29,6 +29,7 @@ typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
/// Hides the platform differences.
class PlatformFFI {
String _dir = '';
// _homeDir is only needed for Android and IOS.
String _homeDir = '';
F2? _translate;
final _eventHandlers = <String, Map<String, HandleEvent>>{};
@ -119,11 +120,13 @@ class PlatformFFI {
if (isAndroid) {
// only support for android
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
} else if (isIOS) {
_homeDir = _ffiBind.mainGetDataDirIos();
} else {
_homeDir = (await getDownloadsDirectory())?.path ?? '';
// no need to set home dir
}
} catch (e) {
debugPrint('initialize failed: $e');
debugPrintStack(label: 'initialize failed: $e');
}
String id = 'NA';
String name = 'Flutter';
@ -148,9 +151,8 @@ class PlatformFFI {
WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo;
name = winInfo.computerName;
id = winInfo.computerName;
} catch (e, stacktrace) {
debugPrint("get windows device info failed: $e");
debugPrintStack(stackTrace: stacktrace);
} catch (e) {
debugPrintStack(label: "get windows device info failed: $e");
name = "unknown";
id = "unknown";
}
@ -159,14 +161,19 @@ class PlatformFFI {
name = macOsInfo.computerName;
id = macOsInfo.systemGUID ?? '';
}
debugPrint(
'_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir');
if (isAndroid || isIOS) {
debugPrint(
'_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir');
} else {
debugPrint(
'_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir');
}
await _ffiBind.mainDeviceId(id: id);
await _ffiBind.mainDeviceName(name: name);
await _ffiBind.mainSetHomeDir(home: _homeDir);
await _ffiBind.mainInit(appDir: _dir);
} catch (e) {
debugPrint('initialize failed: $e');
debugPrintStack(label: 'initialize failed: $e');
}
version = await getVersion();
}

View File

@ -9,6 +9,9 @@ class Peer {
final String platform;
String alias;
List<dynamic> tags;
bool forceAlwaysRelay = false;
String rdpPort;
String rdpUsername;
bool online = false;
Peer.fromJson(Map<String, dynamic> json)
@ -17,7 +20,10 @@ class Peer {
hostname = json['hostname'] ?? '',
platform = json['platform'] ?? '',
alias = json['alias'] ?? '',
tags = json['tags'] ?? [];
tags = json['tags'] ?? [],
forceAlwaysRelay = json['forceAlwaysRelay'] == 'true',
rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? '';
Map<String, dynamic> toJson() {
return <String, dynamic>{
@ -27,6 +33,9 @@ class Peer {
"platform": platform,
"alias": alias,
"tags": tags,
"forceAlwaysRelay": forceAlwaysRelay.toString(),
"rdpPort": rdpPort,
"rdpUsername": rdpUsername,
};
}
@ -37,16 +46,23 @@ class Peer {
required this.platform,
required this.alias,
required this.tags,
required this.forceAlwaysRelay,
required this.rdpPort,
required this.rdpUsername,
});
Peer.loading()
: this(
id: '...',
username: '...',
hostname: '...',
platform: '...',
alias: '',
tags: []);
id: '...',
username: '...',
hostname: '...',
platform: '...',
alias: '',
tags: [],
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
);
}
class Peers extends ChangeNotifier {

View File

@ -304,8 +304,8 @@ class ServerModel with ChangeNotifier {
]),
content: Text(translate("android_service_will_start_tip")),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@ -501,8 +501,8 @@ class ServerModel with ChangeNotifier {
],
),
actions: [
TextButton(onPressed: cancel, child: Text(translate("Dismiss"))),
ElevatedButton(onPressed: submit, child: Text(translate("Accept"))),
dialogButton("Dismiss", onPressed: cancel, isOutline: true),
dialogButton("Accept", onPressed: submit),
],
onSubmit: submit,
onCancel: cancel,
@ -581,10 +581,17 @@ class ServerModel with ChangeNotifier {
}
}
enum ClientType {
remote,
file,
portForward,
}
class Client {
int id = 0; // client connections inner count id
bool authorized = false;
bool isFileTransfer = false;
String portForward = "";
String name = "";
String peerId = ""; // peer user's id,show at app
bool keyboard = false;
@ -594,6 +601,7 @@ class Client {
bool restart = false;
bool recording = false;
bool disconnected = false;
bool fromSwitch = false;
RxBool hasUnreadChatMessage = false.obs;
@ -604,6 +612,7 @@ class Client {
id = json['id'];
authorized = json['authorized'];
isFileTransfer = json['is_file_transfer'];
portForward = json['port_forward'];
name = json['name'];
peerId = json['peer_id'];
keyboard = json['keyboard'];
@ -613,6 +622,7 @@ class Client {
restart = json['restart'];
recording = json['recording'];
disconnected = json['disconnected'];
fromSwitch = json['from_switch'];
}
Map<String, dynamic> toJson() {
@ -620,6 +630,7 @@ class Client {
data['id'] = id;
data['is_start'] = authorized;
data['is_file_transfer'] = isFileTransfer;
data['port_forward'] = portForward;
data['name'] = name;
data['peer_id'] = peerId;
data['keyboard'] = keyboard;
@ -629,8 +640,19 @@ class Client {
data['restart'] = restart;
data['recording'] = recording;
data['disconnected'] = disconnected;
data['from_switch'] = fromSwitch;
return data;
}
ClientType type_() {
if (isFileTransfer) {
return ClientType.file;
} else if (portForward.isNotEmpty) {
return ClientType.portForward;
} else {
return ClientType.remote;
}
}
}
String getLoginDialogTag(int id) {
@ -655,9 +677,8 @@ showInputWarnAlert(FFI ffi) {
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(
onPressed: submit, child: Text(translate("Open System Setting"))),
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("Open System Setting", onPressed: submit),
],
onSubmit: submit,
onCancel: close,

View File

@ -9,6 +9,7 @@ import '../consts.dart';
class StateGlobal {
int _windowId = -1;
bool _fullscreen = false;
bool grabKeyboard = false;
final RxBool _showTabBar = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);

View File

@ -1,7 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
@ -10,17 +11,19 @@ import 'model.dart';
import 'platform_model.dart';
class UserModel {
var userName = ''.obs;
final RxString userName = ''.obs;
final RxString groupName = ''.obs;
final RxBool isAdmin = false.obs;
WeakReference<FFI> parent;
UserModel(this.parent) {
refreshCurrentUser();
}
UserModel(this.parent);
void refreshCurrentUser() async {
await getUserName();
final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') return;
if (token == '') {
await _updateOtherModels();
return;
}
final url = await bind.mainGetApiServer();
final body = {
'id': await bind.mainGetMyId(),
@ -35,96 +38,95 @@ class UserModel {
body: json.encode(body));
final status = response.statusCode;
if (status == 401 || status == 400) {
resetToken();
reset();
return;
}
await _parseResp(response.body);
final data = json.decode(response.body);
final error = data['error'];
if (error != null) {
throw error;
}
final user = UserPayload.fromJson(data);
await _parseAndUpdateUser(user);
} catch (e) {
print('Failed to refreshCurrentUser: $e');
} finally {
await _updateOtherModels();
}
}
void resetToken() async {
Future<void> reset() async {
await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
await gFFI.abModel.reset();
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
statePeerTab.check();
}
Future<String> _parseResp(String body) async {
final data = json.decode(body);
final error = data['error'];
if (error != null) {
return error!;
}
final token = data['access_token'];
if (token != null) {
await bind.mainSetLocalOption(key: 'access_token', value: token);
}
final info = data['user'];
if (info != null) {
final value = json.encode(info);
await bind.mainSetOption(key: 'user_info', value: value);
userName.value = info['name'];
}
return '';
Future<void> _parseAndUpdateUser(UserPayload user) async {
userName.value = user.name;
groupName.value = user.grp;
isAdmin.value = user.isAdmin;
}
Future<String> getUserName() async {
if (userName.isNotEmpty) {
return userName.value;
}
final userInfo = bind.mainGetLocalOption(key: 'user_info');
if (userInfo.trim().isEmpty) {
return '';
}
final m = jsonDecode(userInfo);
if (m == null) {
userName.value = '';
} else {
userName.value = m['name'] ?? '';
}
return userName.value;
Future<void> _updateOtherModels() async {
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
}
Future<void> logOut() async {
final tag = gFFI.dialogManager.showLoading(translate('Waiting'));
final url = await bind.mainGetApiServer();
final _ = await http.post(Uri.parse('$url/api/logout'),
body: {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(),
},
headers: await getHttpHeaders());
await Future.wait([
bind.mainSetLocalOption(key: 'access_token', value: ''),
bind.mainSetLocalOption(key: 'user_info', value: ''),
bind.mainSetLocalOption(key: 'selected-tags', value: ''),
]);
parent.target?.abModel.clear();
userName.value = '';
gFFI.dialogManager.dismissByTag(tag);
}
Future<Map<String, dynamic>> login(String userName, String pass) async {
final url = await bind.mainGetApiServer();
try {
final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': userName,
'password': pass,
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid()
}));
final body = jsonDecode(resp.body);
bind.mainSetLocalOption(
key: 'access_token', value: body['access_token'] ?? '');
bind.mainSetLocalOption(
key: 'user_info', value: jsonEncode(body['user']));
this.userName.value = body['user']?['name'] ?? '';
return body;
} catch (err) {
return {'error': '$err'};
final url = await bind.mainGetApiServer();
await http
.post(Uri.parse('$url/api/logout'),
body: {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(),
},
headers: getHttpHeaders())
.timeout(Duration(seconds: 2));
} catch (e) {
print("request /api/logout failed: err=$e");
} finally {
await reset();
gFFI.dialogManager.dismissByTag(tag);
}
}
/// throw [RequestException]
Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer();
final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(loginRequest.toJson()));
final Map<String, dynamic> body;
try {
body = jsonDecode(resp.body);
} catch (e) {
print("jsonDecode resp body failed: ${e.toString()}");
rethrow;
}
if (resp.statusCode != 200) {
throw RequestException(resp.statusCode, body['error'] ?? '');
}
final LoginResponse loginResponse;
try {
loginResponse = LoginResponse.fromJson(body);
} catch (e) {
print("jsonDecode LoginResponse failed: ${e.toString()}");
rethrow;
}
if (loginResponse.user != null) {
await _parseAndUpdateUser(loginResponse.user!);
}
return loginResponse;
}
}

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
@ -34,14 +35,18 @@ class RustDeskMultiWindowManager {
static final instance = RustDeskMultiWindowManager._();
final List<int> _activeWindows = List.empty(growable: true);
final List<VoidCallback> _windowActiveCallbacks = List.empty(growable: true);
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
int? _remoteDesktopWindowId;
int? _fileTransferWindowId;
int? _portForwardWindowId;
Future<dynamic> newRemoteDesktop(String remoteId) async {
final msg =
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
Future<dynamic> newRemoteDesktop(String remoteId,
{String? switch_uuid}) async {
final msg = jsonEncode({
"type": WindowType.RemoteDesktop.index,
"id": remoteId,
"switch_uuid": switch_uuid ?? ""
});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
@ -57,7 +62,8 @@ class RustDeskMultiWindowManager {
remoteDesktopController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle("rustdesk - remote desktop")
..setTitle(getWindowNameWithId(remoteId,
overrideType: WindowType.RemoteDesktop))
..show();
registerActiveWindow(remoteDesktopController.windowId);
_remoteDesktopWindowId = remoteDesktopController.windowId;
@ -83,7 +89,8 @@ class RustDeskMultiWindowManager {
fileTransferController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle("rustdesk - file transfer")
..setTitle(getWindowNameWithId(remoteId,
overrideType: WindowType.FileTransfer))
..show();
registerActiveWindow(fileTransferController.windowId);
_fileTransferWindowId = fileTransferController.windowId;
@ -109,7 +116,8 @@ class RustDeskMultiWindowManager {
portForwardController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle("rustdesk - port forward")
..setTitle(
getWindowNameWithId(remoteId, overrideType: WindowType.PortForward))
..show();
registerActiveWindow(portForwardController.windowId);
_portForwardWindowId = portForwardController.windowId;
@ -191,41 +199,41 @@ class RustDeskMultiWindowManager {
return _activeWindows;
}
void _notifyActiveWindow() {
Future<void> _notifyActiveWindow() async {
for (final callback in _windowActiveCallbacks) {
callback.call();
await callback.call();
}
}
void registerActiveWindow(int windowId) {
Future<void> registerActiveWindow(int windowId) async {
if (_activeWindows.contains(windowId)) {
// ignore
} else {
_activeWindows.add(windowId);
}
_notifyActiveWindow();
await _notifyActiveWindow();
}
/// Remove active window which has [`windowId`]
///
/// [Avaliability]
///
/// [Availability]
/// This function should only be called from main window.
/// For other windows, please post a unregister(hide) event to main window handler:
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
void unregisterActiveWindow(int windowId) {
Future<void> unregisterActiveWindow(int windowId) async {
if (!_activeWindows.contains(windowId)) {
// ignore
} else {
_activeWindows.remove(windowId);
}
_notifyActiveWindow();
await _notifyActiveWindow();
}
void registerActiveWindowListener(VoidCallback callback) {
void registerActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.add(callback);
}
void unregisterActiveWindowListener(VoidCallback callback) {
void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback);
}
}

View File

@ -1,32 +0,0 @@
import 'dart:io';
import 'package:tray_manager/tray_manager.dart';
import '../common.dart';
const kTrayItemShowKey = "show";
const kTrayItemQuitKey = "quit";
Future<void> initTray({List<MenuItem>? extra_item}) async {
List<MenuItem> items = [
MenuItem(key: kTrayItemShowKey, label: translate("Show RustDesk")),
MenuItem.separator(),
MenuItem(key: kTrayItemQuitKey, label: translate("Quit")),
];
if (extra_item != null) {
items.insertAll(0, extra_item);
}
if (Platform.isMacOS || Platform.isWindows) {
await trayManager.setToolTip("rustdesk");
}
if (Platform.isMacOS || Platform.isLinux) {
await trayManager.setTitle("rustdesk");
}
await trayManager
.setIcon(Platform.isWindows ? "assets/logo.ico" : "assets/logo.png");
await trayManager.setContextMenu(Menu(items: items));
}
Future<void> destoryTray() async {
return trayManager.destroy();
}

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