Merge branch 'rustdesk:master' into master

This commit is contained in:
Onyx47 2023-01-09 14:21:18 +01:00 committed by GitHub
commit 2bdfa15fd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
223 changed files with 8263 additions and 4004 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ libc = "0.2"
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } 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"

View File

@ -39,6 +39,7 @@ Below are the servers you are using for free, it may change along the time. If y
| Germany | Codext | 4 vCPU / 8GB RAM | | 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

View 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

View File

@ -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')))

View File

@ -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");
} }

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 267 KiB

View File

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

View File

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

1
flutter/.gitignore vendored
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -46,7 +46,7 @@ var isWebDesktop = false;
var version = ""; 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'] ?? "";
}

View File

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

View File

@ -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),

View File

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

View File

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

View File

@ -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) {

View File

@ -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(),
);
} }
} }

View File

@ -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,
);
}

View File

@ -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,
/* /*

View File

@ -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>{

View File

@ -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'),

View File

@ -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'],

View File

@ -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),

View File

@ -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();
}
});
}
} }

View File

@ -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;

View File

@ -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);

View File

@ -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) {

View 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,
);
});
}

View File

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

View File

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

View File

@ -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()),
), ),
))); )));
} }

View File

@ -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,
), ),
), ),

View File

@ -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();
} }

View File

@ -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 {

View File

@ -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();
} }

View File

@ -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'));
} }

View File

@ -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;
}

View File

@ -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();

View File

@ -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) {

View File

@ -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;
}); });

View File

@ -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();
}
} }

View File

@ -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();

View File

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

View File

@ -17,6 +17,10 @@ import './state_model.dart';
/// Mouse button enum. /// 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));

View File

@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/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)

View File

@ -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;
}
} }

View File

@ -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);
} }
} }

View File

@ -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).

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 901 B

View File

@ -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>

View File

@ -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()
}
} }

View File

@ -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>

View File

@ -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;
}; };

View File

@ -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:

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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