fix conflicts
954
.github/workflows/flutter-ci.yml
vendored
260
.github/workflows/flutter-nightly.yml
vendored
@ -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
@ -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
20
Cargo.toml
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
85
appimage/AppImageBuilder-aarch64.yml
Normal 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
|
88
appimage/AppImageBuilder-x86_64.yml
Normal 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
|
67
build.py
@ -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:
|
||||
|
39
build.rs
@ -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");
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
‫<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)
|
||||
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی.
|
||||
راستدسک (RustDesk) نرمافزاری برای گارکردن با رایانهی رومیزی از راه دور است و با زبان برنامهنویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آنها کنترل کامل داشته باشید.
|
||||
|
||||
میتوانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راهاندازی کنید](https://rustdesk.com/server) یا
|
||||
[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk).
|
||||
|
||||
‫راست دسک (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 منتقل خواهیم کرد.
|
||||
|
||||
## مراحل بنیادین برای ساخت
|
||||
## نیازمندیهای ساخت
|
||||
|
||||
‫- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید
|
||||
- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید
|
||||
|
||||
‫- نرم افزار [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
|
||||
|
||||
## اسکرین شات ها
|
||||
## تصاویر محیط نرمافزار
|
||||
|
||||

|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 318 KiB |
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 422 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 623 KiB After Width: | Height: | Size: 452 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 379 KiB |
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 267 KiB |
11
fastlane/metadata/android/fr-FR/full_description.txt
Normal 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.
|
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
Une application de bureau à distance open source, l'alternative open source à TeamViewer.
|
1
flutter/.gitignore
vendored
@ -54,3 +54,4 @@ lib/generated_bridge.freezed.dart
|
||||
flutter_export_environment.sh
|
||||
Flutter-Generated.xcconfig
|
||||
key.jks
|
||||
macos/rustdesk.xcodeproj/project.xcworkspace/
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 3.7 KiB |
1
flutter/assets/kb_layout_iso.svg
Normal 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 |
1
flutter/assets/kb_layout_not_iso.svg
Normal 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 |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.4 KiB |
@ -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
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 360 B |
Before Width: | Height: | Size: 574 B After Width: | Height: | Size: 564 B |
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 779 B |
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 455 B |
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 781 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 574 B After Width: | Height: | Size: 564 B |
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 978 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 939 B After Width: | Height: | Size: 926 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
@ -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)}";
|
||||
}
|
||||
|
119
flutter/lib/common/hbbs/hbbs.dart
Normal 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";
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
121
flutter/lib/common/widgets/custom_password.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
|
676
flutter/lib/common/widgets/login.dart
Normal 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;
|
||||
}
|
225
flutter/lib/common/widgets/my_group.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
/*
|
||||
|
@ -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>{
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -31,7 +31,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
initialPage: initialPage,
|
||||
)));
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
debugPrintStack(label: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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'],
|
||||
|
@ -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(
|
||||
|
@ -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'],
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
225
flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
}
|
75
flutter/lib/desktop/widgets/list_search_action_listener.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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()),
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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')),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 "";
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
140
flutter/lib/models/group_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|