Merge branch 'rustdesk:master' into master
107
.github/workflows/flutter-nightly.yml
vendored
@ -47,8 +47,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
flutter doctor -v
|
flutter doctor -v
|
||||||
flutter precache --windows
|
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
|
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-release-flutter.zip -DestinationPath engine
|
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/
|
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
@ -142,13 +142,42 @@ jobs:
|
|||||||
job:
|
job:
|
||||||
- {
|
- {
|
||||||
target: x86_64-apple-darwin,
|
target: x86_64-apple-darwin,
|
||||||
os: macos-10.15,
|
os: macos-latest,
|
||||||
extra-build-args: "",
|
extra-build-args: "",
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Import the codesign cert
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
security default-keychain -s rustdesk.keychain
|
||||||
|
security find-identity -v
|
||||||
|
|
||||||
|
- name: Import notarize key
|
||||||
|
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
|
||||||
|
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
|
- name: Install build runtime
|
||||||
run: |
|
run: |
|
||||||
brew install llvm create-dmg nasm yasm cmake gcc wget ninja
|
brew install llvm create-dmg nasm yasm cmake gcc wget ninja
|
||||||
@ -158,7 +187,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
@ -177,8 +205,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
dart pub global activate ffigen --version 5.0.1
|
dart pub global activate ffigen --version 5.0.1
|
||||||
# flutter_rust_bridge
|
# flutter_rust_bridge
|
||||||
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
|
pushd /tmp
|
||||||
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
|
wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz
|
||||||
|
tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz
|
||||||
|
mkdir -p ~/.cargo/bin
|
||||||
|
mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen
|
||||||
|
popd
|
||||||
pushd flutter && flutter pub get && popd
|
pushd flutter && flutter pub get && popd
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
@ -192,10 +224,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
|
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
|
||||||
|
|
||||||
- name: Install cargo bundle tools
|
|
||||||
run: |
|
|
||||||
cargo install cargo-bundle
|
|
||||||
|
|
||||||
- name: Show version information (Rust, cargo, Clang)
|
- name: Show version information (Rust, cargo, Clang)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@ -211,6 +239,18 @@ jobs:
|
|||||||
# --hwcodec not supported on macos yet
|
# --hwcodec not supported on macos yet
|
||||||
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
||||||
|
|
||||||
|
- name: Codesign app and create signed dmg
|
||||||
|
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
|
||||||
|
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
|
- name: Rename rustdesk
|
||||||
run: |
|
run: |
|
||||||
for name in rustdesk*??.dmg; do
|
for name in rustdesk*??.dmg; do
|
||||||
@ -377,7 +417,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
- name: Upload Artifcat
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@master
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
@ -559,6 +599,12 @@ jobs:
|
|||||||
os: ubuntu-20.04,
|
os: ubuntu-20.04,
|
||||||
extra-build-features: "flatpak",
|
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 }
|
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
steps:
|
steps:
|
||||||
- name: Maximize build space
|
- name: Maximize build space
|
||||||
@ -1012,7 +1058,7 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
|
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
|
||||||
|
|
||||||
- name: Upload Artifcat
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@master
|
||||||
if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }}
|
if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }}
|
||||||
with:
|
with:
|
||||||
@ -1108,6 +1154,12 @@ jobs:
|
|||||||
os: ubuntu-18.04,
|
os: ubuntu-18.04,
|
||||||
extra-build-features: "flatpak",
|
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 }
|
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
@ -1122,7 +1174,7 @@ jobs:
|
|||||||
- name: Prepare env
|
- name: Prepare env
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -y
|
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/
|
mkdir -p ./target/release/
|
||||||
|
|
||||||
- name: Restore the rustdesk lib file
|
- name: Restore the rustdesk lib file
|
||||||
@ -1177,10 +1229,12 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
for name in rustdesk*??.deb; do
|
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
|
done
|
||||||
|
|
||||||
- name: Publish debian package
|
- name: Publish debian package
|
||||||
|
if: ${{ matrix.job.extra-build-features == '' }}
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@ -1188,7 +1242,7 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
|
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
|
||||||
|
|
||||||
- name: Upload Artifcat
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@master
|
||||||
if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }}
|
if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }}
|
||||||
with:
|
with:
|
||||||
@ -1244,6 +1298,29 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
res/rustdesk*.zst
|
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
|
||||||
|
|
||||||
|
- 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
|
- name: Publish fedora28/centos8 package
|
||||||
if: ${{ matrix.job.extra-build-features == '' }}
|
if: ${{ matrix.job.extra-build-features == '' }}
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
443
Cargo.lock
generated
@ -47,7 +47,7 @@ libc = "0.2"
|
|||||||
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
|
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
|
||||||
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
|
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
|
||||||
runas = "0.2"
|
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 }
|
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
|
||||||
rubato = { version = "0.12", optional = true }
|
rubato = { version = "0.12", optional = true }
|
||||||
samplerate = { version = "0.2", optional = true }
|
samplerate = { version = "0.2", optional = true }
|
||||||
@ -59,11 +59,11 @@ base64 = "0.13"
|
|||||||
sysinfo = "0.24"
|
sysinfo = "0.24"
|
||||||
num_cpus = "1.13"
|
num_cpus = "1.13"
|
||||||
bytes = { version = "1.2", features = ["serde"] }
|
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"
|
wol-rs = "0.9.1"
|
||||||
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
|
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
|
||||||
errno = "0.2.8"
|
errno = "0.2.8"
|
||||||
rdev = { git = "https://github.com/rustdesk/rdev" }
|
rdev = { git = "https://github.com/fufesou/rdev" }
|
||||||
url = { version = "2.1", features = ["serde"] }
|
url = { version = "2.1", features = ["serde"] }
|
||||||
|
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
|
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
|
||||||
@ -118,6 +118,8 @@ dbus = "0.9"
|
|||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
gtk = "0.15"
|
gtk = "0.15"
|
||||||
libappindicator = "0.7"
|
libappindicator = "0.7"
|
||||||
|
glib = "0.16.5"
|
||||||
|
backtrace = "0.3"
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.11"
|
android_logger = "0.11"
|
||||||
|
@ -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 |
|
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||||
| USA (Ashburn) | 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
|
## Dependencies
|
||||||
|
|
||||||
|
87
appimage/AppImageBuilder.yml
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# 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/
|
||||||
|
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
|
21
build.py
@ -21,7 +21,7 @@ skip_cargo = False
|
|||||||
def custom_os_system(cmd):
|
def custom_os_system(cmd):
|
||||||
err = os._system(cmd)
|
err = os._system(cmd)
|
||||||
if err != 0:
|
if err != 0:
|
||||||
print(f"Error occured when executing: {cmd}. Exiting.")
|
print(f"Error occurred when executing: {cmd}. Exiting.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
# replace prebuilt os.system
|
# replace prebuilt os.system
|
||||||
os._system = os.system
|
os._system = os.system
|
||||||
@ -99,6 +99,11 @@ def make_parser():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Build rustdesk libs with the flatpak feature enabled'
|
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(
|
parser.add_argument(
|
||||||
'--skip-cargo',
|
'--skip-cargo',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@ -236,6 +241,8 @@ def get_features(args):
|
|||||||
features.append('flutter')
|
features.append('flutter')
|
||||||
if args.flatpak:
|
if args.flatpak:
|
||||||
features.append('flatpak')
|
features.append('flatpak')
|
||||||
|
if args.appimage:
|
||||||
|
features.append('appimage')
|
||||||
print("features:", features)
|
print("features:", features)
|
||||||
return features
|
return features
|
||||||
|
|
||||||
@ -305,7 +312,8 @@ def build_flutter_deb(version, features):
|
|||||||
|
|
||||||
def build_flutter_dmg(version, features):
|
def build_flutter_dmg(version, features):
|
||||||
if not skip_cargo:
|
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
|
# copy dylib
|
||||||
os.system(
|
os.system(
|
||||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||||
@ -469,6 +477,7 @@ def main():
|
|||||||
if pa:
|
if pa:
|
||||||
os.system('''
|
os.system('''
|
||||||
# buggy: rcodesign sign ... path/*, have to sign one by one
|
# 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/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/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
|
#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
|
||||||
@ -481,9 +490,15 @@ def main():
|
|||||||
version, 'rustdesk-%s.dmg' % version)
|
version, 'rustdesk-%s.dmg' % version)
|
||||||
if pa:
|
if pa:
|
||||||
os.system('''
|
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
|
#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
|
codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg
|
||||||
# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html
|
# 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
|
rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg
|
||||||
# verify: spctl -a -t exec -v /Applications/RustDesk.app
|
# verify: spctl -a -t exec -v /Applications/RustDesk.app
|
||||||
'''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key')))
|
'''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key')))
|
||||||
|
16
build.rs
@ -1,9 +1,16 @@
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn build_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:rustc-link-lib=WtsApi32");
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed={}", file);
|
||||||
println!("cargo:rerun-if-changed=windows.cc");
|
}
|
||||||
|
|
||||||
|
#[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"))]
|
#[cfg(all(windows, feature = "inline"))]
|
||||||
@ -117,5 +124,8 @@ fn main() {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
build_windows();
|
build_windows();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
build_mac();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ 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 Addroid 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
|
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_export_environment.sh
|
||||||
Flutter-Generated.xcconfig
|
Flutter-Generated.xcconfig
|
||||||
key.jks
|
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
|
#!/bin/bash
|
||||||
|
|
||||||
# Build libyuv / opus / libvpx / oboe for Android
|
# Build libyuv / opus / libvpx / oboe for Android
|
||||||
# Required:
|
# Required:
|
||||||
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
|
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
|
||||||
# 2. vcpkg initialized
|
# 2. vcpkg initialized
|
||||||
# 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`)
|
# 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
|
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
ANDROID_ABI=$1
|
ANDROID_ABI=$1
|
||||||
VCPKG_TARGET=$2
|
VCPKG_TARGET=$2
|
||||||
NDK_LLVM_TARGET=$3
|
NDK_LLVM_TARGET=$3
|
||||||
LIBVPX_TARGET=$4
|
LIBVPX_TARGET=$4
|
||||||
@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch
|
|||||||
# x86_64-linux-android
|
# x86_64-linux-android
|
||||||
# i686-linux-android
|
# i686-linux-android
|
||||||
|
|
||||||
# LIBVPX_TARGET :
|
# LIBVPX_TARGET :
|
||||||
# arm64-android-gcc
|
# arm64-android-gcc
|
||||||
# armv7-android-gcc
|
# armv7-android-gcc
|
||||||
# x86_64-android-gcc
|
# x86_64-android-gcc
|
||||||
# x86-android-gcc
|
# x86-android-gcc
|
||||||
|
|
||||||
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
|
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
|
||||||
build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
|
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/libvpx
|
||||||
# rm -rf build/oboe
|
# 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 = "";
|
var version = "";
|
||||||
int androidVersion = 0;
|
int androidVersion = 0;
|
||||||
|
|
||||||
/// only avaliable for Windows target
|
/// only available for Windows target
|
||||||
int windowsBuildNumber = 0;
|
int windowsBuildNumber = 0;
|
||||||
DesktopType? desktopType;
|
DesktopType? desktopType;
|
||||||
|
|
||||||
@ -99,22 +99,28 @@ class IconFont {
|
|||||||
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||||
const ColorThemeExtension({
|
const ColorThemeExtension({
|
||||||
required this.border,
|
required this.border,
|
||||||
|
required this.highlight,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Color? border;
|
final Color? border;
|
||||||
|
final Color? highlight;
|
||||||
|
|
||||||
static const light = ColorThemeExtension(
|
static const light = ColorThemeExtension(
|
||||||
border: Color(0xFFCCCCCC),
|
border: Color(0xFFCCCCCC),
|
||||||
|
highlight: Color(0xFFE5E5E5),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const dark = ColorThemeExtension(
|
static const dark = ColorThemeExtension(
|
||||||
border: Color(0xFF555555),
|
border: Color(0xFF555555),
|
||||||
|
highlight: Color(0xFF3F3F3F),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ThemeExtension<ColorThemeExtension> copyWith({Color? border}) {
|
ThemeExtension<ColorThemeExtension> copyWith(
|
||||||
|
{Color? border, Color? highlight}) {
|
||||||
return ColorThemeExtension(
|
return ColorThemeExtension(
|
||||||
border: border ?? this.border,
|
border: border ?? this.border,
|
||||||
|
highlight: highlight ?? this.highlight,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
|||||||
}
|
}
|
||||||
return ColorThemeExtension(
|
return ColorThemeExtension(
|
||||||
border: Color.lerp(border, other.border, t),
|
border: Color.lerp(border, other.border, t),
|
||||||
|
highlight: Color.lerp(highlight, other.highlight, t),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,7 +230,7 @@ class MyTheme {
|
|||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||||
}
|
}
|
||||||
bind.mainChangeTheme(dark: currentThemeMode().toShortString());
|
bind.mainChangeTheme(dark: mode.toShortString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1360,13 +1367,13 @@ connect(BuildContext context, String id,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String>> getHttpHeaders() async {
|
Map<String, String> getHttpHeaders() {
|
||||||
return {
|
return {
|
||||||
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
|
'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> {
|
class SimpleWrapper<T> {
|
||||||
T value;
|
T value;
|
||||||
SimpleWrapper(this.value);
|
SimpleWrapper(this.value);
|
||||||
@ -1402,7 +1409,7 @@ Future<void> reloadAllWindows() async {
|
|||||||
/// Indicate the flutter app is running in portable mode.
|
/// Indicate the flutter app is running in portable mode.
|
||||||
///
|
///
|
||||||
/// [Note]
|
/// [Note]
|
||||||
/// Portable build is only avaliable on Windows.
|
/// Portable build is only available on Windows.
|
||||||
bool isRunningInPortableMode() {
|
bool isRunningInPortableMode() {
|
||||||
if (!Platform.isWindows) {
|
if (!Platform.isWindows) {
|
||||||
return false;
|
return false;
|
||||||
@ -1411,7 +1418,7 @@ bool isRunningInPortableMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Window status callback
|
/// Window status callback
|
||||||
void onActiveWindowChanged() async {
|
Future<void> onActiveWindowChanged() async {
|
||||||
print(
|
print(
|
||||||
"[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
|
"[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
|
||||||
if (rustDeskWinManager.getActiveWindows().isEmpty) {
|
if (rustDeskWinManager.getActiveWindows().isEmpty) {
|
||||||
@ -1503,3 +1510,53 @@ Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
|
|||||||
bool get kUseCompatibleUiMode =>
|
bool get kUseCompatibleUiMode =>
|
||||||
Platform.isWindows &&
|
Platform.isWindows &&
|
||||||
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
|
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'] ?? "";
|
||||||
|
}
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
@ -3,14 +3,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
|||||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.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/popup_menu.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/login.dart';
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../desktop/pages/desktop_home_page.dart';
|
import 'login.dart';
|
||||||
import '../../mobile/pages/settings_page.dart';
|
|
||||||
|
|
||||||
class AddressBook extends StatefulWidget {
|
class AddressBook extends StatefulWidget {
|
||||||
final EdgeInsets? menuPadding;
|
final EdgeInsets? menuPadding;
|
||||||
@ -28,7 +26,6 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -42,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 {
|
Future<Widget> buildBody(BuildContext context) async {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
if (gFFI.userModel.userName.value.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: handleLogin,
|
onTap: loginDialog,
|
||||||
child: Text(
|
child: Text(
|
||||||
translate("Login"),
|
translate("Login"),
|
||||||
style: const TextStyle(decoration: TextDecoration.underline),
|
style: const TextStyle(decoration: TextDecoration.underline),
|
||||||
|
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: [msgBoxButton(translate('Close'), 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: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: onVerify, child: Text(translate("Verify"))),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -321,6 +321,7 @@ enum CardType {
|
|||||||
fav,
|
fav,
|
||||||
lan,
|
lan,
|
||||||
ab,
|
ab,
|
||||||
|
grp,
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasePeerCard extends StatelessWidget {
|
abstract class BasePeerCard extends StatelessWidget {
|
||||||
@ -463,7 +464,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only avaliable on Windows.
|
/// Only available on Windows.
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> _createShortCutAction(String id) {
|
MenuEntryBase<String> _createShortCutAction(String id) {
|
||||||
return MenuEntryButton<String>(
|
return MenuEntryButton<String>(
|
||||||
@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
case CardType.ab:
|
case CardType.ab:
|
||||||
gFFI.abModel.pullAb();
|
gFFI.abModel.pullAb();
|
||||||
break;
|
break;
|
||||||
|
case CardType.grp:
|
||||||
|
gFFI.groupModel.pull();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MyGroupPeerCard extends BasePeerCard {
|
||||||
|
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
||||||
|
: super(
|
||||||
|
peer: peer,
|
||||||
|
cardType: CardType.grp,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _rdpDialog(String id, CardType card) async {
|
void _rdpDialog(String id, CardType card) async {
|
||||||
String port, username;
|
String port, username;
|
||||||
if (card == CardType.ab) {
|
if (card == CardType.ab) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/address_book.dart';
|
import 'package:flutter_hbb/common/widgets/address_book.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/my_group.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@ -16,6 +16,101 @@ import 'package:get/get.dart';
|
|||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../models/platform_model.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 {
|
class PeerTabPage extends StatefulWidget {
|
||||||
const PeerTabPage({Key? key}) : super(key: key);
|
const PeerTabPage({Key? key}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
@ -23,10 +118,9 @@ class PeerTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TabEntry {
|
class _TabEntry {
|
||||||
final String name;
|
|
||||||
final Widget widget;
|
final Widget widget;
|
||||||
final Function() load;
|
final Function() load;
|
||||||
_TabEntry(this.name, this.widget, this.load);
|
_TabEntry(this.widget, this.load);
|
||||||
}
|
}
|
||||||
|
|
||||||
EdgeInsets? _menuPadding() {
|
EdgeInsets? _menuPadding() {
|
||||||
@ -35,65 +129,36 @@ EdgeInsets? _menuPadding() {
|
|||||||
|
|
||||||
class _PeerTabPageState extends State<PeerTabPage>
|
class _PeerTabPageState extends State<PeerTabPage>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late final RxInt tabHiddenFlag;
|
|
||||||
late final RxString currentTab;
|
|
||||||
late final RxList<String> visibleOrderedTabs;
|
|
||||||
final List<_TabEntry> entries = [
|
final List<_TabEntry> entries = [
|
||||||
_TabEntry(
|
_TabEntry(
|
||||||
'Recent Sessions',
|
|
||||||
RecentPeersView(
|
RecentPeersView(
|
||||||
menuPadding: _menuPadding(),
|
menuPadding: _menuPadding(),
|
||||||
),
|
),
|
||||||
bind.mainLoadRecentPeers),
|
bind.mainLoadRecentPeers),
|
||||||
_TabEntry(
|
_TabEntry(
|
||||||
'Favorites',
|
|
||||||
FavoritePeersView(
|
FavoritePeersView(
|
||||||
menuPadding: _menuPadding(),
|
menuPadding: _menuPadding(),
|
||||||
),
|
),
|
||||||
bind.mainLoadFavPeers),
|
bind.mainLoadFavPeers),
|
||||||
_TabEntry(
|
_TabEntry(
|
||||||
'Discovered',
|
|
||||||
DiscoveredPeersView(
|
DiscoveredPeersView(
|
||||||
menuPadding: _menuPadding(),
|
menuPadding: _menuPadding(),
|
||||||
),
|
),
|
||||||
bind.mainDiscover),
|
bind.mainDiscover),
|
||||||
_TabEntry(
|
_TabEntry(
|
||||||
'Address Book',
|
|
||||||
AddressBook(
|
AddressBook(
|
||||||
menuPadding: _menuPadding(),
|
menuPadding: _menuPadding(),
|
||||||
),
|
),
|
||||||
() => {}),
|
() => {}),
|
||||||
|
_TabEntry(
|
||||||
|
MyGroup(
|
||||||
|
menuPadding: _menuPadding(),
|
||||||
|
),
|
||||||
|
() => {}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
tabHiddenFlag = (int.tryParse(
|
|
||||||
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
|
|
||||||
radix: 2) ??
|
|
||||||
0)
|
|
||||||
.obs;
|
|
||||||
currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs;
|
|
||||||
visibleOrderedTabs = entries
|
|
||||||
.where((e) => !isTabHidden(e.name))
|
|
||||||
.map((e) => e.name)
|
|
||||||
.toList()
|
|
||||||
.obs;
|
|
||||||
try {
|
|
||||||
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
|
|
||||||
if (conf.isNotEmpty) {
|
|
||||||
final json = jsonDecode(conf);
|
|
||||||
if (json is List) {
|
|
||||||
final List<String> list = json.map((e) => e.toString()).toList();
|
|
||||||
if (list.length == visibleOrderedTabs.length &&
|
|
||||||
visibleOrderedTabs.every((e) => list.contains(e))) {
|
|
||||||
visibleOrderedTabs.value = list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrintStack(label: '$e');
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustTab();
|
adjustTab();
|
||||||
|
|
||||||
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
|
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
|
||||||
@ -105,10 +170,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleTabSelection(String tabName) async {
|
Future<void> handleTabSelection(int tabIndex) async {
|
||||||
currentTab.value = tabName;
|
if (tabIndex < entries.length) {
|
||||||
await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName);
|
statePeerTab.currentTab.value = tabIndex;
|
||||||
entries.firstWhereOrNull((e) => e.name == tabName)?.load();
|
entries[tabIndex].load();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -149,65 +215,80 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Widget _createSwitchBar(BuildContext context) {
|
Widget _createSwitchBar(BuildContext context) {
|
||||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
int indexCounter = -1;
|
var tabs = statePeerTab.currentTabs();
|
||||||
return ReorderableListView(
|
return ListView(
|
||||||
buildDefaultDragHandles: false,
|
|
||||||
onReorder: (oldIndex, newIndex) {
|
|
||||||
var list = visibleOrderedTabs.toList();
|
|
||||||
if (oldIndex < newIndex) {
|
|
||||||
newIndex -= 1;
|
|
||||||
}
|
|
||||||
final String item = list.removeAt(oldIndex);
|
|
||||||
list.insert(newIndex, item);
|
|
||||||
bind.setLocalFlutterConfig(
|
|
||||||
k: 'peer-tab-order', v: jsonEncode(list));
|
|
||||||
visibleOrderedTabs.value = list;
|
|
||||||
},
|
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
shrinkWrap: true,
|
physics: NeverScrollableScrollPhysics(),
|
||||||
scrollController: ScrollController(),
|
controller: ScrollController(),
|
||||||
children: visibleOrderedTabs.map((t) {
|
children: tabs.map((t) {
|
||||||
indexCounter++;
|
return InkWell(
|
||||||
return ReorderableDragStartListener(
|
child: Container(
|
||||||
key: ValueKey(t),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
index: indexCounter,
|
decoration: BoxDecoration(
|
||||||
child: InkWell(
|
color: statePeerTab.currentTab.value == t
|
||||||
child: Container(
|
? Theme.of(context).backgroundColor
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
: null,
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
|
||||||
color: currentTab.value == t
|
),
|
||||||
? Theme.of(context).backgroundColor
|
child: Align(
|
||||||
: null,
|
alignment: Alignment.center,
|
||||||
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
|
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,
|
onTap: () async {
|
||||||
child: Text(
|
await handleTabSelection(t);
|
||||||
translate(t),
|
await bind.setLocalFlutterConfig(
|
||||||
textAlign: TextAlign.center,
|
k: 'peer-tab-index', v: t.toString());
|
||||||
style: TextStyle(
|
},
|
||||||
height: 1,
|
|
||||||
fontSize: 14,
|
|
||||||
color: currentTab.value == t ? textColor : textColor
|
|
||||||
?..withOpacity(0.5)),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
onTap: () async => await handleTabSelection(t),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}).toList());
|
}).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() {
|
Widget _createPeersView() {
|
||||||
final verticalMargin = isDesktop ? 12.0 : 6.0;
|
final verticalMargin = isDesktop ? 12.0 : 6.0;
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Obx(() =>
|
child: Obx(() {
|
||||||
entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ??
|
var tabs = statePeerTab.currentTabs();
|
||||||
visibleContextMenuListener(Center(
|
if (tabs.isEmpty) {
|
||||||
child: Text(translate('Right click to select tabs')),
|
return visibleContextMenuListener(Center(
|
||||||
))).marginSymmetric(vertical: verticalMargin),
|
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) {
|
Widget _createPeerViewTypeSwitch(BuildContext context) {
|
||||||
@ -240,22 +321,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isTabHidden(String name) {
|
|
||||||
int index = entries.indexWhere((e) => e.name == name);
|
|
||||||
if (index >= 0) {
|
|
||||||
return tabHiddenFlag & (1 << index) != 0;
|
|
||||||
}
|
|
||||||
assert(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustTab() {
|
adjustTab() {
|
||||||
if (visibleOrderedTabs.isNotEmpty) {
|
var tabs = statePeerTab.currentTabs();
|
||||||
if (!visibleOrderedTabs.contains(currentTab.value)) {
|
if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
|
||||||
handleTabSelection(visibleOrderedTabs[0]);
|
statePeerTab.currentTab.value = tabs[0];
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentTab.value = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,47 +347,44 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget visibleContextMenu(CancelFunc cancelFunc) {
|
Widget visibleContextMenu(CancelFunc cancelFunc) {
|
||||||
final List<MenuEntryBase> menu = entries.asMap().entries.map((e) {
|
return Obx(() {
|
||||||
int bitMask = 1 << e.key;
|
final List<MenuEntryBase> menu = List.empty(growable: true);
|
||||||
return MenuEntrySwitch(
|
for (int i = 0; i < statePeerTab.tabNames.length; i++) {
|
||||||
switchType: SwitchType.scheckbox,
|
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
|
||||||
text: translate(e.value.name),
|
continue;
|
||||||
getter: () async {
|
}
|
||||||
return tabHiddenFlag.value & bitMask == 0;
|
int bitMask = 1 << i;
|
||||||
},
|
menu.add(MenuEntrySwitch(
|
||||||
setter: (show) async {
|
switchType: SwitchType.scheckbox,
|
||||||
if (show) {
|
text: translatedTabname(i),
|
||||||
tabHiddenFlag.value &= ~bitMask;
|
getter: () async {
|
||||||
} else {
|
return statePeerTab.tabHiddenFlag & bitMask == 0;
|
||||||
tabHiddenFlag.value |= bitMask;
|
},
|
||||||
}
|
setter: (show) async {
|
||||||
await bind.setLocalFlutterConfig(
|
if (show) {
|
||||||
k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2));
|
statePeerTab.tabHiddenFlag.value &= ~bitMask;
|
||||||
visibleOrderedTabs.removeWhere((e) => isTabHidden(e));
|
} else {
|
||||||
visibleOrderedTabs.addAll(entries
|
statePeerTab.tabHiddenFlag.value |= bitMask;
|
||||||
.where((e) =>
|
}
|
||||||
!visibleOrderedTabs.contains(e.name) &&
|
await bind.setLocalFlutterConfig(
|
||||||
!isTabHidden(e.name))
|
k: 'hidden-peer-card',
|
||||||
.map((e) => e.name)
|
v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
|
||||||
.toList());
|
cancelFunc();
|
||||||
await bind.setLocalFlutterConfig(
|
adjustTab();
|
||||||
k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs));
|
}));
|
||||||
cancelFunc();
|
}
|
||||||
adjustTab();
|
return mod_menu.PopupMenu(
|
||||||
});
|
items: menu
|
||||||
}).toList();
|
.map((entry) => entry.build(
|
||||||
return mod_menu.PopupMenu(
|
context,
|
||||||
items: menu
|
const MenuConfig(
|
||||||
.map((entry) => entry.build(
|
commonColor: MyTheme.accent,
|
||||||
context,
|
height: 20.0,
|
||||||
const MenuConfig(
|
dividerHeight: 12.0,
|
||||||
commonColor: MyTheme.accent,
|
)))
|
||||||
height: 20.0,
|
.expand((i) => i)
|
||||||
dividerHeight: 12.0,
|
.toList());
|
||||||
)))
|
});
|
||||||
.expand((i) => i)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView {
|
|||||||
return true;
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -9,11 +9,12 @@ class RawKeyFocusScope extends StatelessWidget {
|
|||||||
final InputModel inputModel;
|
final InputModel inputModel;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
RawKeyFocusScope(
|
RawKeyFocusScope({
|
||||||
{this.focusNode,
|
this.focusNode,
|
||||||
this.onFocusChange,
|
this.onFocusChange,
|
||||||
required this.inputModel,
|
required this.inputModel,
|
||||||
required this.child});
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -35,11 +36,15 @@ class RawPointerMouseRegion extends StatelessWidget {
|
|||||||
final MouseCursor? cursor;
|
final MouseCursor? cursor;
|
||||||
final PointerEnterEventListener? onEnter;
|
final PointerEnterEventListener? onEnter;
|
||||||
final PointerExitEventListener? onExit;
|
final PointerExitEventListener? onExit;
|
||||||
|
final PointerDownEventListener? onPointerDown;
|
||||||
|
final PointerUpEventListener? onPointerUp;
|
||||||
|
|
||||||
RawPointerMouseRegion(
|
RawPointerMouseRegion(
|
||||||
{this.onEnter,
|
{this.onEnter,
|
||||||
this.onExit,
|
this.onExit,
|
||||||
this.cursor,
|
this.cursor,
|
||||||
|
this.onPointerDown,
|
||||||
|
this.onPointerUp,
|
||||||
required this.inputModel,
|
required this.inputModel,
|
||||||
required this.child});
|
required this.child});
|
||||||
|
|
||||||
@ -47,8 +52,14 @@ class RawPointerMouseRegion extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerHover: inputModel.onPointHoverImage,
|
onPointerHover: inputModel.onPointHoverImage,
|
||||||
onPointerDown: inputModel.onPointDownImage,
|
onPointerDown: (evt) {
|
||||||
onPointerUp: inputModel.onPointUpImage,
|
onPointerDown?.call(evt);
|
||||||
|
inputModel.onPointDownImage(evt);
|
||||||
|
},
|
||||||
|
onPointerUp: (evt) {
|
||||||
|
onPointerUp?.call(evt);
|
||||||
|
inputModel.onPointUpImage(evt);
|
||||||
|
},
|
||||||
onPointerMove: inputModel.onPointMoveImage,
|
onPointerMove: inputModel.onPointMoveImage,
|
||||||
onPointerSignal: inputModel.onPointerSignalImage,
|
onPointerSignal: inputModel.onPointerSignalImage,
|
||||||
/*
|
/*
|
||||||
|
@ -100,6 +100,8 @@ const kRemoteImageQualityLow = 'low';
|
|||||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||||
const kRemoteImageQualityCustom = 'custom';
|
const kRemoteImageQualityCustom = 'custom';
|
||||||
|
|
||||||
|
const kIgnoreDpi = true;
|
||||||
|
|
||||||
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
||||||
/// see [LogicalKeyboardKey.keyLabel]
|
/// see [LogicalKeyboardKey.keyLabel]
|
||||||
const Map<int, String> logicalKeyMap = <int, String>{
|
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:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.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/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -16,7 +15,6 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/formatter/id_formatter.dart';
|
import '../../common/formatter/id_formatter.dart';
|
||||||
import '../../common/widgets/peer_tab_page.dart';
|
import '../../common/widgets/peer_tab_page.dart';
|
||||||
import '../../common/widgets/peers_view.dart';
|
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../widgets/button.dart';
|
import '../widgets/button.dart';
|
||||||
|
|
||||||
@ -172,6 +170,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => TextField(
|
() => TextField(
|
||||||
|
maxLength: 90,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
@ -179,12 +178,13 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
height: 1,
|
height: 1.25,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
cursorColor:
|
cursorColor:
|
||||||
Theme.of(context).textTheme.titleLarge?.color,
|
Theme.of(context).textTheme.titleLarge?.color,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
counterText: '',
|
||||||
hintText: _idInputFocused.value
|
hintText: _idInputFocused.value
|
||||||
? null
|
? null
|
||||||
: translate('Enter Remote ID'),
|
: translate('Enter Remote ID'),
|
||||||
|
@ -42,6 +42,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
var svcStopped = false.obs;
|
var svcStopped = false.obs;
|
||||||
var watchIsCanScreenRecording = false;
|
var watchIsCanScreenRecording = false;
|
||||||
var watchIsProcessTrust = false;
|
var watchIsProcessTrust = false;
|
||||||
|
var watchIsInputMonitoring = false;
|
||||||
Timer? _updateTimer;
|
Timer? _updateTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -334,6 +335,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
bind.mainIsProcessTrusted(prompt: true);
|
bind.mainIsProcessTrusted(prompt: true);
|
||||||
watchIsProcessTrust = true;
|
watchIsProcessTrust = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, 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 &&
|
} else if (!svcStopped.value &&
|
||||||
bind.mainIsInstalled() &&
|
bind.mainIsInstalled() &&
|
||||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||||
@ -438,7 +445,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
bind.mainStartGrabKeyboard();
|
|
||||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||||
await gFFI.serverModel.fetchID();
|
await gFFI.serverModel.fetchID();
|
||||||
final url = await bind.mainGetSoftwareUpdateUrl();
|
final url = await bind.mainGetSoftwareUpdateUrl();
|
||||||
@ -468,6 +474,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (watchIsInputMonitoring) {
|
||||||
|
if (bind.mainIsCanInputMonitoring(prompt: false)) {
|
||||||
|
watchIsInputMonitoring = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Get.put<RxBool>(svcStopped, tag: 'stop-service');
|
Get.put<RxBool>(svcStopped, tag: 'stop-service');
|
||||||
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
|
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
|
||||||
@ -501,9 +513,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
} else if (call.method == kWindowActionRebuild) {
|
} else if (call.method == kWindowActionRebuild) {
|
||||||
reloadCurrentWindow();
|
reloadCurrentWindow();
|
||||||
} else if (call.method == kWindowEventShow) {
|
} else if (call.method == kWindowEventShow) {
|
||||||
rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
|
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
|
||||||
} else if (call.method == kWindowEventHide) {
|
} else if (call.method == kWindowEventHide) {
|
||||||
rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
|
await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
|
||||||
} else if (call.method == kWindowConnect) {
|
} else if (call.method == kWindowConnect) {
|
||||||
await connectMainDesktop(
|
await connectMainDesktop(
|
||||||
call.arguments['id'],
|
call.arguments['id'],
|
||||||
|
@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart';
|
|||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/login.dart';
|
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:get/get.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 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||||
|
|
||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
|
import '../../common/widgets/login.dart';
|
||||||
|
|
||||||
const double _kTabWidth = 235;
|
const double _kTabWidth = 235;
|
||||||
const double _kTabHeight = 42;
|
const double _kTabHeight = 42;
|
||||||
@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
child: PageView(
|
child: PageView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
children: const [
|
children: const [
|
||||||
_General(),
|
_General(),
|
||||||
_Safety(),
|
_Safety(),
|
||||||
@ -273,6 +274,15 @@ class _GeneralState extends State<_General> {
|
|||||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||||
'enable-confirm-closing-tabs'),
|
'enable-confirm-closing-tabs'),
|
||||||
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
|
_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;
|
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
|
// should set one by one
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'custom-rendezvous-server', value: idServer);
|
key: 'custom-rendezvous-server', value: idServer);
|
||||||
@ -954,23 +968,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
import() {
|
import() {
|
||||||
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
||||||
TextEditingController mytext = TextEditingController();
|
final text = value?.text;
|
||||||
String? aNullableString = '';
|
if (text != null && text.isNotEmpty) {
|
||||||
aNullableString = value?.text;
|
|
||||||
mytext.text = aNullableString.toString();
|
|
||||||
if (mytext.text.isNotEmpty) {
|
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic> config = jsonDecode(mytext.text);
|
final sc = ServerConfig.decode(text);
|
||||||
if (config.containsKey('IdServer')) {
|
if (sc.idServer.isNotEmpty) {
|
||||||
String id = config['IdServer'] ?? '';
|
idController.text = sc.idServer;
|
||||||
String relay = config['RelayServer'] ?? '';
|
relayController.text = sc.relayServer;
|
||||||
String api = config['ApiServer'] ?? '';
|
apiController.text = sc.apiServer;
|
||||||
String key = config['Key'] ?? '';
|
keyController.text = sc.key;
|
||||||
idController.text = id;
|
Future<bool> success =
|
||||||
relayController.text = relay;
|
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
|
||||||
apiController.text = api;
|
|
||||||
keyController.text = key;
|
|
||||||
Future<bool> success = set(id, relay, api, key);
|
|
||||||
success.then((value) {
|
success.then((value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
showToast(
|
showToast(
|
||||||
@ -992,12 +1000,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export() {
|
export() {
|
||||||
Map<String, String> config = {};
|
final text = ServerConfig(
|
||||||
config['IdServer'] = idController.text.trim();
|
idServer: idController.text,
|
||||||
config['RelayServer'] = relayController.text.trim();
|
relayServer: relayController.text,
|
||||||
config['ApiServer'] = apiController.text.trim();
|
apiServer: apiController.text,
|
||||||
config['Key'] = keyController.text.trim();
|
key: keyController.text)
|
||||||
Clipboard.setData(ClipboardData(text: jsonEncode(config)));
|
.encode();
|
||||||
|
debugPrint("ServerConfig export: $text");
|
||||||
|
|
||||||
|
Clipboard.setData(ClipboardData(text: text));
|
||||||
showToast(translate('Export server configuration successfully'));
|
showToast(translate('Export server configuration successfully'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1059,21 +1070,13 @@ class _AccountState extends State<_Account> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget accountAction() {
|
Widget accountAction() {
|
||||||
return _futureBuilder(future: () async {
|
return Obx(() => _Button(
|
||||||
return await gFFI.userModel.getUserName();
|
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
||||||
}(), hasData: (_) {
|
() => {
|
||||||
return Obx(() => _Button(
|
gFFI.userModel.userName.value.isEmpty
|
||||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
? loginDialog()
|
||||||
() => {
|
: gFFI.userModel.logOut()
|
||||||
gFFI.userModel.userName.value.isEmpty
|
}));
|
||||||
? loginDialog().then((success) {
|
|
||||||
if (success) {
|
|
||||||
gFFI.abModel.pullAb();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: gFFI.userModel.logOut()
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1103,29 +1106,31 @@ class _AboutState extends State<_About> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
child: _Card(title: 'About RustDesk', children: [
|
child: _Card(title: '${translate('About')} RustDesk', children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
Text('Version: $version').marginSymmetric(vertical: 4.0),
|
Text('${translate('Version')}: $version')
|
||||||
Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0),
|
.marginSymmetric(vertical: 4.0),
|
||||||
|
Text('${translate('Build Date')}: $buildDate')
|
||||||
|
.marginSymmetric(vertical: 4.0),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString('https://rustdesk.com/privacy');
|
launchUrlString('https://rustdesk.com/privacy');
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Privacy Statement',
|
translate('Privacy Statement'),
|
||||||
style: linkStyle,
|
style: linkStyle,
|
||||||
).marginSymmetric(vertical: 4.0)),
|
).marginSymmetric(vertical: 4.0)),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString('https://rustdesk.com');
|
launchUrlString('https://rustdesk.com');
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Website',
|
translate('Website'),
|
||||||
style: linkStyle,
|
style: linkStyle,
|
||||||
).marginSymmetric(vertical: 4.0)),
|
).marginSymmetric(vertical: 4.0)),
|
||||||
Container(
|
Container(
|
||||||
@ -1142,8 +1147,8 @@ class _AboutState extends State<_About> {
|
|||||||
'Copyright © 2022 Purslane Ltd.\n$license',
|
'Copyright © 2022 Purslane Ltd.\n$license',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
const Text(
|
Text(
|
||||||
'Made with heart in this chaotic world!',
|
translate('Slogan_tip'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: Colors.white),
|
color: Colors.white),
|
||||||
@ -1227,7 +1232,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
|
|||||||
ref.value = option;
|
ref.value = option;
|
||||||
if (reverse) option = !option;
|
if (reverse) option = !option;
|
||||||
String value = bool2option(key, option);
|
String value = bool2option(key, option);
|
||||||
bind.mainSetOption(key: key, value: value);
|
await bind.mainSetOption(key: key, value: value);
|
||||||
update?.call();
|
update?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1455,6 +1460,8 @@ _LabeledTextField(
|
|||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
obscureText: secure,
|
obscureText: secure,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||||
errorText: errorText.isNotEmpty ? errorText : null),
|
errorText: errorText.isNotEmpty ? errorText : null),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _disabledTextColor(context, enabled),
|
color: _disabledTextColor(context, enabled),
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.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/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/models/file_model.dart';
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -32,6 +33,18 @@ enum LocationStatus {
|
|||||||
fileSearchBar
|
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 {
|
class FileManagerPage extends StatefulWidget {
|
||||||
const FileManagerPage({Key? key, required this.id}) : super(key: key);
|
const FileManagerPage({Key? key, required this.id}) : super(key: key);
|
||||||
final String id;
|
final String id;
|
||||||
@ -55,6 +68,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
final _searchTextRemote = "".obs;
|
final _searchTextRemote = "".obs;
|
||||||
final _breadCrumbScrollerLocal = ScrollController();
|
final _breadCrumbScrollerLocal = ScrollController();
|
||||||
final _breadCrumbScrollerRemote = 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
|
/// [_lastClickTime], [_lastClickEntry] help to handle double click
|
||||||
int _lastClickTime =
|
int _lastClickTime =
|
||||||
@ -197,6 +215,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget body({bool isLocal = false}) {
|
Widget body({bool isLocal = false}) {
|
||||||
|
final scrollController = ScrollController();
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
|
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
|
||||||
margin: const EdgeInsets.all(16.0),
|
margin: const EdgeInsets.all(16.0),
|
||||||
@ -217,8 +236,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: ScrollController(),
|
controller: scrollController,
|
||||||
child: _buildDataTable(context, isLocal),
|
child: _buildDataTable(context, isLocal, scrollController),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -228,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 fd = model.getCurrentDir(isLocal);
|
||||||
final entries = fd.entries;
|
final entries = fd.entries;
|
||||||
final sortIndex = (SortBy style) {
|
final sortIndex = (SortBy style) {
|
||||||
@ -246,130 +267,219 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
final sortAscending =
|
final sortAscending =
|
||||||
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
||||||
|
|
||||||
return ObxValue<RxString>(
|
return MouseRegion(
|
||||||
(searchText) {
|
onEnter: (evt) {
|
||||||
final filteredEntries = searchText.isNotEmpty
|
_mouseFocusScope.value =
|
||||||
? entries.where((element) {
|
isLocal ? MouseFocusScope.local : MouseFocusScope.remote;
|
||||||
return element.name.contains(searchText.value);
|
if (isLocal) {
|
||||||
}).toList(growable: false)
|
_keyboardNodeLocal.requestFocus();
|
||||||
: entries;
|
} else {
|
||||||
return DataTable(
|
_keyboardNodeRemote.requestFocus();
|
||||||
key: ValueKey(isLocal ? 0 : 1),
|
}
|
||||||
showCheckboxColumn: false,
|
},
|
||||||
dataRowHeight: 25,
|
onExit: (evt) {
|
||||||
headingRowHeight: 30,
|
_mouseFocusScope.value = MouseFocusScope.none;
|
||||||
horizontalMargin: 8,
|
},
|
||||||
columnSpacing: 8,
|
child: ListSearchActionListener(
|
||||||
showBottomBorder: true,
|
node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote,
|
||||||
sortColumnIndex: sortIndex,
|
buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote,
|
||||||
sortAscending: sortAscending,
|
onNext: (buffer) {
|
||||||
columns: [
|
debugPrint("searching next for $buffer");
|
||||||
DataColumn(
|
assert(buffer.length == 1);
|
||||||
label: Text(
|
final selectedEntries = getSelectedItems(isLocal);
|
||||||
translate("Name"),
|
assert(selectedEntries.length <= 1);
|
||||||
).marginSymmetric(horizontal: 4),
|
var skipCount = 0;
|
||||||
onSort: (columnIndex, ascending) {
|
if (selectedEntries.items.isNotEmpty) {
|
||||||
model.changeSortStyle(SortBy.name,
|
final index = entries.indexOf(selectedEntries.items.first);
|
||||||
isLocal: isLocal, ascending: ascending);
|
if (index < 0) {
|
||||||
}),
|
return;
|
||||||
DataColumn(
|
}
|
||||||
label: Text(
|
skipCount = index + 1;
|
||||||
translate("Modified"),
|
}
|
||||||
),
|
var searchResult = entries
|
||||||
onSort: (columnIndex, ascending) {
|
.skip(skipCount)
|
||||||
model.changeSortStyle(SortBy.modified,
|
.where((element) => element.name.startsWith(buffer));
|
||||||
isLocal: isLocal, ascending: ascending);
|
if (searchResult.isEmpty) {
|
||||||
}),
|
// cannot find next, lets restart search from head
|
||||||
DataColumn(
|
searchResult =
|
||||||
label: Text(translate("Size")),
|
entries.where((element) => element.name.startsWith(buffer));
|
||||||
onSort: (columnIndex, ascending) {
|
}
|
||||||
model.changeSortStyle(SortBy.size,
|
if (searchResult.isEmpty) {
|
||||||
isLocal: isLocal, ascending: ascending);
|
setState(() {
|
||||||
}),
|
getSelectedItems(isLocal).clear();
|
||||||
],
|
});
|
||||||
rows: filteredEntries.map((entry) {
|
return;
|
||||||
final sizeStr =
|
}
|
||||||
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
_jumpToEntry(
|
||||||
final lastModifiedStr = entry.isDrive
|
isLocal, searchResult.first, scrollController, rowHeight, buffer);
|
||||||
? " "
|
},
|
||||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
onSearch: (buffer) {
|
||||||
return DataRow(
|
debugPrint("searching for $buffer");
|
||||||
key: ValueKey(entry.name),
|
final selectedEntries = getSelectedItems(isLocal);
|
||||||
onSelectChanged: (s) {
|
final searchResult =
|
||||||
_onSelectedChanged(getSelectedItems(isLocal), filteredEntries,
|
entries.where((element) => element.name.startsWith(buffer));
|
||||||
entry, isLocal);
|
selectedEntries.clear();
|
||||||
},
|
if (searchResult.isEmpty) {
|
||||||
selected: getSelectedItems(isLocal).contains(entry),
|
setState(() {
|
||||||
cells: [
|
getSelectedItems(isLocal).clear();
|
||||||
DataCell(
|
});
|
||||||
Container(
|
return;
|
||||||
width: 200,
|
}
|
||||||
child: Tooltip(
|
_jumpToEntry(
|
||||||
waitDuration: Duration(milliseconds: 500),
|
isLocal, searchResult.first, scrollController, rowHeight, buffer);
|
||||||
message: entry.name,
|
},
|
||||||
child: Row(children: [
|
child: ObxValue<RxString>(
|
||||||
entry.isDrive
|
(searchText) {
|
||||||
? Image(
|
final filteredEntries = searchText.isNotEmpty
|
||||||
image: iconHardDrive,
|
? entries.where((element) {
|
||||||
fit: BoxFit.scaleDown,
|
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)
|
color: Theme.of(context)
|
||||||
.iconTheme
|
.iconTheme
|
||||||
.color
|
.color
|
||||||
?.withOpacity(0.7))
|
?.withOpacity(0.7),
|
||||||
.paddingAll(4)
|
).marginSymmetric(horizontal: 2),
|
||||||
: Icon(
|
Expanded(
|
||||||
entry.isFile
|
child: Text(entry.name,
|
||||||
? Icons.feed_outlined
|
overflow: TextOverflow.ellipsis))
|
||||||
: Icons.folder,
|
]),
|
||||||
size: 20,
|
)),
|
||||||
color: Theme.of(context)
|
onTap: () {
|
||||||
.iconTheme
|
final items = getSelectedItems(isLocal);
|
||||||
.color
|
|
||||||
?.withOpacity(0.7),
|
|
||||||
).marginSymmetric(horizontal: 2),
|
|
||||||
Expanded(
|
|
||||||
child: Text(entry.name,
|
|
||||||
overflow: TextOverflow.ellipsis))
|
|
||||||
]),
|
|
||||||
)),
|
|
||||||
onTap: () {
|
|
||||||
final items = getSelectedItems(isLocal);
|
|
||||||
|
|
||||||
// handle double click
|
// handle double click
|
||||||
if (_checkDoubleClick(entry)) {
|
if (_checkDoubleClick(entry)) {
|
||||||
openDirectory(entry.path, isLocal: isLocal);
|
openDirectory(entry.path, isLocal: isLocal);
|
||||||
items.clear();
|
items.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_onSelectedChanged(
|
_onSelectedChanged(
|
||||||
items, filteredEntries, entry, isLocal);
|
items, filteredEntries, entry, isLocal);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
DataCell(FittedBox(
|
DataCell(FittedBox(
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: lastModifiedStr,
|
||||||
|
child: Text(
|
||||||
|
lastModifiedStr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: MyTheme.darkGray),
|
||||||
|
)))),
|
||||||
|
DataCell(Tooltip(
|
||||||
waitDuration: Duration(milliseconds: 500),
|
waitDuration: Duration(milliseconds: 500),
|
||||||
message: lastModifiedStr,
|
message: sizeStr,
|
||||||
child: Text(
|
child: Text(
|
||||||
lastModifiedStr,
|
sizeStr,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: MyTheme.darkGray),
|
fontSize: 10, color: MyTheme.darkGray),
|
||||||
)))),
|
))),
|
||||||
DataCell(Tooltip(
|
]);
|
||||||
waitDuration: Duration(milliseconds: 500),
|
}).toList(growable: false),
|
||||||
message: sizeStr,
|
);
|
||||||
child: Text(
|
},
|
||||||
sizeStr,
|
isLocal ? _searchTextLocal : _searchTextRemote,
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
style: TextStyle(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,
|
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
|
||||||
Entry entry, bool isLocal) {
|
Entry entry, bool isLocal) {
|
||||||
final isCtrlDown = RawKeyboard.instance.keysPressed
|
final isCtrlDown = RawKeyboard.instance.keysPressed
|
||||||
@ -872,6 +982,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
},
|
},
|
||||||
dismissOnClicked: true));
|
dismissOnClicked: true));
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("buildBread fetchDirectory err=$e");
|
||||||
} finally {
|
} finally {
|
||||||
if (!isLocal) {
|
if (!isLocal) {
|
||||||
_ffi.dialogManager.dismissByTag(loadingTag);
|
_ffi.dialogManager.dismissByTag(loadingTag);
|
||||||
@ -1015,4 +1127,14 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
}
|
}
|
||||||
model.sendFiles(items, isRemote: false);
|
model.sendFiles(items, isRemote: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refocusKeyboardListener(bool isLocal) {
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
if (isLocal) {
|
||||||
|
_keyboardNodeLocal.requestFocus();
|
||||||
|
} else {
|
||||||
|
_keyboardNodeRemote.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,12 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
@ -20,6 +23,7 @@ import '../../models/model.dart';
|
|||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import '../widgets/remote_menubar.dart';
|
import '../widgets/remote_menubar.dart';
|
||||||
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
bool _isCustomCursorInited = false;
|
bool _isCustomCursorInited = false;
|
||||||
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
||||||
@ -46,17 +50,17 @@ class RemotePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RemotePageState extends State<RemotePage>
|
class _RemotePageState extends State<RemotePage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin, MultiWindowListener {
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
String keyboardMode = "legacy";
|
String keyboardMode = "legacy";
|
||||||
|
bool _isWindowBlur = false;
|
||||||
final _cursorOverImage = false.obs;
|
final _cursorOverImage = false.obs;
|
||||||
late RxBool _showRemoteCursor;
|
late RxBool _showRemoteCursor;
|
||||||
late RxBool _zoomCursor;
|
late RxBool _zoomCursor;
|
||||||
late RxBool _remoteCursorMoved;
|
late RxBool _remoteCursorMoved;
|
||||||
late RxBool _keyboardEnabled;
|
late RxBool _keyboardEnabled;
|
||||||
|
|
||||||
final FocusNode _rawKeyFocusNode = FocusNode();
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||||
var _imageFocused = false;
|
|
||||||
|
|
||||||
Function(bool)? _onEnterOrLeaveImage4Menubar;
|
Function(bool)? _onEnterOrLeaveImage4Menubar;
|
||||||
|
|
||||||
@ -92,6 +96,10 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_initStates(widget.id);
|
_initStates(widget.id);
|
||||||
_ffi = FFI();
|
_ffi = FFI();
|
||||||
Get.put(_ffi, tag: widget.id);
|
Get.put(_ffi, tag: widget.id);
|
||||||
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
|
});
|
||||||
_ffi.start(widget.id);
|
_ffi.start(widget.id);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||||
@ -101,7 +109,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (!Platform.isLinux) {
|
if (!Platform.isLinux) {
|
||||||
Wakelock.enable();
|
Wakelock.enable();
|
||||||
}
|
}
|
||||||
_rawKeyFocusNode.requestFocus();
|
|
||||||
_ffi.ffiModel.updateEventListener(widget.id);
|
_ffi.ffiModel.updateEventListener(widget.id);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||||
// Session option should be set after models.dart/FFI.start
|
// Session option should be set after models.dart/FFI.start
|
||||||
@ -109,22 +116,59 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
id: widget.id, arg: 'show-remote-cursor');
|
id: widget.id, arg: 'show-remote-cursor');
|
||||||
_zoomCursor.value =
|
_zoomCursor.value =
|
||||||
bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor');
|
bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor');
|
||||||
if (!_isCustomCursorInited) {
|
DesktopMultiWindow.addListener(this);
|
||||||
customCursorController.registerNeedUpdateCursorCallback(
|
// if (!_isCustomCursorInited) {
|
||||||
(String? lastKey, String? currentKey) async {
|
// customCursorController.registerNeedUpdateCursorCallback(
|
||||||
if (_firstEnterImage.value) {
|
// (String? lastKey, String? currentKey) async {
|
||||||
_firstEnterImage.value = false;
|
// if (_firstEnterImage.value) {
|
||||||
return true;
|
// _firstEnterImage.value = false;
|
||||||
}
|
// return true;
|
||||||
return lastKey == null || lastKey != currentKey;
|
// }
|
||||||
});
|
// return lastKey == null || lastKey != currentKey;
|
||||||
_isCustomCursorInited = true;
|
// });
|
||||||
|
// _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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
debugPrint("REMOTE PAGE dispose ${widget.id}");
|
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.dialogManager.hideMobileActionsOverlay();
|
||||||
_ffi.recordingModel.onClose();
|
_ffi.recordingModel.onClose();
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
@ -153,8 +197,23 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: RawKeyFocusScope(
|
child: RawKeyFocusScope(
|
||||||
focusNode: _rawKeyFocusNode,
|
focusNode: _rawKeyFocusNode,
|
||||||
onFocusChange: (bool v) {
|
onFocusChange: (bool imageFocused) {
|
||||||
_imageFocused = v;
|
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,
|
inputModel: _ffi.inputModel,
|
||||||
child: getBodyForDesktop(context)));
|
child: getBodyForDesktop(context)));
|
||||||
@ -181,9 +240,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void enterView(PointerEnterEvent evt) {
|
void enterView(PointerEnterEvent evt) {
|
||||||
if (!_imageFocused) {
|
|
||||||
_rawKeyFocusNode.requestFocus();
|
|
||||||
}
|
|
||||||
_cursorOverImage.value = true;
|
_cursorOverImage.value = true;
|
||||||
_firstEnterImage.value = true;
|
_firstEnterImage.value = true;
|
||||||
if (_onEnterOrLeaveImage4Menubar != null) {
|
if (_onEnterOrLeaveImage4Menubar != null) {
|
||||||
@ -193,7 +249,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) {
|
void leaveView(PointerExitEvent evt) {
|
||||||
@ -206,7 +268,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) {
|
Widget getBodyForDesktop(BuildContext context) {
|
||||||
@ -228,6 +293,21 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
listenerBuilder: (child) => RawPointerMouseRegion(
|
listenerBuilder: (child) => RawPointerMouseRegion(
|
||||||
onEnter: enterView,
|
onEnter: enterView,
|
||||||
onExit: leaveView,
|
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,
|
inputModel: _ffi.inputModel,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
@ -235,9 +315,9 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!_ffi.canvasModel.cursorEmbeded) {
|
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||||
paints.add(Obx(() => Visibility(
|
paints.add(Obx(() => Offstage(
|
||||||
visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue,
|
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
||||||
child: CursorPaint(
|
child: CursorPaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
zoomCursor: _zoomCursor,
|
zoomCursor: _zoomCursor,
|
||||||
@ -302,7 +382,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
|
|
||||||
mouseRegion({child}) => Obx(() => MouseRegion(
|
mouseRegion({child}) => Obx(() => MouseRegion(
|
||||||
cursor: cursorOverImage.isTrue
|
cursor: cursorOverImage.isTrue
|
||||||
? c.cursorEmbeded
|
? c.cursorEmbedded
|
||||||
? SystemMouseCursors.none
|
? SystemMouseCursors.none
|
||||||
: keyboardEnabled.isTrue
|
: keyboardEnabled.isTrue
|
||||||
? (() {
|
? (() {
|
||||||
@ -322,35 +402,36 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
onHover: (evt) {},
|
onHover: (evt) {},
|
||||||
child: child));
|
child: child));
|
||||||
|
|
||||||
if (c.scrollStyle == ScrollStyle.scrollbar) {
|
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||||
final imageWidth = c.getDisplayWidth() * s;
|
final imageWidth = c.getDisplayWidth() * s;
|
||||||
final imageHeight = c.getDisplayHeight() * s;
|
final imageHeight = c.getDisplayHeight() * s;
|
||||||
|
final imageSize = Size(imageWidth, imageHeight);
|
||||||
final imageWidget = CustomPaint(
|
final imageWidget = CustomPaint(
|
||||||
size: Size(imageWidth, imageHeight),
|
size: imageSize,
|
||||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||||
);
|
);
|
||||||
|
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
final percentX = _horizontal.hasClients
|
final percentX = _horizontal.hasClients
|
||||||
? _horizontal.position.extentBefore /
|
? _horizontal.position.extentBefore /
|
||||||
(_horizontal.position.extentBefore +
|
(_horizontal.position.extentBefore +
|
||||||
_horizontal.position.extentInside +
|
_horizontal.position.extentInside +
|
||||||
_horizontal.position.extentAfter)
|
_horizontal.position.extentAfter)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
final percentY = _vertical.hasClients
|
final percentY = _vertical.hasClients
|
||||||
? _vertical.position.extentBefore /
|
? _vertical.position.extentBefore /
|
||||||
(_vertical.position.extentBefore +
|
(_vertical.position.extentBefore +
|
||||||
_vertical.position.extentInside +
|
_vertical.position.extentInside +
|
||||||
_vertical.position.extentAfter)
|
_vertical.position.extentAfter)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
c.setScrollPercent(percentX, percentY);
|
c.setScrollPercent(percentX, percentY);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: mouseRegion(
|
child: mouseRegion(
|
||||||
child: _buildCrossScrollbar(context, _buildListener(imageWidget),
|
child: Obx(() => _buildCrossScrollbarFromLayout(
|
||||||
Size(imageWidth, imageHeight))),
|
context, _buildListener(imageWidget), c.size, imageSize)),
|
||||||
);
|
));
|
||||||
} else {
|
} else {
|
||||||
final imageWidget = CustomPaint(
|
final imageWidget = CustomPaint(
|
||||||
size: Size(c.size.width, c.size.height),
|
size: Size(c.size.width, c.size.height),
|
||||||
@ -366,15 +447,23 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
return MouseCursor.defer;
|
return MouseCursor.defer;
|
||||||
} else {
|
} else {
|
||||||
final key = cache.updateGetKey(scale, zoomCursor.value);
|
final key = cache.updateGetKey(scale, zoomCursor.value);
|
||||||
cursor.addKey(key);
|
if (!cursor.cachedKeys.contains(key)) {
|
||||||
return FlutterCustomMemoryImageCursor(
|
debugPrint("Register custom cursor with key $key");
|
||||||
pixbuf: cache.data,
|
// [Safety]
|
||||||
key: key,
|
// It's ok to call async registerCursor in current synchronous context,
|
||||||
hotx: cache.hotx,
|
// because activating the cursor is also an async call and will always
|
||||||
hoty: cache.hoty,
|
// be executed after this.
|
||||||
imageWidth: (cache.width * cache.scale).toInt(),
|
custom_cursor_manager.CursorManager.instance
|
||||||
imageHeight: (cache.height * cache.scale).toInt(),
|
.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 +566,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
return widget;
|
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) {
|
Widget _buildListener(Widget child) {
|
||||||
if (listenerBuilder != null) {
|
if (listenerBuilder != null) {
|
||||||
return listenerBuilder!(child);
|
return listenerBuilder!(child);
|
||||||
@ -529,7 +600,8 @@ class CursorPaint extends StatelessWidget {
|
|||||||
|
|
||||||
double cx = c.x;
|
double cx = c.x;
|
||||||
double cy = c.y;
|
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 d = c.parent.target!.ffiModel.display;
|
||||||
final imageWidth = d.width * c.scale;
|
final imageWidth = d.width * c.scale;
|
||||||
final imageHeight = d.height * c.scale;
|
final imageHeight = d.height * c.scale;
|
||||||
@ -538,7 +610,7 @@ class CursorPaint extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double x = (m.x - hotx) * c.scale + cx;
|
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;
|
double scale = 1.0;
|
||||||
if (zoomCursor.isTrue) {
|
if (zoomCursor.isTrue) {
|
||||||
x = m.x - hotx + cx / c.scale;
|
x = m.x - hotx + cx / c.scale;
|
||||||
|
@ -257,7 +257,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!ffi.canvasModel.cursorEmbeded) {
|
if (!ffi.canvasModel.cursorEmbedded) {
|
||||||
menu.add(MenuEntryDivider<String>());
|
menu.add(MenuEntryDivider<String>());
|
||||||
menu.add(() {
|
menu.add(() {
|
||||||
final state = ShowRemoteCursorState.find(key);
|
final state = ShowRemoteCursorState.find(key);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/remote_tab_page.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/desktop/widgets/refresh_wrapper.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@ -8,7 +9,9 @@ import 'package:provider/provider.dart';
|
|||||||
class DesktopRemoteScreen extends StatelessWidget {
|
class DesktopRemoteScreen extends StatelessWidget {
|
||||||
final Map<String, dynamic> params;
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key);
|
DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) {
|
||||||
|
bind.mainStartGrabKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
224
flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
|
||||||
|
typedef KBChoosedCallback = 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 choosedType;
|
||||||
|
const _KBImage({
|
||||||
|
Key? key,
|
||||||
|
required this.kbLayoutType,
|
||||||
|
required this.imageWidth,
|
||||||
|
required this.choosedType,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(_kBorderRadius),
|
||||||
|
border: Border.all(
|
||||||
|
color: choosedType.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 choosedType;
|
||||||
|
final KBChoosedCallback cb;
|
||||||
|
const _KBChooser({
|
||||||
|
Key? key,
|
||||||
|
required this.kbLayoutType,
|
||||||
|
required this.imageWidth,
|
||||||
|
required this.choosedType,
|
||||||
|
required this.cb,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
onChanged(String? v) async {
|
||||||
|
if (v != null) {
|
||||||
|
if (await cb(v)) {
|
||||||
|
choosedType.value = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
onChanged(kbLayoutType);
|
||||||
|
},
|
||||||
|
child: _KBImage(
|
||||||
|
kbLayoutType: kbLayoutType,
|
||||||
|
imageWidth: imageWidth,
|
||||||
|
choosedType: choosedType,
|
||||||
|
),
|
||||||
|
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Obx(() => Radio(
|
||||||
|
splashRadius: 0,
|
||||||
|
value: kbLayoutType,
|
||||||
|
groupValue: choosedType.value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
)),
|
||||||
|
Text(kbLayoutType),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
onChanged(kbLayoutType);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KBLayoutTypeChooser extends StatelessWidget {
|
||||||
|
final RxString choosedType;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final double dividerWidth;
|
||||||
|
final KBChoosedCallback cb;
|
||||||
|
KBLayoutTypeChooser({
|
||||||
|
Key? key,
|
||||||
|
required this.choosedType,
|
||||||
|
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,
|
||||||
|
choosedType: choosedType,
|
||||||
|
cb: cb,
|
||||||
|
),
|
||||||
|
VerticalDivider(
|
||||||
|
width: dividerWidth * 2,
|
||||||
|
),
|
||||||
|
_KBChooser(
|
||||||
|
kbLayoutType: _kKBLayoutTypeNotISO,
|
||||||
|
imageWidth: imageWidth,
|
||||||
|
choosedType: choosedType,
|
||||||
|
cb: cb,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RxString KBLayoutType = ''.obs;
|
||||||
|
|
||||||
|
String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
||||||
|
String localPlatform = '';
|
||||||
|
if (peerPlatform != 'Mac OS') {
|
||||||
|
return localPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
localPlatform = 'Windows';
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
localPlatform = 'Linux';
|
||||||
|
}
|
||||||
|
// 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(
|
||||||
|
choosedType: 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: [msgBoxButton(translate('Close'), 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) {
|
|
||||||
debugPrintStack(label: 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,
|
this.enabled,
|
||||||
});
|
});
|
||||||
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
|
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> {
|
class MenuEntryDivider<T> extends MenuEntryBase<T> {
|
||||||
@ -189,54 +198,76 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
|||||||
|
|
||||||
mod_menu.PopupMenuEntry<T> _buildMenuItem(
|
mod_menu.PopupMenuEntry<T> _buildMenuItem(
|
||||||
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
|
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(
|
return mod_menu.PopupMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
height: conf.height,
|
height: conf.height,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: conf.boxWidth,
|
width: conf.boxWidth,
|
||||||
child: TextButton(
|
child: opt.enabled == null
|
||||||
child: Container(
|
? TextButton(
|
||||||
padding: padding,
|
child: child,
|
||||||
alignment: AlignmentDirectional.centerStart,
|
onPressed: onPressed,
|
||||||
constraints: BoxConstraints(
|
)
|
||||||
minHeight: conf.height, maxHeight: conf.height),
|
: Obx(() => TextButton(
|
||||||
child: Row(
|
child: child,
|
||||||
children: [
|
onPressed: opt.enabled!.isTrue ? onPressed : null,
|
||||||
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);
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,12 +598,9 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
|||||||
const SizedBox(width: MenuConfig.midPadding),
|
const SizedBox(width: MenuConfig.midPadding),
|
||||||
Obx(() => Text(
|
Obx(() => Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(
|
style: super.enabled!.value
|
||||||
color: super.enabled!.value
|
? enabledStyle(context)
|
||||||
? Theme.of(context).textTheme.titleLarge?.color
|
: disabledStyle(),
|
||||||
: Colors.grey,
|
|
||||||
fontSize: MenuConfig.fontSize,
|
|
||||||
fontWeight: FontWeight.normal),
|
|
||||||
)),
|
)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
@ -605,14 +633,6 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildChild(BuildContext context, MenuConfig conf) {
|
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;
|
super.enabled ??= true.obs;
|
||||||
return Obx(() => Container(
|
return Obx(() => Container(
|
||||||
width: conf.boxWidth,
|
width: conf.boxWidth,
|
||||||
@ -631,7 +651,7 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
|||||||
constraints:
|
constraints:
|
||||||
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
||||||
child: childBuilder(
|
child: childBuilder(
|
||||||
super.enabled!.value ? enabledStyle : disabledStyle),
|
super.enabled!.value ? enabledStyle(context) : disabledStyle()),
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import '../../models/platform_model.dart';
|
|||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import './popup_menu.dart';
|
import './popup_menu.dart';
|
||||||
import './material_mod_popup_menu.dart' as mod_menu;
|
import './material_mod_popup_menu.dart' as mod_menu;
|
||||||
|
import './kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
class MenubarState {
|
class MenubarState {
|
||||||
final kStoreKey = 'remoteMenubarState';
|
final kStoreKey = 'remoteMenubarState';
|
||||||
@ -171,6 +172,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// No need to use future builder here.
|
||||||
|
_updateScreen();
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Obx(() => show.value
|
child: Obx(() => show.value
|
||||||
@ -362,8 +365,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
RxInt display = CurrentDisplayState.find(widget.id);
|
RxInt display = CurrentDisplayState.find(widget.id);
|
||||||
if (display.value != i) {
|
if (display.value != i) {
|
||||||
bind.sessionSwitchDisplay(id: widget.id, value: i);
|
bind.sessionSwitchDisplay(id: widget.id, value: i);
|
||||||
pi.currentDisplay = i;
|
|
||||||
display.value = i;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -569,7 +570,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
|
// {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) {
|
if (auditServer.isNotEmpty) {
|
||||||
displayMenu.add(
|
displayMenu.add(
|
||||||
MenuEntryButton<String>(
|
MenuEntryButton<String>(
|
||||||
@ -697,12 +699,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
if (_screen == null) {
|
if (_screen == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
double scale = _screen!.scaleFactor;
|
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
|
||||||
double selfWidth = _screen!.frame.width;
|
double selfWidth = _screen!.visibleFrame.width;
|
||||||
double selfHeight = _screen!.frame.height;
|
double selfHeight = _screen!.visibleFrame.height;
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
selfWidth = _screen!.visibleFrame.width;
|
selfWidth = _screen!.frame.width;
|
||||||
selfHeight = _screen!.visibleFrame.height;
|
selfHeight = _screen!.frame.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
final canvasModel = widget.ffi.canvasModel;
|
final canvasModel = widget.ffi.canvasModel;
|
||||||
@ -827,7 +829,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
qualityInitValue = qualityMaxValue;
|
qualityInitValue = qualityMaxValue;
|
||||||
}
|
}
|
||||||
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
||||||
final debouncerQuanlity = Debouncer<double>(
|
final debouncerQuality = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: (double v) {
|
||||||
setCustomValues(quality: v);
|
setCustomValues(quality: v);
|
||||||
@ -843,7 +845,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
divisions: 90,
|
divisions: 90,
|
||||||
onChanged: (double value) {
|
onChanged: (double value) {
|
||||||
qualitySliderValue.value = value;
|
qualitySliderValue.value = value;
|
||||||
debouncerQuanlity.value = value;
|
debouncerQuality.value = value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -934,11 +936,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
text: translate('ScrollAuto'),
|
text: translate('ScrollAuto'),
|
||||||
value: kRemoteScrollStyleAuto,
|
value: kRemoteScrollStyleAuto,
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: widget.ffi.canvasModel.imageOverflow,
|
||||||
),
|
),
|
||||||
MenuEntryRadioOption(
|
MenuEntryRadioOption(
|
||||||
text: translate('Scrollbar'),
|
text: translate('Scrollbar'),
|
||||||
value: kRemoteScrollStyleBar,
|
value: kRemoteScrollStyleBar,
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: widget.ffi.canvasModel.imageOverflow,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
curOptionGetter: () async =>
|
curOptionGetter: () async =>
|
||||||
@ -984,15 +988,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
||||||
|
|
||||||
final canvasModel = widget.ffi.canvasModel;
|
final canvasModel = widget.ffi.canvasModel;
|
||||||
final width = (canvasModel.getDisplayWidth() +
|
final width =
|
||||||
canvasModel.windowBorderWidth * 2) *
|
(canvasModel.getDisplayWidth() * canvasModel.scale +
|
||||||
scale +
|
canvasModel.windowBorderWidth * 2) *
|
||||||
magicWidth;
|
scale +
|
||||||
final height = (canvasModel.getDisplayHeight() +
|
magicWidth;
|
||||||
canvasModel.tabBarHeight +
|
final height =
|
||||||
canvasModel.windowBorderWidth * 2) *
|
(canvasModel.getDisplayHeight() * canvasModel.scale +
|
||||||
scale +
|
canvasModel.tabBarHeight +
|
||||||
magicHeight;
|
canvasModel.windowBorderWidth * 2) *
|
||||||
|
scale +
|
||||||
|
magicHeight;
|
||||||
double left = wndRect.left + (wndRect.width - width) / 2;
|
double left = wndRect.left + (wndRect.width - width) / 2;
|
||||||
double top = wndRect.top + (wndRect.height - height) / 2;
|
double top = wndRect.top + (wndRect.height - height) / 2;
|
||||||
|
|
||||||
@ -1032,7 +1038,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
final h265 = codecsJson['h265'] ?? false;
|
final h265 = codecsJson['h265'] ?? false;
|
||||||
codecs.add(h264);
|
codecs.add(h264);
|
||||||
codecs.add(h265);
|
codecs.add(h265);
|
||||||
} finally {}
|
} catch (e) {
|
||||||
|
debugPrint("Show Codec Preference err=$e");
|
||||||
|
}
|
||||||
if (codecs.length == 2 && (codecs[0] || codecs[1])) {
|
if (codecs.length == 2 && (codecs[0] || codecs[1])) {
|
||||||
displayMenu.add(MenuEntryRadios<String>(
|
displayMenu.add(MenuEntryRadios<String>(
|
||||||
text: translate('Codec Preference'),
|
text: translate('Codec Preference'),
|
||||||
@ -1082,7 +1090,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show remote cursor
|
/// Show remote cursor
|
||||||
if (!widget.ffi.canvasModel.cursorEmbeded) {
|
if (!widget.ffi.canvasModel.cursorEmbedded) {
|
||||||
displayMenu.add(() {
|
displayMenu.add(() {
|
||||||
final state = ShowRemoteCursorState.find(widget.id);
|
final state = ShowRemoteCursorState.find(widget.id);
|
||||||
return MenuEntrySwitch2<String>(
|
return MenuEntrySwitch2<String>(
|
||||||
@ -1182,22 +1190,70 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<MenuEntryBase<String>> _getKeyboardMenu() {
|
List<MenuEntryBase<String>> _getKeyboardMenu() {
|
||||||
final keyboardMenu = [
|
final List<MenuEntryBase<String>> keyboardMenu = [
|
||||||
MenuEntryRadios<String>(
|
MenuEntryRadios<String>(
|
||||||
text: translate('Ratio'),
|
text: translate('Ratio'),
|
||||||
optionsGetter: () => [
|
optionsGetter: () => [
|
||||||
MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'),
|
MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'),
|
||||||
MenuEntryRadioOption(text: translate('Map mode'), value: 'map'),
|
MenuEntryRadioOption(text: translate('Map mode'), value: 'map'),
|
||||||
],
|
],
|
||||||
curOptionGetter: () async =>
|
curOptionGetter: () async {
|
||||||
await bind.sessionGetKeyboardName(id: widget.id),
|
return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
|
||||||
|
},
|
||||||
optionSetter: (String oldValue, String newValue) async {
|
optionSetter: (String oldValue, String newValue) async {
|
||||||
await bind.sessionSetKeyboardMode(
|
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
|
||||||
id: widget.id, keyboardMode: 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;
|
return keyboardMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1357,10 +1413,10 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_DraggableShowHide> createState() => __DraggableShowHideState();
|
State<_DraggableShowHide> createState() => _DraggableShowHideState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class __DraggableShowHideState extends State<_DraggableShowHide> {
|
class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||||
Offset position = Offset.zero;
|
Offset position = Offset.zero;
|
||||||
Size size = Size.zero;
|
Size size = Size.zero;
|
||||||
|
|
||||||
@ -1369,7 +1425,8 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
axis: Axis.horizontal,
|
axis: Axis.horizontal,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.drag_indicator,
|
Icons.drag_indicator,
|
||||||
size: 15,
|
size: 20,
|
||||||
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
feedback: widget,
|
feedback: widget,
|
||||||
onDragStarted: (() {
|
onDragStarted: (() {
|
||||||
@ -1412,7 +1469,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
}),
|
}),
|
||||||
child: Obx((() => Icon(
|
child: Obx((() => Icon(
|
||||||
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||||
size: 15,
|
size: 20,
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1425,7 +1482,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
border: Border.all(color: MyTheme.border),
|
border: Border.all(color: MyTheme.border),
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 15,
|
height: 20,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget {
|
|||||||
return _buildBlock(
|
return _buildBlock(
|
||||||
child: Obx(() => PageView(
|
child: Obx(() => PageView(
|
||||||
controller: state.value.pageController,
|
controller: state.value.pageController,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
children: state.value.tabs
|
children: state.value.tabs
|
||||||
.map((tab) => tab.page)
|
.map((tab) => tab.page)
|
||||||
.toList(growable: false))));
|
.toList(growable: false))));
|
||||||
@ -526,13 +527,19 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
void onWindowClose() async {
|
void onWindowClose() async {
|
||||||
// hide window on close
|
// hide window on close
|
||||||
if (widget.isMainWindow) {
|
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();
|
await windowManager.hide();
|
||||||
rustDeskWinManager.unregisterActiveWindow(0);
|
|
||||||
} else {
|
} else {
|
||||||
widget.onClose?.call();
|
// it's safe to hide the subwindow
|
||||||
await WindowController.fromWindowId(windowId!).hide();
|
await WindowController.fromWindowId(windowId!).hide();
|
||||||
rustDeskWinManager
|
await Future.wait([
|
||||||
.call(WindowType.Main, kWindowEventHide, {"id": windowId!});
|
rustDeskWinManager
|
||||||
|
.call(WindowType.Main, kWindowEventHide, {"id": windowId!}),
|
||||||
|
widget.onClose?.call() ?? Future.microtask(() => null)
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
super.onWindowClose();
|
super.onWindowClose();
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,7 @@ void runMainApp(bool startService) async {
|
|||||||
// await windowManager.ensureInitialized();
|
// await windowManager.ensureInitialized();
|
||||||
gFFI.serverModel.startService();
|
gFFI.serverModel.startService();
|
||||||
}
|
}
|
||||||
|
gFFI.userModel.refreshCurrentUser();
|
||||||
runApp(App());
|
runApp(App());
|
||||||
// restore the location of the main window before window hide or show
|
// restore the location of the main window before window hide or show
|
||||||
await restoreWindowPosition(WindowType.Main);
|
await restoreWindowPosition(WindowType.Main);
|
||||||
@ -195,6 +196,8 @@ void runMultiWindow(
|
|||||||
// no such appType
|
// no such appType
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
// show window from hidden status
|
||||||
|
WindowController.fromWindowId(windowId!).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void runConnectionManagerScreen(bool hide) async {
|
void runConnectionManagerScreen(bool hide) async {
|
||||||
|
@ -7,9 +7,8 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../common.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/peer_tab_page.dart';
|
||||||
import '../../common/widgets/peers_view.dart';
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
@ -258,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
|
|||||||
}
|
}
|
||||||
if (value == 'login') {
|
if (value == 'login') {
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
if (gFFI.userModel.userName.value.isEmpty) {
|
||||||
showLogin(gFFI.dialogManager);
|
loginDialog();
|
||||||
} else {
|
} else {
|
||||||
gFFI.userModel.logOut();
|
gFFI.userModel.logOut();
|
||||||
}
|
}
|
||||||
|
@ -518,7 +518,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (!gFFI.canvasModel.cursorEmbeded) {
|
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint());
|
||||||
}
|
}
|
||||||
return paints;
|
return paints;
|
||||||
@ -527,7 +527,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
|
|
||||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
Widget getBodyForDesktopWithListener(bool keyboard) {
|
||||||
var paints = <Widget>[ImagePaint()];
|
var paints = <Widget>[ImagePaint()];
|
||||||
if (!gFFI.canvasModel.cursorEmbeded) {
|
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||||
final cursor = bind.sessionGetToggleOptionSync(
|
final cursor = bind.sessionGetToggleOptionSync(
|
||||||
id: widget.id, arg: 'show-remote-cursor');
|
id: widget.id, arg: 'show-remote-cursor');
|
||||||
if (keyboard || cursor) {
|
if (keyboard || cursor) {
|
||||||
@ -692,10 +692,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void changePhysicalKeyboardInputMode() async {
|
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) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
void setMode(String? v) async {
|
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 ?? '');
|
setState(() => current = v ?? '');
|
||||||
Future.delayed(Duration(milliseconds: 300), close);
|
Future.delayed(Duration(milliseconds: 300), close);
|
||||||
}
|
}
|
||||||
@ -977,7 +978,9 @@ void showOptions(
|
|||||||
final h265 = codecsJson['h265'] ?? false;
|
final h265 = codecsJson['h265'] ?? false;
|
||||||
codecs.add(h264);
|
codecs.add(h264);
|
||||||
codecs.add(h265);
|
codecs.add(h265);
|
||||||
} finally {}
|
} catch (e) {
|
||||||
|
debugPrint("Show Codec Preference err=$e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
@ -1055,7 +1058,7 @@ void showOptions(
|
|||||||
final toggles = [
|
final toggles = [
|
||||||
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
|
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
|
||||||
];
|
];
|
||||||
if (!gFFI.canvasModel.cursorEmbeded) {
|
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||||
toggles.insert(0,
|
toggles.insert(0,
|
||||||
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
|
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
|
|||||||
import 'package:zxing2/qrcode.dart';
|
import 'package:zxing2/qrcode.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../widgets/dialog.dart';
|
||||||
|
|
||||||
class ScanPage extends StatefulWidget {
|
class ScanPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_ScanPageState createState() => _ScanPageState();
|
State<ScanPage> createState() => _ScanPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScanPageState extends State<ScanPage> {
|
class _ScanPageState extends State<ScanPage> {
|
||||||
@ -42,9 +41,9 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
icon: Icon(Icons.image_search),
|
icon: Icon(Icons.image_search),
|
||||||
iconSize: 32.0,
|
iconSize: 32.0,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
final XFile? file =
|
final XFile? file =
|
||||||
await _picker.pickImage(source: ImageSource.gallery);
|
await picker.pickImage(source: ImageSource.gallery);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
var image = img.decodeNamedImage(
|
var image = img.decodeNamedImage(
|
||||||
File(file.path).readAsBytesSync(), file.path)!;
|
File(file.path).readAsBytesSync(), file.path)!;
|
||||||
@ -139,155 +138,12 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic> values = json.decode(data.substring(7));
|
final sc = ServerConfig.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 : '';
|
|
||||||
Timer(Duration(milliseconds: 60), () {
|
Timer(Duration(milliseconds: 60), () {
|
||||||
showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager);
|
showServerSettingsWithValue(sc, gFFI.dialogManager);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Invalid QR code');
|
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/material.dart';
|
||||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage> {
|
class _ServerPageState extends State<ServerPage> {
|
||||||
|
Timer? _updateTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
|
||||||
|
await gFFI.serverModel.fetchID();
|
||||||
|
});
|
||||||
gFFI.serverModel.checkAndroidPermission();
|
gFFI.serverModel.checkAndroidPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_updateTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
checkService();
|
checkService();
|
||||||
|
@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
|
import '../../common/widgets/login.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
@ -300,7 +301,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
leading: Icon(Icons.person),
|
leading: Icon(Icons.person),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
if (gFFI.userModel.userName.value.isEmpty) {
|
||||||
showLogin(gFFI.dialogManager);
|
loginDialog();
|
||||||
} else {
|
} else {
|
||||||
gFFI.userModel.logOut();
|
gFFI.userModel.logOut();
|
||||||
}
|
}
|
||||||
@ -391,17 +392,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||||
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
|
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
|
||||||
String id = options['custom-rendezvous-server'] ?? "";
|
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||||
String relay = options['relay-server'] ?? "";
|
|
||||||
String api = options['api-server'] ?? "";
|
|
||||||
String key = options['key'] ?? "";
|
|
||||||
showServerSettingsWithValue(id, relay, key, api, dialogManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||||
try {
|
try {
|
||||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
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) {
|
dialogManager.show((setState, close) {
|
||||||
setLang(v) {
|
setLang(v) {
|
||||||
if (lang != v) {
|
if (lang != v) {
|
||||||
@ -486,78 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) {
|
|||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, 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 {
|
class ScanButton extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
@ -236,6 +237,145 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: [
|
||||||
|
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 (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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
class PasswordWidget extends StatefulWidget {
|
class PasswordWidget extends StatefulWidget {
|
||||||
PasswordWidget({Key? key, required this.controller, this.autoFocus = true})
|
PasswordWidget({Key? key, required this.controller, this.autoFocus = true})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@ -285,7 +425,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
|||||||
color: Theme.of(context).primaryColorDark,
|
color: Theme.of(context).primaryColorDark,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Update the state i.e. toogle the state of passwordVisible variable
|
// Update the state i.e. toggle the state of passwordVisible variable
|
||||||
setState(() {
|
setState(() {
|
||||||
_passwordVisible = !_passwordVisible;
|
_passwordVisible = !_passwordVisible;
|
||||||
});
|
});
|
||||||
|
@ -21,16 +21,13 @@ class AbModel {
|
|||||||
|
|
||||||
AbModel(this.parent);
|
AbModel(this.parent);
|
||||||
|
|
||||||
FFI? get _ffi => parent.target;
|
|
||||||
|
|
||||||
Future<dynamic> pullAb() async {
|
Future<dynamic> pullAb() async {
|
||||||
if (_ffi!.userModel.userName.isEmpty) return;
|
if (gFFI.userModel.userName.isEmpty) return;
|
||||||
abLoading.value = true;
|
abLoading.value = true;
|
||||||
abError.value = "";
|
abError.value = "";
|
||||||
final api = "${await bind.mainGetApiServer()}/api/ab/get";
|
final api = "${await bind.mainGetApiServer()}/api/ab/get";
|
||||||
try {
|
try {
|
||||||
final resp =
|
final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
|
||||||
await http.post(Uri.parse(api), headers: await getHttpHeaders());
|
|
||||||
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
|
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
|
||||||
Map<String, dynamic> json = jsonDecode(resp.body);
|
Map<String, dynamic> json = jsonDecode(resp.body);
|
||||||
if (json.containsKey('error')) {
|
if (json.containsKey('error')) {
|
||||||
@ -63,7 +60,8 @@ class AbModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
Future<void> reset() async {
|
||||||
|
await bind.mainSetLocalOption(key: "selected-tags", value: '');
|
||||||
tags.clear();
|
tags.clear();
|
||||||
peers.clear();
|
peers.clear();
|
||||||
}
|
}
|
||||||
@ -103,7 +101,7 @@ class AbModel {
|
|||||||
Future<void> pushAb() async {
|
Future<void> pushAb() async {
|
||||||
abLoading.value = true;
|
abLoading.value = true;
|
||||||
final api = "${await bind.mainGetApiServer()}/api/ab";
|
final api = "${await bind.mainGetApiServer()}/api/ab";
|
||||||
var authHeaders = await getHttpHeaders();
|
var authHeaders = getHttpHeaders();
|
||||||
authHeaders['Content-Type'] = "application/json";
|
authHeaders['Content-Type'] = "application/json";
|
||||||
final peersJsonData = peers.map((e) => e.toJson()).toList();
|
final peersJsonData = peers.map((e) => e.toJson()).toList();
|
||||||
final body = jsonEncode({
|
final body = jsonEncode({
|
||||||
@ -188,9 +186,4 @@ class AbModel {
|
|||||||
await pushAb();
|
await pushAb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
|
||||||
peers.clear();
|
|
||||||
tags.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,6 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiveFileDir(Map<String, dynamic> evt) {
|
receiveFileDir(Map<String, dynamic> evt) {
|
||||||
debugPrint("recv file dir:$evt");
|
|
||||||
if (evt['is_local'] == "false") {
|
if (evt['is_local'] == "false") {
|
||||||
// init remote home, the connection will automatic read remote home when established,
|
// init remote home, the connection will automatic read remote home when established,
|
||||||
try {
|
try {
|
||||||
@ -237,7 +236,9 @@ class FileModel extends ChangeNotifier {
|
|||||||
debugPrint("init remote home:${fd.path}");
|
debugPrint("init remote home:${fd.path}");
|
||||||
_currentRemoteDir = fd;
|
_currentRemoteDir = fd;
|
||||||
}
|
}
|
||||||
} finally {}
|
} catch (e) {
|
||||||
|
debugPrint("receiveFileDir err=$e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
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.
|
/// Mouse button enum.
|
||||||
enum MouseButtons { left, right, wheel }
|
enum MouseButtons { left, right, wheel }
|
||||||
|
|
||||||
|
const _kMouseEventDown = 'mousedown';
|
||||||
|
const _kMouseEventUp = 'mouseup';
|
||||||
|
const _kMouseEventMove = 'mousemove';
|
||||||
|
|
||||||
extension ToString on MouseButtons {
|
extension ToString on MouseButtons {
|
||||||
String get value {
|
String get value {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@ -46,7 +50,7 @@ class InputModel {
|
|||||||
|
|
||||||
// mouse
|
// mouse
|
||||||
final isPhysicalMouse = false.obs;
|
final isPhysicalMouse = false.obs;
|
||||||
int _lastMouseDownButtons = 0;
|
int _lastButtons = 0;
|
||||||
Offset lastMousePos = Offset.zero;
|
Offset lastMousePos = Offset.zero;
|
||||||
|
|
||||||
get id => parent.target?.id ?? "";
|
get id => parent.target?.id ?? "";
|
||||||
@ -54,7 +58,7 @@ class InputModel {
|
|||||||
InputModel(this.parent);
|
InputModel(this.parent);
|
||||||
|
|
||||||
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
||||||
bind.sessionGetKeyboardName(id: id).then((result) {
|
bind.sessionGetKeyboardMode(id: id).then((result) {
|
||||||
keyboardMode = result.toString();
|
keyboardMode = result.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -183,20 +187,42 @@ class InputModel {
|
|||||||
|
|
||||||
Map<String, dynamic> getEvent(PointerEvent evt, String type) {
|
Map<String, dynamic> getEvent(PointerEvent evt, String type) {
|
||||||
final Map<String, dynamic> out = {};
|
final Map<String, dynamic> out = {};
|
||||||
out['type'] = type;
|
|
||||||
out['x'] = evt.position.dx;
|
out['x'] = evt.position.dx;
|
||||||
out['y'] = evt.position.dy;
|
out['y'] = evt.position.dy;
|
||||||
if (alt) out['alt'] = 'true';
|
if (alt) out['alt'] = 'true';
|
||||||
if (shift) out['shift'] = 'true';
|
if (shift) out['shift'] = 'true';
|
||||||
if (ctrl) out['ctrl'] = 'true';
|
if (ctrl) out['ctrl'] = 'true';
|
||||||
if (command) out['command'] = 'true';
|
if (command) out['command'] = 'true';
|
||||||
out['buttons'] = evt
|
|
||||||
.buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right)
|
// Check update event type and set buttons to be sent.
|
||||||
if (evt.buttons != 0) {
|
int buttons = _lastButtons;
|
||||||
_lastMouseDownButtons = evt.buttons;
|
if (type == _kMouseEventMove) {
|
||||||
|
// flutter may emit move event if one button is pressed and anoter 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 {
|
} else {
|
||||||
out['buttons'] = _lastMouseDownButtons;
|
if (evt.buttons != 0) {
|
||||||
|
buttons = evt.buttons;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_lastButtons = evt.buttons;
|
||||||
|
|
||||||
|
out['buttons'] = buttons;
|
||||||
|
out['type'] = type;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +286,7 @@ class InputModel {
|
|||||||
isPhysicalMouse.value = true;
|
isPhysicalMouse.value = true;
|
||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(getEvent(e, 'mousemove'));
|
handleMouse(getEvent(e, _kMouseEventMove));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,21 +351,21 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(getEvent(e, 'mousedown'));
|
handleMouse(getEvent(e, _kMouseEventDown));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPointUpImage(PointerUpEvent e) {
|
void onPointUpImage(PointerUpEvent e) {
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(getEvent(e, 'mouseup'));
|
handleMouse(getEvent(e, _kMouseEventUp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPointMoveImage(PointerMoveEvent e) {
|
void onPointMoveImage(PointerMoveEvent e) {
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(getEvent(e, 'mousemove'));
|
handleMouse(getEvent(e, _kMouseEventMove));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,13 +414,13 @@ class InputModel {
|
|||||||
var type = '';
|
var type = '';
|
||||||
var isMove = false;
|
var isMove = false;
|
||||||
switch (evt['type']) {
|
switch (evt['type']) {
|
||||||
case 'mousedown':
|
case _kMouseEventDown:
|
||||||
type = 'down';
|
type = 'down';
|
||||||
break;
|
break;
|
||||||
case 'mouseup':
|
case _kMouseEventUp:
|
||||||
type = 'up';
|
type = 'up';
|
||||||
break;
|
break;
|
||||||
case 'mousemove':
|
case _kMouseEventMove:
|
||||||
isMove = true;
|
isMove = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -440,15 +466,21 @@ class InputModel {
|
|||||||
evt['y'] = '${y.round()}';
|
evt['y'] = '${y.round()}';
|
||||||
var buttons = '';
|
var buttons = '';
|
||||||
switch (evt['buttons']) {
|
switch (evt['buttons']) {
|
||||||
case 1:
|
case kPrimaryMouseButton:
|
||||||
buttons = 'left';
|
buttons = 'left';
|
||||||
break;
|
break;
|
||||||
case 2:
|
case kSecondaryMouseButton:
|
||||||
buttons = 'right';
|
buttons = 'right';
|
||||||
break;
|
break;
|
||||||
case 4:
|
case kMiddleMouseButton:
|
||||||
buttons = 'wheel';
|
buttons = 'wheel';
|
||||||
break;
|
break;
|
||||||
|
case kBackMouseButton:
|
||||||
|
buttons = 'back';
|
||||||
|
break;
|
||||||
|
case kForwardMouseButton:
|
||||||
|
buttons = 'forward';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
evt['buttons'] = buttons;
|
evt['buttons'] = buttons;
|
||||||
bind.sessionSendMouse(id: id, msg: json.encode(evt));
|
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/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/file_model.dart';
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/group_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/models/user_model.dart';
|
import 'package:flutter_hbb/models/user_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_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:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:image/image.dart' as img2;
|
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:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../common/shared_state.dart';
|
import '../common/shared_state.dart';
|
||||||
@ -140,7 +142,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
setConnectionType(
|
setConnectionType(
|
||||||
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
handleSwitchDisplay(evt);
|
handleSwitchDisplay(evt, peerId);
|
||||||
} else if (name == 'cursor_data') {
|
} else if (name == 'cursor_data') {
|
||||||
await parent.target?.cursorModel.updateCursorData(evt);
|
await parent.target?.cursorModel.updateCursorData(evt);
|
||||||
} else if (name == 'cursor_id') {
|
} else if (name == 'cursor_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;
|
final oldOrientation = _display.width > _display.height;
|
||||||
var old = _pi.currentDisplay;
|
var old = _pi.currentDisplay;
|
||||||
_pi.currentDisplay = int.parse(evt['display']);
|
_pi.currentDisplay = int.parse(evt['display']);
|
||||||
@ -221,11 +223,17 @@ class FfiModel with ChangeNotifier {
|
|||||||
_display.y = double.parse(evt['y']);
|
_display.y = double.parse(evt['y']);
|
||||||
_display.width = int.parse(evt['width']);
|
_display.width = int.parse(evt['width']);
|
||||||
_display.height = int.parse(evt['height']);
|
_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) {
|
if (old != _pi.currentDisplay) {
|
||||||
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
// remote is mobile, and orientation changed
|
// remote is mobile, and orientation changed
|
||||||
if ((_display.width > _display.height) != oldOrientation) {
|
if ((_display.width > _display.height) != oldOrientation) {
|
||||||
gFFI.canvasModel.updateViewStyle();
|
gFFI.canvasModel.updateViewStyle();
|
||||||
@ -331,7 +339,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
d.y = d0['y'].toDouble();
|
d.y = d0['y'].toDouble();
|
||||||
d.width = d0['width'];
|
d.width = d0['width'];
|
||||||
d.height = d0['height'];
|
d.height = d0['height'];
|
||||||
d.cursorEmbeded = d0['cursor_embeded'] == 1;
|
d.cursorEmbedded = d0['cursor_embedded'] == 1;
|
||||||
_pi.displays.add(d);
|
_pi.displays.add(d);
|
||||||
}
|
}
|
||||||
if (_pi.currentDisplay < _pi.displays.length) {
|
if (_pi.currentDisplay < _pi.displays.length) {
|
||||||
@ -380,12 +388,22 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
final List<Function(String)> _callbacksOnFirstImage = [];
|
||||||
|
|
||||||
ImageModel(this.parent);
|
ImageModel(this.parent);
|
||||||
|
|
||||||
|
addCallbackOnFirstImage(Function(String) cb) =>
|
||||||
|
_callbacksOnFirstImage.add(cb);
|
||||||
|
|
||||||
onRgba(Uint8List rgba) {
|
onRgba(Uint8List rgba) {
|
||||||
if (_waitForImage[id]!) {
|
if (_waitForImage[id]!) {
|
||||||
_waitForImage[id] = false;
|
_waitForImage[id] = false;
|
||||||
parent.target?.dialogManager.dismissAll();
|
parent.target?.dialogManager.dismissAll();
|
||||||
|
if (isDesktop) {
|
||||||
|
for (final cb in _callbacksOnFirstImage) {
|
||||||
|
cb(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final pid = parent.target?.id;
|
final pid = parent.target?.id;
|
||||||
ui.decodeImageFromPixels(
|
ui.decodeImageFromPixels(
|
||||||
@ -495,7 +513,7 @@ class ViewStyle {
|
|||||||
|
|
||||||
double get scale {
|
double get scale {
|
||||||
double s = 1.0;
|
double s = 1.0;
|
||||||
if (style == 'adaptive') {
|
if (style == kRemoteViewStyleAdaptive) {
|
||||||
final s1 = width / displayWidth;
|
final s1 = width / displayWidth;
|
||||||
final s2 = height / displayHeight;
|
final s2 = height / displayHeight;
|
||||||
s = s1 < s2 ? s1 : s2;
|
s = s1 < s2 ? s1 : s2;
|
||||||
@ -511,6 +529,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double _y = 0;
|
double _y = 0;
|
||||||
// image scale
|
// image scale
|
||||||
double _scale = 1.0;
|
double _scale = 1.0;
|
||||||
|
Size _size = Size.zero;
|
||||||
// the tabbar over the image
|
// the tabbar over the image
|
||||||
// double tabBarHeight = 0.0;
|
// double tabBarHeight = 0.0;
|
||||||
// the window border's width
|
// the window border's width
|
||||||
@ -524,6 +543,8 @@ class CanvasModel with ChangeNotifier {
|
|||||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||||
ViewStyle _lastViewStyle = ViewStyle();
|
ViewStyle _lastViewStyle = ViewStyle();
|
||||||
|
|
||||||
|
final _imageOverflow = false.obs;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
CanvasModel(this.parent);
|
CanvasModel(this.parent);
|
||||||
@ -531,7 +552,12 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double get x => _x;
|
double get x => _x;
|
||||||
double get y => _y;
|
double get y => _y;
|
||||||
double get scale => _scale;
|
double get scale => _scale;
|
||||||
|
Size get size => _size;
|
||||||
ScrollStyle get scrollStyle => _scrollStyle;
|
ScrollStyle get scrollStyle => _scrollStyle;
|
||||||
|
ViewStyle get viewStyle => _lastViewStyle;
|
||||||
|
RxBool get imageOverflow => _imageOverflow;
|
||||||
|
|
||||||
|
_resetScroll() => setScrollPercent(0.0, 0.0);
|
||||||
|
|
||||||
setScrollPercent(double x, double y) {
|
setScrollPercent(double x, double y) {
|
||||||
_scrollX = x;
|
_scrollX = x;
|
||||||
@ -542,28 +568,44 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double get scrollY => _scrollY;
|
double get scrollY => _scrollY;
|
||||||
|
|
||||||
updateViewStyle() async {
|
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);
|
final style = await bind.sessionGetViewStyle(id: id);
|
||||||
if (style == null) {
|
if (style == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final sizeWidth = size.width;
|
|
||||||
final sizeHeight = size.height;
|
_size = getSize();
|
||||||
final displayWidth = getDisplayWidth();
|
final displayWidth = getDisplayWidth();
|
||||||
final displayHeight = getDisplayHeight();
|
final displayHeight = getDisplayHeight();
|
||||||
final viewStyle = ViewStyle(
|
final viewStyle = ViewStyle(
|
||||||
style: style,
|
style: style,
|
||||||
width: sizeWidth,
|
width: size.width,
|
||||||
height: sizeHeight,
|
height: size.height,
|
||||||
displayWidth: displayWidth,
|
displayWidth: displayWidth,
|
||||||
displayHeight: displayHeight,
|
displayHeight: displayHeight,
|
||||||
);
|
);
|
||||||
if (_lastViewStyle == viewStyle) {
|
if (_lastViewStyle == viewStyle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_lastViewStyle.style != viewStyle.style) {
|
||||||
|
_resetScroll();
|
||||||
|
}
|
||||||
_lastViewStyle = viewStyle;
|
_lastViewStyle = viewStyle;
|
||||||
_scale = viewStyle.scale;
|
_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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,8 +613,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
final style = await bind.sessionGetScrollStyle(id: id);
|
final style = await bind.sessionGetScrollStyle(id: id);
|
||||||
if (style == kRemoteScrollStyleBar) {
|
if (style == kRemoteScrollStyleBar) {
|
||||||
_scrollStyle = ScrollStyle.scrollbar;
|
_scrollStyle = ScrollStyle.scrollbar;
|
||||||
_scrollX = 0.0;
|
_resetScroll();
|
||||||
_scrollY = 0.0;
|
|
||||||
} else {
|
} else {
|
||||||
_scrollStyle = ScrollStyle.scrollauto;
|
_scrollStyle = ScrollStyle.scrollauto;
|
||||||
}
|
}
|
||||||
@ -586,8 +627,8 @@ class CanvasModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get cursorEmbeded =>
|
bool get cursorEmbedded =>
|
||||||
parent.target?.ffiModel.display.cursorEmbeded ?? false;
|
parent.target?.ffiModel.display.cursorEmbedded ?? false;
|
||||||
|
|
||||||
int getDisplayWidth() {
|
int getDisplayWidth() {
|
||||||
final defaultWidth = (isDesktop || isWebDesktop)
|
final defaultWidth = (isDesktop || isWebDesktop)
|
||||||
@ -606,14 +647,6 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||||
double get tabBarHeight => stateGlobal.tabBarHeight;
|
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) {
|
moveDesktopMouse(double x, double y) {
|
||||||
// On mobile platforms, move the canvas with the cursor.
|
// On mobile platforms, move the canvas with the cursor.
|
||||||
final dw = getDisplayWidth() * _scale;
|
final dw = getDisplayWidth() * _scale;
|
||||||
@ -1113,7 +1146,8 @@ class CursorModel with ChangeNotifier {
|
|||||||
_clearCache() {
|
_clearCache() {
|
||||||
final keys = {...cachedKeys};
|
final keys = {...cachedKeys};
|
||||||
for (var k in keys) {
|
for (var k in keys) {
|
||||||
customCursorController.freeCache(k);
|
debugPrint("deleting cursor with key $k");
|
||||||
|
CursorManager.instance.deleteCursor(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1220,6 +1254,7 @@ class FFI {
|
|||||||
late final ChatModel chatModel; // session
|
late final ChatModel chatModel; // session
|
||||||
late final FileModel fileModel; // session
|
late final FileModel fileModel; // session
|
||||||
late final AbModel abModel; // global
|
late final AbModel abModel; // global
|
||||||
|
late final GroupModel groupModel; // global
|
||||||
late final UserModel userModel; // global
|
late final UserModel userModel; // global
|
||||||
late final QualityMonitorModel qualityMonitorModel; // session
|
late final QualityMonitorModel qualityMonitorModel; // session
|
||||||
late final RecordingModel recordingModel; // recording
|
late final RecordingModel recordingModel; // recording
|
||||||
@ -1233,8 +1268,9 @@ class FFI {
|
|||||||
serverModel = ServerModel(WeakReference(this));
|
serverModel = ServerModel(WeakReference(this));
|
||||||
chatModel = ChatModel(WeakReference(this));
|
chatModel = ChatModel(WeakReference(this));
|
||||||
fileModel = FileModel(WeakReference(this));
|
fileModel = FileModel(WeakReference(this));
|
||||||
abModel = AbModel(WeakReference(this));
|
|
||||||
userModel = UserModel(WeakReference(this));
|
userModel = UserModel(WeakReference(this));
|
||||||
|
abModel = AbModel(WeakReference(this));
|
||||||
|
groupModel = GroupModel(WeakReference(this));
|
||||||
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
||||||
recordingModel = RecordingModel(WeakReference(this));
|
recordingModel = RecordingModel(WeakReference(this));
|
||||||
inputModel = InputModel(WeakReference(this));
|
inputModel = InputModel(WeakReference(this));
|
||||||
@ -1318,7 +1354,7 @@ class Display {
|
|||||||
double y = 0;
|
double y = 0;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
bool cursorEmbeded = false;
|
bool cursorEmbedded = false;
|
||||||
|
|
||||||
Display() {
|
Display() {
|
||||||
width = (isDesktop || isWebDesktop)
|
width = (isDesktop || isWebDesktop)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
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:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
@ -10,17 +11,19 @@ import 'model.dart';
|
|||||||
import 'platform_model.dart';
|
import 'platform_model.dart';
|
||||||
|
|
||||||
class UserModel {
|
class UserModel {
|
||||||
var userName = ''.obs;
|
final RxString userName = ''.obs;
|
||||||
|
final RxString groupName = ''.obs;
|
||||||
|
final RxBool isAdmin = false.obs;
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
UserModel(this.parent) {
|
UserModel(this.parent);
|
||||||
refreshCurrentUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
void refreshCurrentUser() async {
|
void refreshCurrentUser() async {
|
||||||
await getUserName();
|
|
||||||
final token = bind.mainGetLocalOption(key: 'access_token');
|
final token = bind.mainGetLocalOption(key: 'access_token');
|
||||||
if (token == '') return;
|
if (token == '') {
|
||||||
|
await _updateOtherModels();
|
||||||
|
return;
|
||||||
|
}
|
||||||
final url = await bind.mainGetApiServer();
|
final url = await bind.mainGetApiServer();
|
||||||
final body = {
|
final body = {
|
||||||
'id': await bind.mainGetMyId(),
|
'id': await bind.mainGetMyId(),
|
||||||
@ -35,96 +38,95 @@ class UserModel {
|
|||||||
body: json.encode(body));
|
body: json.encode(body));
|
||||||
final status = response.statusCode;
|
final status = response.statusCode;
|
||||||
if (status == 401 || status == 400) {
|
if (status == 401 || status == 400) {
|
||||||
resetToken();
|
reset();
|
||||||
return;
|
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) {
|
} catch (e) {
|
||||||
print('Failed to refreshCurrentUser: $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: 'access_token', value: '');
|
||||||
await bind.mainSetLocalOption(key: 'user_info', value: '');
|
await gFFI.abModel.reset();
|
||||||
|
await gFFI.groupModel.reset();
|
||||||
userName.value = '';
|
userName.value = '';
|
||||||
|
groupName.value = '';
|
||||||
|
statePeerTab.check();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _parseResp(String body) async {
|
Future<void> _parseAndUpdateUser(UserPayload user) async {
|
||||||
final data = json.decode(body);
|
userName.value = user.name;
|
||||||
final error = data['error'];
|
groupName.value = user.grp;
|
||||||
if (error != null) {
|
isAdmin.value = user.isAdmin;
|
||||||
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<String> getUserName() async {
|
Future<void> _updateOtherModels() async {
|
||||||
if (userName.isNotEmpty) {
|
await gFFI.abModel.pullAb();
|
||||||
return userName.value;
|
await gFFI.groupModel.pull();
|
||||||
}
|
|
||||||
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> logOut() async {
|
Future<void> logOut() async {
|
||||||
final tag = gFFI.dialogManager.showLoading(translate('Waiting'));
|
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 {
|
try {
|
||||||
final resp = await http.post(Uri.parse('$url/api/login'),
|
final url = await bind.mainGetApiServer();
|
||||||
headers: {'Content-Type': 'application/json'},
|
await http
|
||||||
body: jsonEncode({
|
.post(Uri.parse('$url/api/logout'),
|
||||||
'username': userName,
|
body: {
|
||||||
'password': pass,
|
'id': await bind.mainGetMyId(),
|
||||||
'id': await bind.mainGetMyId(),
|
'uuid': await bind.mainGetUuid(),
|
||||||
'uuid': await bind.mainGetUuid()
|
},
|
||||||
}));
|
headers: getHttpHeaders())
|
||||||
final body = jsonDecode(resp.body);
|
.timeout(Duration(seconds: 2));
|
||||||
bind.mainSetLocalOption(
|
} catch (e) {
|
||||||
key: 'access_token', value: body['access_token'] ?? '');
|
print("request /api/logout failed: err=$e");
|
||||||
bind.mainSetLocalOption(
|
} finally {
|
||||||
key: 'user_info', value: jsonEncode(body['user']));
|
await reset();
|
||||||
this.userName.value = body['user']?['name'] ?? '';
|
gFFI.dialogManager.dismissByTag(tag);
|
||||||
return body;
|
|
||||||
} catch (err) {
|
|
||||||
return {'error': '$err'};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 'dart:convert';
|
||||||
|
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@ -34,7 +35,7 @@ class RustDeskMultiWindowManager {
|
|||||||
static final instance = RustDeskMultiWindowManager._();
|
static final instance = RustDeskMultiWindowManager._();
|
||||||
|
|
||||||
final List<int> _activeWindows = List.empty(growable: true);
|
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? _remoteDesktopWindowId;
|
||||||
int? _fileTransferWindowId;
|
int? _fileTransferWindowId;
|
||||||
int? _portForwardWindowId;
|
int? _portForwardWindowId;
|
||||||
@ -191,41 +192,41 @@ class RustDeskMultiWindowManager {
|
|||||||
return _activeWindows;
|
return _activeWindows;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _notifyActiveWindow() {
|
Future<void> _notifyActiveWindow() async {
|
||||||
for (final callback in _windowActiveCallbacks) {
|
for (final callback in _windowActiveCallbacks) {
|
||||||
callback.call();
|
await callback.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerActiveWindow(int windowId) {
|
Future<void> registerActiveWindow(int windowId) async {
|
||||||
if (_activeWindows.contains(windowId)) {
|
if (_activeWindows.contains(windowId)) {
|
||||||
// ignore
|
// ignore
|
||||||
} else {
|
} else {
|
||||||
_activeWindows.add(windowId);
|
_activeWindows.add(windowId);
|
||||||
}
|
}
|
||||||
_notifyActiveWindow();
|
await _notifyActiveWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove active window which has [`windowId`]
|
/// Remove active window which has [`windowId`]
|
||||||
///
|
///
|
||||||
/// [Avaliability]
|
/// [Availability]
|
||||||
/// This function should only be called from main window.
|
/// This function should only be called from main window.
|
||||||
/// For other windows, please post a unregister(hide) event to main window handler:
|
/// For other windows, please post a unregister(hide) event to main window handler:
|
||||||
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
|
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
|
||||||
void unregisterActiveWindow(int windowId) {
|
Future<void> unregisterActiveWindow(int windowId) async {
|
||||||
if (!_activeWindows.contains(windowId)) {
|
if (!_activeWindows.contains(windowId)) {
|
||||||
// ignore
|
// ignore
|
||||||
} else {
|
} else {
|
||||||
_activeWindows.remove(windowId);
|
_activeWindows.remove(windowId);
|
||||||
}
|
}
|
||||||
_notifyActiveWindow();
|
await _notifyActiveWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerActiveWindowListener(VoidCallback callback) {
|
void registerActiveWindowListener(AsyncCallback callback) {
|
||||||
_windowActiveCallbacks.add(callback);
|
_windowActiveCallbacks.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void unregisterActiveWindowListener(VoidCallback callback) {
|
void unregisterActiveWindowListener(AsyncCallback callback) {
|
||||||
_windowActiveCallbacks.remove(callback);
|
_windowActiveCallbacks.remove(callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,15 @@ static void my_application_activate(GApplication* application) {
|
|||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
// we have custom window frame
|
// we have custom window frame
|
||||||
gtk_window_set_decorated(window, FALSE);
|
gtk_window_set_decorated(window, FALSE);
|
||||||
|
// try setting icon for rustdesk, which uses the system cache
|
||||||
|
GtkIconTheme* theme = gtk_icon_theme_get_default();
|
||||||
|
gint icons[4] = {256, 128, 64, 32};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
GdkPixbuf* icon = gtk_icon_theme_load_icon(theme, "rustdesk", icons[i], GTK_ICON_LOOKUP_NO_SVG, NULL);
|
||||||
|
if (icon != nullptr) {
|
||||||
|
gtk_window_set_icon(window, icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Use a header bar when running in GNOME as this is the common style used
|
// Use a header bar when running in GNOME as this is the common style used
|
||||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||||
// desktop).
|
// desktop).
|
||||||
|
@ -26,6 +26,8 @@
|
|||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; };
|
||||||
|
7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; };
|
||||||
84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
|
84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||||
84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; };
|
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; };
|
||||||
@ -74,6 +76,8 @@
|
|||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = "<group>"; };
|
||||||
|
7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = "<group>"; };
|
||||||
84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = "<group>"; };
|
84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
@ -127,6 +131,8 @@
|
|||||||
33CC11242044D66E0003C045 /* Resources */ = {
|
33CC11242044D66E0003C045 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */,
|
||||||
|
7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */,
|
||||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||||
@ -253,6 +259,8 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */,
|
||||||
|
7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */,
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||||
);
|
);
|
||||||
@ -378,7 +386,7 @@
|
|||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ARCHS = x86_64;
|
ARCHS = "$(ARCHS_STANDARD)";
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
@ -403,6 +411,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -428,8 +437,11 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -459,7 +471,7 @@
|
|||||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ARCHS = x86_64;
|
ARCHS = "$(ARCHS_STANDARD)";
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
@ -513,7 +525,7 @@
|
|||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ARCHS = x86_64;
|
ARCHS = "$(ARCHS_STANDARD)";
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
@ -538,6 +550,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -550,6 +563,12 @@
|
|||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"-sectcreate",
|
||||||
|
__CGPreLoginApp,
|
||||||
|
__cgpreloginapp,
|
||||||
|
/dev/null,
|
||||||
|
);
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
@ -563,8 +582,10 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -590,8 +611,11 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -602,6 +626,12 @@
|
|||||||
../../target/release,
|
../../target/release,
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"-sectcreate",
|
||||||
|
__CGPreLoginApp,
|
||||||
|
__cgpreloginapp,
|
||||||
|
/dev/null,
|
||||||
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
|
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
|
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 349 B |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 562 B |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 901 B |
@ -6,6 +6,8 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -49,7 +49,8 @@ class MainFlutterWindow: NSWindow {
|
|||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
}
|
}
|
||||||
|
|
||||||
// override func bitsdojo_window_configure() -> UInt {
|
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
|
||||||
// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
|
super.order(place, relativeTo: otherWin)
|
||||||
// }
|
hiddenWindowAtLaunch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -108,6 +108,12 @@
|
|||||||
PRODUCT_NAME = rustdesk;
|
PRODUCT_NAME = rustdesk;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"-sectcreate",
|
||||||
|
__CGPreLoginApp,
|
||||||
|
__cgpreloginapp,
|
||||||
|
/dev/null,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
@ -105,7 +105,7 @@ packages:
|
|||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.3"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -182,7 +182,7 @@ packages:
|
|||||||
name: code_builder
|
name: code_builder
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.4.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -352,7 +352,7 @@ packages:
|
|||||||
name: file_picker
|
name: file_picker
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.2"
|
version: "5.2.4"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -389,12 +389,10 @@ packages:
|
|||||||
flutter_custom_cursor:
|
flutter_custom_cursor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: flutter_custom_cursor
|
||||||
ref: "74b1b314142b6775c1243067a3503ac568ebc74b"
|
url: "https://pub.dartlang.org"
|
||||||
resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b"
|
source: hosted
|
||||||
url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor"
|
version: "0.0.2"
|
||||||
source: git
|
|
||||||
version: "0.0.1"
|
|
||||||
flutter_improved_scrolling:
|
flutter_improved_scrolling:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -455,7 +453,7 @@ packages:
|
|||||||
name: freezed
|
name: freezed
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.3.2"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -546,7 +544,7 @@ packages:
|
|||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.5+3"
|
version: "0.8.5+4"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -560,7 +558,7 @@ packages:
|
|||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.6+1"
|
version: "0.8.6+3"
|
||||||
image_picker_platform_interface:
|
image_picker_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -637,7 +635,7 @@ packages:
|
|||||||
name: mime
|
name: mime
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.3"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -826,7 +824,7 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.4"
|
version: "6.0.5"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1113,35 +1111,35 @@ packages:
|
|||||||
name: video_player
|
name: video_player
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.8"
|
version: "2.4.10"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_android
|
name: video_player_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.9"
|
version: "2.3.10"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.7"
|
version: "2.3.8"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_platform_interface
|
name: video_player_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.4"
|
version: "6.0.1"
|
||||||
video_player_web:
|
video_player_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_web
|
name: video_player_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.12"
|
version: "2.0.13"
|
||||||
visibility_detector:
|
visibility_detector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1204,7 +1202,7 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.3"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -63,12 +63,9 @@ dependencies:
|
|||||||
desktop_multi_window:
|
desktop_multi_window:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||||
ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75
|
ref: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124
|
||||||
freezed_annotation: ^2.0.3
|
freezed_annotation: ^2.0.3
|
||||||
flutter_custom_cursor:
|
flutter_custom_cursor: ^0.0.2
|
||||||
git:
|
|
||||||
url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor
|
|
||||||
ref: 74b1b314142b6775c1243067a3503ac568ebc74b
|
|
||||||
window_size:
|
window_size:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/google/flutter-desktop-embedding.git
|
url: https://github.com/google/flutter-desktop-embedding.git
|
||||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 12 KiB |