Merge branch 'rustdesk/master'
64
.github/workflows/ci.yml
vendored
@@ -78,11 +78,46 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
case ${{ matrix.job.target }} in
|
case ${{ matrix.job.target }} in
|
||||||
x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ;;
|
x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;;
|
||||||
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
|
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
|
||||||
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: 'stable'
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
target: ${{ matrix.job.target }}
|
||||||
|
override: true
|
||||||
|
profile: minimal # minimal component installation (ie, no documentation)
|
||||||
|
|
||||||
|
- name: Install flutter rust bridge deps
|
||||||
|
run: |
|
||||||
|
dart pub global activate ffigen --version 5.0.1
|
||||||
|
# flutter_rust_bridge
|
||||||
|
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
|
||||||
|
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Install corrosion
|
||||||
|
run: |
|
||||||
|
mkdir /tmp/corrosion
|
||||||
|
pushd /tmp/corrosion
|
||||||
|
git clone https://github.com/corrosion-rs/corrosion.git
|
||||||
|
# Optionally, specify -DCMAKE_INSTALL_PREFIX=<target-install-path>. You can install Corrosion anyway
|
||||||
|
cmake -Scorrosion -Bbuild -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build build --config Release
|
||||||
|
# This next step may require sudo or admin privileges if you're installing to a system location,
|
||||||
|
# which is the default.
|
||||||
|
sudo cmake --install build --config Release
|
||||||
|
popd
|
||||||
|
|
||||||
- name: Restore from cache and install vcpkg
|
- name: Restore from cache and install vcpkg
|
||||||
uses: lukka/run-vcpkg@v7
|
uses: lukka/run-vcpkg@v7
|
||||||
with:
|
with:
|
||||||
@@ -94,14 +129,6 @@ jobs:
|
|||||||
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
|
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
target: ${{ matrix.job.target }}
|
|
||||||
override: true
|
|
||||||
profile: minimal # minimal component installation (ie, no documentation)
|
|
||||||
|
|
||||||
- name: Show version information (Rust, cargo, GCC)
|
- name: Show version information (Rust, cargo, GCC)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -114,12 +141,19 @@ jobs:
|
|||||||
|
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
|
||||||
- name: Build
|
# - name: Build
|
||||||
uses: actions-rs/cargo@v1
|
# uses: actions-rs/cargo@v1
|
||||||
with:
|
# with:
|
||||||
use-cross: ${{ matrix.job.use-cross }}
|
# use-cross: ${{ matrix.job.use-cross }}
|
||||||
command: build
|
# command: build
|
||||||
args: --locked --release --target=${{ matrix.job.target }}
|
# args: --locked --release --target=${{ matrix.job.target }} --features flutter -v
|
||||||
|
|
||||||
|
- name: Build Flutter
|
||||||
|
run: |
|
||||||
|
pushd flutter
|
||||||
|
flutter pub get
|
||||||
|
flutter build linux --release -v
|
||||||
|
popd
|
||||||
|
|
||||||
# - name: Strip debug information from executable
|
# - name: Strip debug information from executable
|
||||||
# id: strip
|
# id: strip
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/build
|
||||||
/target
|
/target
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
BIN
128x128.png
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
128x128@2x.png
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 3.0 KiB |
BIN
32x32.png
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 504 B |
549
Cargo.lock
generated
11
Cargo.toml
@@ -24,8 +24,9 @@ appimage = []
|
|||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
|
flutter = ["flutter_rust_bridge"]
|
||||||
|
default = ["use_dasp","flutter"]
|
||||||
hwcodec = ["scrap/hwcodec"]
|
hwcodec = ["scrap/hwcodec"]
|
||||||
default = ["use_dasp"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -44,7 +45,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/open-trade/magnum-opus" }
|
magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/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 }
|
||||||
@@ -58,6 +59,8 @@ num_cpus = "1.13"
|
|||||||
bytes = { version = "1.2", features = ["serde"] }
|
bytes = { version = "1.2", features = ["serde"] }
|
||||||
default-net = "0.11.0"
|
default-net = "0.11.0"
|
||||||
wol-rs = "0.9.1"
|
wol-rs = "0.9.1"
|
||||||
|
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
|
||||||
|
errno = "0.2.8"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false }
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false }
|
||||||
@@ -113,7 +116,7 @@ android_logger = "0.11"
|
|||||||
jni = "0.19"
|
jni = "0.19"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||||
flutter_rust_bridge = "=1.30.0"
|
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc"]
|
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc"]
|
||||||
@@ -131,7 +134,7 @@ winapi = { version = "0.3", features = [ "winnt" ] }
|
|||||||
cc = "1.0"
|
cc = "1.0"
|
||||||
hbb_common = { path = "libs/hbb_common" }
|
hbb_common = { path = "libs/hbb_common" }
|
||||||
simple_rc = { path = "libs/simple_rc", optional = true }
|
simple_rc = { path = "libs/simple_rc", optional = true }
|
||||||
flutter_rust_bridge_codegen = "=1.30.0"
|
flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hound = "3.4"
|
hound = "3.4"
|
||||||
|
|||||||
4
PKGBUILD
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.1.9
|
pkgver=1.1.10
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
@@ -27,5 +27,5 @@ package() {
|
|||||||
install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files"
|
install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files"
|
||||||
install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files"
|
install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files"
|
||||||
install -Dm 644 $HBB/pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files"
|
install -Dm 644 $HBB/pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files"
|
||||||
install -Dm 644 $HBB/256-no-margin.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png"
|
install -Dm 644 $HBB/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ Below are the servers you are using for free, it may change along the time. If y
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself.
|
Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only.
|
||||||
|
|
||||||
|
Please download sciter dynamic library yourself.
|
||||||
|
|
||||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||||
|
|
||||||
Mobile versions use Flutter. We will migrate desktop version from Sciter to Flutter.
|
|
||||||
|
|
||||||
## Raw steps to build
|
## Raw steps to build
|
||||||
|
|
||||||
- Prepare your Rust development env and C++ build env
|
- Prepare your Rust development env and C++ build env
|
||||||
|
|||||||
195
build.py
@@ -66,6 +66,8 @@ def make_parser():
|
|||||||
default='',
|
default='',
|
||||||
help='Integrate features, windows only.'
|
help='Integrate features, windows only.'
|
||||||
'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
||||||
|
parser.add_argument('--flutter', action='store_true',
|
||||||
|
help='Build flutter package', default=False)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--hwcodec',
|
'--hwcodec',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@@ -114,9 +116,58 @@ def get_features(args):
|
|||||||
features.extend(get_rc_features(args))
|
features.extend(get_rc_features(args))
|
||||||
if args.hwcodec:
|
if args.hwcodec:
|
||||||
features.append('hwcodec')
|
features.append('hwcodec')
|
||||||
|
if args.flutter:
|
||||||
|
features.append('flutter')
|
||||||
print("features:", features)
|
print("features:", features)
|
||||||
return features
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
def build_flutter_deb(version):
|
||||||
|
os.chdir('flutter')
|
||||||
|
os.system('dpkg-deb -R rustdesk.deb tmpdeb')
|
||||||
|
# os.system('flutter build linux --release')
|
||||||
|
os.system('rm tmpdeb/usr/bin/rustdesk')
|
||||||
|
os.system('strip build/linux/x64/release/liblibrustdesk.so')
|
||||||
|
os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||||
|
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
|
os.system(
|
||||||
|
'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/')
|
||||||
|
os.system(
|
||||||
|
'pushd tmpdeb && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd')
|
||||||
|
os.system(
|
||||||
|
'cp build/linux/x64/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so')
|
||||||
|
os.system(
|
||||||
|
'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
|
os.system(
|
||||||
|
'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
|
os.system(
|
||||||
|
'cp ../pynput_service.py tmpdeb/usr/share/rustdesk/files/')
|
||||||
|
os.system(
|
||||||
|
'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
|
||||||
|
os.system(
|
||||||
|
'cp rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
|
os.system('mkdir -p tmpdeb/DEBIAN')
|
||||||
|
os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/')
|
||||||
|
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||||
|
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user')
|
||||||
|
md5_file('usr/share/rustdesk/files/pynput_service.py')
|
||||||
|
os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
||||||
|
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||||
|
os.chdir("..")
|
||||||
|
|
||||||
|
|
||||||
|
def build_flutter_arch_manjaro(version):
|
||||||
|
os.chdir('flutter')
|
||||||
|
os.system('flutter build linux --release')
|
||||||
|
os.system('strip build/linux/x64/release/liblibrustdesk.so')
|
||||||
|
os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version)
|
||||||
|
# pacman -S -needed base-devel
|
||||||
|
os.system('HBB=`pwd` makepkg -f')
|
||||||
|
os.system(
|
||||||
|
'mv rustdesk-%s-0-x86_64.pkg.tar.zst ../rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version))
|
||||||
|
os.chdir('..')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = make_parser()
|
parser = make_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -135,6 +186,7 @@ def main():
|
|||||||
os.system('git checkout src/ui/common.tis')
|
os.system('git checkout src/ui/common.tis')
|
||||||
version = get_version()
|
version = get_version()
|
||||||
features = ",".join(get_features(args))
|
features = ",".join(get_features(args))
|
||||||
|
flutter = args.flutter
|
||||||
if windows:
|
if windows:
|
||||||
os.system('cargo build --release --features ' + features)
|
os.system('cargo build --release --features ' + features)
|
||||||
# os.system('upx.exe target/release/rustdesk.exe')
|
# os.system('upx.exe target/release/rustdesk.exe')
|
||||||
@@ -146,15 +198,19 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print('Not signed')
|
print('Not signed')
|
||||||
os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe')
|
os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe')
|
||||||
elif os.path.isfile('/usr/bin/pacman'):
|
elif os.path.isfile('/usr/bin/pacman1'):
|
||||||
os.system('cargo build --release --features ' + features)
|
if flutter:
|
||||||
os.system('git checkout src/ui/common.tis')
|
build_flutter_arch_manjaro(version)
|
||||||
os.system('strip target/release/rustdesk')
|
else:
|
||||||
os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version)
|
# os.system('cargo build --release --features ' + features)
|
||||||
# pacman -S -needed base-devel
|
os.system('git checkout src/ui/common.tis')
|
||||||
os.system('HBB=`pwd` makepkg -f')
|
os.system('strip target/release/rustdesk')
|
||||||
os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version))
|
os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version)
|
||||||
# pacman -U ./rustdesk.pkg.tar.zst
|
# pacman -S -needed base-devel
|
||||||
|
os.system('HBB=`pwd` makepkg -f')
|
||||||
|
os.system(
|
||||||
|
'mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version))
|
||||||
|
# pacman -U ./rustdesk.pkg.tar.zst
|
||||||
elif os.path.isfile('/usr/bin/yum'):
|
elif os.path.isfile('/usr/bin/yum'):
|
||||||
os.system('cargo build --release --features ' + features)
|
os.system('cargo build --release --features ' + features)
|
||||||
os.system('strip target/release/rustdesk')
|
os.system('strip target/release/rustdesk')
|
||||||
@@ -172,62 +228,75 @@ def main():
|
|||||||
# yum localinstall rustdesk.rpm
|
# yum localinstall rustdesk.rpm
|
||||||
else:
|
else:
|
||||||
os.system('cargo bundle --release --features ' + features)
|
os.system('cargo bundle --release --features ' + features)
|
||||||
if osx:
|
if flutter:
|
||||||
os.system(
|
if osx:
|
||||||
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
|
# todo: OSX build
|
||||||
os.system(
|
pass
|
||||||
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
|
|
||||||
# https://github.com/sindresorhus/create-dmg
|
|
||||||
os.system('/bin/rm -rf *.dmg')
|
|
||||||
plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist"
|
|
||||||
txt = open(plist).read()
|
|
||||||
with open(plist, "wt") as fh:
|
|
||||||
fh.write(txt.replace("</dict>", """
|
|
||||||
<key>LSUIElement</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>"""))
|
|
||||||
pa = os.environ.get('P')
|
|
||||||
if pa:
|
|
||||||
os.system('''
|
|
||||||
# buggy: rcodesign sign ... path/*, have to sign one by one
|
|
||||||
#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
|
|
||||||
# goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:"
|
|
||||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
|
|
||||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
|
|
||||||
'''.format(pa))
|
|
||||||
os.system('create-dmg target/release/bundle/osx/RustDesk.app')
|
|
||||||
os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version)
|
|
||||||
if pa:
|
|
||||||
os.system('''
|
|
||||||
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg
|
|
||||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg
|
|
||||||
# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html
|
|
||||||
rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg
|
|
||||||
# verify: spctl -a -t exec -v /Applications/RustDesk.app
|
|
||||||
'''.format(pa, version))
|
|
||||||
else:
|
else:
|
||||||
print('Not signed')
|
os.system(
|
||||||
|
'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
|
||||||
|
build_flutter_deb(version)
|
||||||
else:
|
else:
|
||||||
os.system('mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
|
if osx:
|
||||||
os.system('dpkg-deb -R rustdesk.deb tmpdeb')
|
os.system(
|
||||||
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
|
||||||
os.system(
|
os.system(
|
||||||
'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
|
||||||
os.system(
|
# https://github.com/sindresorhus/create-dmg
|
||||||
'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/')
|
os.system('/bin/rm -rf *.dmg')
|
||||||
os.system('cp pynput_service.py tmpdeb/usr/share/rustdesk/files/')
|
plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist"
|
||||||
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
txt = open(plist).read()
|
||||||
os.system('strip tmpdeb/usr/bin/rustdesk')
|
with open(plist, "wt") as fh:
|
||||||
os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
|
fh.write(txt.replace("</dict>", """
|
||||||
os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
|
<key>LSUIElement</key>
|
||||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
<string>1</string>
|
||||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user')
|
</dict>"""))
|
||||||
md5_file('usr/share/rustdesk/files/pynput_service.py')
|
pa = os.environ.get('P')
|
||||||
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
|
if pa:
|
||||||
os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
os.system('''
|
||||||
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
# buggy: rcodesign sign ... path/*, have to sign one by one
|
||||||
|
#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
|
||||||
|
# goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:"
|
||||||
|
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
|
||||||
|
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
|
||||||
|
'''.format(pa))
|
||||||
|
os.system('create-dmg target/release/bundle/osx/RustDesk.app')
|
||||||
|
os.rename('RustDesk %s.dmg' %
|
||||||
|
version, 'rustdesk-%s.dmg' % version)
|
||||||
|
if pa:
|
||||||
|
os.system('''
|
||||||
|
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg
|
||||||
|
codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg
|
||||||
|
# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html
|
||||||
|
rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg
|
||||||
|
# verify: spctl -a -t exec -v /Applications/RustDesk.app
|
||||||
|
'''.format(pa, version))
|
||||||
|
else:
|
||||||
|
print('Not signed')
|
||||||
|
else:
|
||||||
|
# buid deb package
|
||||||
|
os.system(
|
||||||
|
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
|
||||||
|
os.system('dpkg-deb -R rustdesk.deb tmpdeb')
|
||||||
|
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
|
os.system(
|
||||||
|
'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
|
os.system(
|
||||||
|
'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
|
os.system(
|
||||||
|
'cp pynput_service.py tmpdeb/usr/share/rustdesk/files/')
|
||||||
|
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
||||||
|
os.system('strip tmpdeb/usr/bin/rustdesk')
|
||||||
|
os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||||
|
os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
|
||||||
|
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||||
|
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user')
|
||||||
|
md5_file('usr/share/rustdesk/files/pynput_service.py')
|
||||||
|
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
|
||||||
|
os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
||||||
|
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
||||||
os.system("mv Cargo.toml.bk Cargo.toml")
|
os.system("mv Cargo.toml.bk Cargo.toml")
|
||||||
os.system("mv src/main.rs.bk src/main.rs")
|
os.system("mv src/main.rs.bk src/main.rs")
|
||||||
|
|
||||||
|
|||||||
21
build.rs
@@ -77,15 +77,22 @@ fn install_oboe() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gen_flutter_rust_bridge() {
|
fn gen_flutter_rust_bridge() {
|
||||||
|
let llvm_path = match std::env::var("LLVM_HOME") {
|
||||||
|
Ok(path) => Some(vec![path]),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
// Tell Cargo that if the given file changes, to rerun this build script.
|
// Tell Cargo that if the given file changes, to rerun this build script.
|
||||||
println!("cargo:rerun-if-changed=src/mobile_ffi.rs");
|
println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
|
||||||
// settings for fbr_codegen
|
// settings for fbr_codegen
|
||||||
let opts = lib_flutter_rust_bridge_codegen::Opts {
|
let opts = lib_flutter_rust_bridge_codegen::Opts {
|
||||||
// Path of input Rust code
|
// Path of input Rust code
|
||||||
rust_input: "src/mobile_ffi.rs".to_string(),
|
rust_input: "src/flutter_ffi.rs".to_string(),
|
||||||
// Path of output generated Dart code
|
// Path of output generated Dart code
|
||||||
dart_output: "flutter/lib/generated_bridge.dart".to_string(),
|
dart_output: "flutter/lib/generated_bridge.dart".to_string(),
|
||||||
|
// Path of output generated C header
|
||||||
|
c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
|
||||||
// for other options lets use default
|
// for other options lets use default
|
||||||
|
llvm_path,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
// run fbr_codegen
|
// run fbr_codegen
|
||||||
@@ -96,11 +103,11 @@ fn main() {
|
|||||||
hbb_common::gen_version();
|
hbb_common::gen_version();
|
||||||
install_oboe();
|
install_oboe();
|
||||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
// let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
if target_os == "android" || target_os == "ios" {
|
// if target_os == "android" || target_os == "ios" {
|
||||||
gen_flutter_rust_bridge();
|
gen_flutter_rust_bridge();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
#[cfg(all(windows, feature = "with_rc"))]
|
#[cfg(all(windows, feature = "with_rc"))]
|
||||||
build_rc_source();
|
build_rc_source();
|
||||||
#[cfg(all(windows, feature = "inline"))]
|
#[cfg(all(windows, feature = "inline"))]
|
||||||
|
|||||||
13
flutter/.gitignore
vendored
@@ -45,15 +45,14 @@ jniLibs
|
|||||||
|
|
||||||
# flutter rust bridge
|
# flutter rust bridge
|
||||||
lib/generated_bridge.dart
|
lib/generated_bridge.dart
|
||||||
|
lib/generated_bridge.freezed.dart
|
||||||
|
|
||||||
# Flutter Generated Files
|
# Flutter Generated Files
|
||||||
linux/flutter/generated_plugin_registrant.cc
|
**/flutter/GeneratedPluginRegistrant.swift
|
||||||
linux/flutter/generated_plugin_registrant.h
|
**/flutter/generated_plugin_registrant.cc
|
||||||
linux/flutter/generated_plugins.cmake
|
**/flutter/generated_plugin_registrant.h
|
||||||
macos/Flutter/GeneratedPluginRegistrant.swift
|
**/flutter/generated_plugins.cmake
|
||||||
windows/flutter/generated_plugin_registrant.cc
|
**/Runner/bridge_generated.h
|
||||||
windows/flutter/generated_plugin_registrant.h
|
|
||||||
windows/flutter/generated_plugins.cmake
|
|
||||||
flutter_export_environment.sh
|
flutter_export_environment.sh
|
||||||
Flutter-Generated.xcconfig
|
Flutter-Generated.xcconfig
|
||||||
key.jks
|
key.jks
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d
|
revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
channel: stable
|
channel: stable
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
- platform: linux
|
||||||
|
create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
- platform: macos
|
||||||
|
create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
- platform: windows
|
||||||
|
create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
|
|||||||
16
flutter/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# flutter_hbb
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
29
flutter/analysis_options.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at
|
||||||
|
# https://dart-lang.github.io/linter/lints/index.html.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
BIN
flutter/assets/logo.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
flutter/assets/peer_searchbar.ttf
Normal file
BIN
flutter/assets/tabbar.ttf
Normal file
30
flutter/lib/cm_main.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'desktop/pages/server_page.dart';
|
||||||
|
import 'models/server_model.dart';
|
||||||
|
|
||||||
|
/// -t lib/cm_main.dart to test cm
|
||||||
|
void main(List<String> args) async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await windowManager.ensureInitialized();
|
||||||
|
await windowManager.setSize(Size(400, 600));
|
||||||
|
await windowManager.setAlignment(Alignment.topRight);
|
||||||
|
await initEnv(kAppTypeMain);
|
||||||
|
gFFI.serverModel.clients
|
||||||
|
.add(Client(0, false, false, "UserA", "123123123", true, false, false));
|
||||||
|
gFFI.serverModel.clients
|
||||||
|
.add(Client(1, false, false, "UserB", "221123123", true, false, false));
|
||||||
|
gFFI.serverModel.clients
|
||||||
|
.add(Client(2, false, false, "UserC", "331123123", true, false, false));
|
||||||
|
gFFI.serverModel.clients
|
||||||
|
.add(Client(3, false, false, "UserD", "441123123", true, false, false));
|
||||||
|
runApp(GetMaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
home: DesktopServerPage()));
|
||||||
|
}
|
||||||
@@ -1,25 +1,136 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:back_button_interceptor/back_button_interceptor.dart';
|
||||||
|
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 'dart:async';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'models/model.dart';
|
import 'models/model.dart';
|
||||||
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
final globalKey = GlobalKey<NavigatorState>();
|
final globalKey = GlobalKey<NavigatorState>();
|
||||||
final navigationBarKey = GlobalKey();
|
final navigationBarKey = GlobalKey();
|
||||||
|
|
||||||
var isAndroid = false;
|
final isAndroid = Platform.isAndroid;
|
||||||
var isIOS = false;
|
final isIOS = Platform.isIOS;
|
||||||
|
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||||
var isWeb = false;
|
var isWeb = false;
|
||||||
var isDesktop = false;
|
var isWebDesktop = false;
|
||||||
var version = "";
|
var version = "";
|
||||||
int androidVersion = 0;
|
int androidVersion = 0;
|
||||||
|
|
||||||
typedef F = String Function(String);
|
typedef F = String Function(String);
|
||||||
typedef FMethod = String Function(String, dynamic);
|
typedef FMethod = String Function(String, dynamic);
|
||||||
|
|
||||||
class Translator {
|
late final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode(
|
||||||
static late F call;
|
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII=")));
|
||||||
|
late final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII=')));
|
||||||
|
late final iconAudio = MemoryImage(Uint8List.fromList(base64Decode(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg==')));
|
||||||
|
late final iconFile = MemoryImage(Uint8List.fromList(base64Decode(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg==')));
|
||||||
|
late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC')));
|
||||||
|
|
||||||
|
class IconFont {
|
||||||
|
static const _family1 = 'Tabbar';
|
||||||
|
static const _family2 = 'PeerSearchbar';
|
||||||
|
IconFont._();
|
||||||
|
|
||||||
|
static const IconData max = IconData(0xe606, fontFamily: _family1);
|
||||||
|
static const IconData restore = IconData(0xe607, fontFamily: _family1);
|
||||||
|
static const IconData close = IconData(0xe668, fontFamily: _family1);
|
||||||
|
static const IconData min = IconData(0xe609, fontFamily: _family1);
|
||||||
|
static const IconData add = IconData(0xe664, fontFamily: _family1);
|
||||||
|
static const IconData menu = IconData(0xe628, fontFamily: _family1);
|
||||||
|
static const IconData search = IconData(0xe6a4, fontFamily: _family2);
|
||||||
|
static const IconData round_close = IconData(0xe6ed, fontFamily: _family2);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||||
|
const ColorThemeExtension({
|
||||||
|
required this.bg,
|
||||||
|
required this.grayBg,
|
||||||
|
required this.text,
|
||||||
|
required this.lightText,
|
||||||
|
required this.lighterText,
|
||||||
|
required this.placeholder,
|
||||||
|
required this.border,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color? bg;
|
||||||
|
final Color? grayBg;
|
||||||
|
final Color? text;
|
||||||
|
final Color? lightText;
|
||||||
|
final Color? lighterText;
|
||||||
|
final Color? placeholder;
|
||||||
|
final Color? border;
|
||||||
|
|
||||||
|
static const light = ColorThemeExtension(
|
||||||
|
bg: Color(0xFFFFFFFF),
|
||||||
|
grayBg: Color(0xFFEEEEEE),
|
||||||
|
text: Color(0xFF222222),
|
||||||
|
lightText: Color(0xFF666666),
|
||||||
|
lighterText: Color(0xFF888888),
|
||||||
|
placeholder: Color(0xFFAAAAAA),
|
||||||
|
border: Color(0xFFCCCCCC),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const dark = ColorThemeExtension(
|
||||||
|
bg: Color(0xFF252525),
|
||||||
|
grayBg: Color(0xFF141414),
|
||||||
|
text: Color(0xFFFFFFFF),
|
||||||
|
lightText: Color(0xFF999999),
|
||||||
|
lighterText: Color(0xFF777777),
|
||||||
|
placeholder: Color(0xFF555555),
|
||||||
|
border: Color(0xFF555555),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeExtension<ColorThemeExtension> copyWith(
|
||||||
|
{Color? bg,
|
||||||
|
Color? grayBg,
|
||||||
|
Color? text,
|
||||||
|
Color? lightText,
|
||||||
|
Color? lighterText,
|
||||||
|
Color? placeholder,
|
||||||
|
Color? border}) {
|
||||||
|
return ColorThemeExtension(
|
||||||
|
bg: bg ?? this.bg,
|
||||||
|
grayBg: grayBg ?? this.grayBg,
|
||||||
|
text: text ?? this.text,
|
||||||
|
lightText: lightText ?? this.lightText,
|
||||||
|
lighterText: lighterText ?? this.lighterText,
|
||||||
|
placeholder: placeholder ?? this.placeholder,
|
||||||
|
border: border ?? this.border,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeExtension<ColorThemeExtension> lerp(
|
||||||
|
ThemeExtension<ColorThemeExtension>? other, double t) {
|
||||||
|
if (other is! ColorThemeExtension) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return ColorThemeExtension(
|
||||||
|
bg: Color.lerp(bg, other.bg, t),
|
||||||
|
grayBg: Color.lerp(grayBg, other.grayBg, t),
|
||||||
|
text: Color.lerp(text, other.text, t),
|
||||||
|
lightText: Color.lerp(lightText, other.lightText, t),
|
||||||
|
lighterText: Color.lerp(lighterText, other.lighterText, t),
|
||||||
|
placeholder: Color.lerp(placeholder, other.placeholder, t),
|
||||||
|
border: Color.lerp(border, other.border, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyTheme {
|
class MyTheme {
|
||||||
@@ -34,6 +145,48 @@ class MyTheme {
|
|||||||
static const Color border = Color(0xFFCCCCCC);
|
static const Color border = Color(0xFFCCCCCC);
|
||||||
static const Color idColor = Color(0xFF00B6F0);
|
static const Color idColor = Color(0xFF00B6F0);
|
||||||
static const Color darkGray = Color(0xFFB9BABC);
|
static const Color darkGray = Color(0xFFB9BABC);
|
||||||
|
static const Color cmIdColor = Color(0xFF21790B);
|
||||||
|
static const Color dark = Colors.black87;
|
||||||
|
static const Color button = Color(0xFF2C8CFF);
|
||||||
|
static const Color hoverBorder = Color(0xFF999999);
|
||||||
|
|
||||||
|
static ThemeData lightTheme = ThemeData(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
|
tabBarTheme: TabBarTheme(
|
||||||
|
labelColor: Colors.black87,
|
||||||
|
),
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
).copyWith(
|
||||||
|
extensions: <ThemeExtension<dynamic>>[
|
||||||
|
ColorThemeExtension.light,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
static ThemeData darkTheme = ThemeData(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
|
tabBarTheme: TabBarTheme(
|
||||||
|
labelColor: Colors.white70,
|
||||||
|
),
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
).copyWith(
|
||||||
|
extensions: <ThemeExtension<dynamic>>[
|
||||||
|
ColorThemeExtension.dark,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
static ColorThemeExtension color(BuildContext context) {
|
||||||
|
return Theme.of(context).extension<ColorThemeExtension>()!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDarkTheme() {
|
||||||
|
final isDark = "Y" == Get.find<SharedPreferences>().getString("darkTheme");
|
||||||
|
return isDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
|
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
|
||||||
@@ -44,17 +197,151 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
void showToast(String text, {Duration? duration}) {
|
String formatDurationToTime(Duration duration) {
|
||||||
SmartDialog.showToast(text, displayTime: duration);
|
var totalTime = duration.inSeconds;
|
||||||
|
final secs = totalTime % 60;
|
||||||
|
totalTime = (totalTime - secs) ~/ 60;
|
||||||
|
final mins = totalTime % 60;
|
||||||
|
totalTime = (totalTime - mins) ~/ 60;
|
||||||
|
return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLoading(String text, {bool clickMaskDismiss = false}) {
|
closeConnection({String? id}) {
|
||||||
SmartDialog.dismiss();
|
if (isAndroid || isIOS) {
|
||||||
SmartDialog.showLoading(
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
clickMaskDismiss: false,
|
} else {
|
||||||
builder: (context) {
|
final controller = Get.find<DesktopTabController>();
|
||||||
return Container(
|
controller.closeBy(id);
|
||||||
color: MyTheme.white,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void window_on_top(int? id) {
|
||||||
|
if (id == null) {
|
||||||
|
// main window
|
||||||
|
windowManager.restore();
|
||||||
|
windowManager.show();
|
||||||
|
windowManager.focus();
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(id)
|
||||||
|
..focus()
|
||||||
|
..show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef DialogBuilder = CustomAlertDialog Function(
|
||||||
|
StateSetter setState, void Function([dynamic]) close);
|
||||||
|
|
||||||
|
class Dialog<T> {
|
||||||
|
OverlayEntry? entry;
|
||||||
|
Completer<T?> completer = Completer<T?>();
|
||||||
|
|
||||||
|
Dialog();
|
||||||
|
|
||||||
|
void complete(T? res) {
|
||||||
|
try {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(res);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Dialog complete catch error: $e");
|
||||||
|
} finally {
|
||||||
|
entry?.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverlayDialogManager {
|
||||||
|
OverlayState? _overlayState;
|
||||||
|
Map<String, Dialog> _dialogs = Map();
|
||||||
|
int _tagCount = 0;
|
||||||
|
|
||||||
|
/// By default OverlayDialogManager use global overlay
|
||||||
|
OverlayDialogManager() {
|
||||||
|
_overlayState = globalKey.currentState?.overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOverlayState(OverlayState? overlayState) {
|
||||||
|
_overlayState = overlayState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismissAll() {
|
||||||
|
_dialogs.forEach((key, value) {
|
||||||
|
value.complete(null);
|
||||||
|
BackButtonInterceptor.removeByName(key);
|
||||||
|
});
|
||||||
|
_dialogs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismissByTag(String tag) {
|
||||||
|
_dialogs[tag]?.complete(null);
|
||||||
|
_dialogs.remove(tag);
|
||||||
|
BackButtonInterceptor.removeByName(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> show<T>(DialogBuilder builder,
|
||||||
|
{bool clickMaskDismiss = false,
|
||||||
|
bool backDismiss = false,
|
||||||
|
String? tag,
|
||||||
|
bool useAnimation = true,
|
||||||
|
bool forceGlobal = false}) {
|
||||||
|
final overlayState =
|
||||||
|
forceGlobal ? globalKey.currentState?.overlay : _overlayState;
|
||||||
|
|
||||||
|
if (overlayState == null) {
|
||||||
|
return Future.error(
|
||||||
|
"[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first");
|
||||||
|
}
|
||||||
|
|
||||||
|
final _tag;
|
||||||
|
if (tag != null) {
|
||||||
|
_tag = tag;
|
||||||
|
} else {
|
||||||
|
_tag = _tagCount.toString();
|
||||||
|
_tagCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dialog = Dialog<T>();
|
||||||
|
_dialogs[_tag] = dialog;
|
||||||
|
|
||||||
|
final close = ([res]) {
|
||||||
|
_dialogs.remove(_tag);
|
||||||
|
dialog.complete(res);
|
||||||
|
BackButtonInterceptor.removeByName(_tag);
|
||||||
|
};
|
||||||
|
dialog.entry = OverlayEntry(builder: (_) {
|
||||||
|
bool innerClicked = false;
|
||||||
|
return Listener(
|
||||||
|
onPointerUp: (_) {
|
||||||
|
if (!innerClicked && clickMaskDismiss) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
innerClicked = false;
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black12,
|
||||||
|
child: StatefulBuilder(builder: (context, setState) {
|
||||||
|
return Listener(
|
||||||
|
onPointerUp: (_) => innerClicked = true,
|
||||||
|
child: builder(setState, close),
|
||||||
|
);
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
overlayState.insert(dialog.entry!);
|
||||||
|
BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) {
|
||||||
|
if (backDismiss) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, name: _tag);
|
||||||
|
return dialog.completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showLoading(String text,
|
||||||
|
{bool clickMaskDismiss = false,
|
||||||
|
bool showCancel = true,
|
||||||
|
VoidCallback? onCancel}) {
|
||||||
|
show((setState, close) => CustomAlertDialog(
|
||||||
|
content: Container(
|
||||||
constraints: BoxConstraints(maxWidth: 240),
|
constraints: BoxConstraints(maxWidth: 240),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -64,74 +351,64 @@ void showLoading(String text, {bool clickMaskDismiss = false}) {
|
|||||||
Center(child: CircularProgressIndicator()),
|
Center(child: CircularProgressIndicator()),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
Center(
|
Center(
|
||||||
child: Text(Translator.call(text),
|
child: Text(translate(text),
|
||||||
style: TextStyle(fontSize: 15))),
|
style: TextStyle(fontSize: 15))),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
Center(
|
Offstage(
|
||||||
child: TextButton(
|
offstage: !showCancel,
|
||||||
style: flatButtonStyle,
|
child: Center(
|
||||||
onPressed: () {
|
child: TextButton(
|
||||||
SmartDialog.dismiss();
|
style: flatButtonStyle,
|
||||||
backToHome();
|
onPressed: () {
|
||||||
},
|
dismissAll();
|
||||||
child: Text(Translator.call('Cancel'),
|
if (onCancel != null) {
|
||||||
style: TextStyle(color: MyTheme.accent))))
|
onCancel();
|
||||||
]));
|
}
|
||||||
});
|
},
|
||||||
|
child: Text(translate('Cancel'),
|
||||||
|
style: TextStyle(color: MyTheme.accent)))))
|
||||||
|
]))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backToHome() {
|
void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
final overlayState = globalKey.currentState?.overlay;
|
||||||
}
|
if (overlayState == null) return;
|
||||||
|
final entry = OverlayEntry(builder: (_) {
|
||||||
typedef DialogBuilder = CustomAlertDialog Function(
|
return IgnorePointer(
|
||||||
StateSetter setState, void Function([dynamic]) close);
|
child: Align(
|
||||||
|
alignment: Alignment(0.0, 0.8),
|
||||||
class DialogManager {
|
child: Container(
|
||||||
static int _tag = 0;
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
static dismissByTag(String tag, [result]) {
|
borderRadius: BorderRadius.all(
|
||||||
SmartDialog.dismiss(tag: tag, result: result);
|
Radius.circular(20),
|
||||||
}
|
),
|
||||||
|
),
|
||||||
static Future<T?> show<T>(DialogBuilder builder,
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||||
{bool clickMaskDismiss = false,
|
child: Text(
|
||||||
bool backDismiss = false,
|
text,
|
||||||
String? tag,
|
style: TextStyle(
|
||||||
bool useAnimation = true}) async {
|
decoration: TextDecoration.none,
|
||||||
final t;
|
fontWeight: FontWeight.w300,
|
||||||
if (tag != null) {
|
fontSize: 18,
|
||||||
t = tag;
|
color: Colors.white),
|
||||||
} else {
|
),
|
||||||
_tag += 1;
|
)));
|
||||||
t = _tag.toString();
|
});
|
||||||
}
|
overlayState.insert(entry);
|
||||||
SmartDialog.dismiss(status: SmartStatus.allToast);
|
Future.delayed(timeout, () {
|
||||||
SmartDialog.dismiss(status: SmartStatus.loading);
|
entry.remove();
|
||||||
final close = ([res]) {
|
});
|
||||||
SmartDialog.dismiss(tag: t, result: res);
|
|
||||||
};
|
|
||||||
final res = await SmartDialog.show<T>(
|
|
||||||
tag: t,
|
|
||||||
clickMaskDismiss: clickMaskDismiss,
|
|
||||||
backDismiss: backDismiss,
|
|
||||||
useAnimation: useAnimation,
|
|
||||||
builder: (_) => StatefulBuilder(
|
|
||||||
builder: (_, setState) => builder(setState, close)));
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomAlertDialog extends StatelessWidget {
|
class CustomAlertDialog extends StatelessWidget {
|
||||||
CustomAlertDialog(
|
CustomAlertDialog(
|
||||||
{required this.title,
|
{this.title, required this.content, this.actions, this.contentPadding});
|
||||||
required this.content,
|
|
||||||
required this.actions,
|
|
||||||
this.contentPadding});
|
|
||||||
|
|
||||||
final Widget title;
|
final Widget? title;
|
||||||
final Widget content;
|
final Widget content;
|
||||||
final List<Widget> actions;
|
final List<Widget>? actions;
|
||||||
final double? contentPadding;
|
final double? contentPadding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -147,7 +424,9 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void msgBox(String type, String title, String text, {bool? hasCancel}) {
|
void msgBox(
|
||||||
|
String type, String title, String text, OverlayDialogManager dialogManager,
|
||||||
|
{bool? hasCancel}) {
|
||||||
var wrap = (String text, void Function() onPressed) => ButtonTheme(
|
var wrap = (String text, void Function() onPressed) => ButtonTheme(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
@@ -158,29 +437,43 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) {
|
|||||||
child: TextButton(
|
child: TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Text(Translator.call(text),
|
child:
|
||||||
style: TextStyle(color: MyTheme.accent))));
|
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
|
||||||
|
|
||||||
SmartDialog.dismiss();
|
dialogManager.dismissAll();
|
||||||
final buttons = [
|
List<Widget> buttons = [];
|
||||||
wrap(Translator.call('OK'), () {
|
if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) {
|
||||||
SmartDialog.dismiss();
|
buttons.insert(
|
||||||
backToHome();
|
0,
|
||||||
})
|
wrap(translate('OK'), () {
|
||||||
];
|
dialogManager.dismissAll();
|
||||||
|
closeConnection();
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (hasCancel == null) {
|
if (hasCancel == null) {
|
||||||
hasCancel = type != 'error';
|
// hasCancel = type != 'error';
|
||||||
|
hasCancel = type.indexOf("error") < 0 &&
|
||||||
|
type.indexOf("nocancel") < 0 &&
|
||||||
|
type != "restarting";
|
||||||
}
|
}
|
||||||
if (hasCancel) {
|
if (hasCancel) {
|
||||||
buttons.insert(
|
buttons.insert(
|
||||||
0,
|
0,
|
||||||
wrap(Translator.call('Cancel'), () {
|
wrap(translate('Cancel'), () {
|
||||||
SmartDialog.dismiss();
|
dialogManager.dismissAll();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
DialogManager.show((setState, close) => CustomAlertDialog(
|
// TODO: test this button
|
||||||
|
if (type.indexOf("hasclose") >= 0) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
wrap(translate('Close'), () {
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
title: Text(translate(title), style: TextStyle(fontSize: 21)),
|
title: Text(translate(title), style: TextStyle(fontSize: 21)),
|
||||||
content: Text(Translator.call(text), style: TextStyle(fontSize: 15)),
|
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
||||||
actions: buttons));
|
actions: buttons));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,21 +568,28 @@ class PermissionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> check(String type) {
|
static Future<bool> check(String type) {
|
||||||
|
if (isDesktop) {
|
||||||
|
return Future.value(true);
|
||||||
|
}
|
||||||
if (!permissions.contains(type))
|
if (!permissions.contains(type))
|
||||||
return Future.error("Wrong permission!$type");
|
return Future.error("Wrong permission!$type");
|
||||||
return FFI.invokeMethod("check_permission", type);
|
return gFFI.invokeMethod("check_permission", type);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> request(String type) {
|
static Future<bool> request(String type) {
|
||||||
|
if (isDesktop) {
|
||||||
|
return Future.value(true);
|
||||||
|
}
|
||||||
if (!permissions.contains(type))
|
if (!permissions.contains(type))
|
||||||
return Future.error("Wrong permission!$type");
|
return Future.error("Wrong permission!$type");
|
||||||
|
|
||||||
FFI.invokeMethod("request_permission", type);
|
gFFI.invokeMethod("request_permission", type);
|
||||||
if (type == "ignore_battery_optimizations") {
|
if (type == "ignore_battery_optimizations") {
|
||||||
return Future.value(false);
|
return Future.value(false);
|
||||||
}
|
}
|
||||||
_current = type;
|
_current = type;
|
||||||
_completer = Completer<bool>();
|
_completer = Completer<bool>();
|
||||||
|
gFFI.invokeMethod("request_permission", type);
|
||||||
|
|
||||||
// timeout
|
// timeout
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
@@ -325,3 +625,118 @@ RadioListTile<T> getRadio<T>(
|
|||||||
dense: true,
|
dense: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckboxListTile getToggle(
|
||||||
|
String id, void Function(void Function()) setState, option, name,
|
||||||
|
{FFI? ffi}) {
|
||||||
|
final opt = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: opt,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
bind.sessionToggleOption(id: id, value: option);
|
||||||
|
});
|
||||||
|
if (option == "show-quality-monitor") {
|
||||||
|
(ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
title: Text(translate(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// find ffi, tag is Remote ID
|
||||||
|
/// for session specific usage
|
||||||
|
FFI ffi(String? tag) {
|
||||||
|
return Get.find<FFI>(tag: tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global FFI object
|
||||||
|
late FFI _globalFFI;
|
||||||
|
|
||||||
|
FFI get gFFI => _globalFFI;
|
||||||
|
|
||||||
|
Future<void> initGlobalFFI() async {
|
||||||
|
debugPrint("_globalFFI init");
|
||||||
|
_globalFFI = FFI();
|
||||||
|
debugPrint("_globalFFI init end");
|
||||||
|
// after `put`, can also be globally found by Get.find<FFI>();
|
||||||
|
Get.put(_globalFFI, permanent: true);
|
||||||
|
// trigger connection status updater
|
||||||
|
await bind.mainCheckConnectStatus();
|
||||||
|
// global shared preference
|
||||||
|
await Get.putAsync(() => SharedPreferences.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
String translate(String name) {
|
||||||
|
if (name.startsWith('Failed to') && name.contains(': ')) {
|
||||||
|
return name.split(': ').map((x) => translate(x)).join(': ');
|
||||||
|
}
|
||||||
|
return platformFFI.translate(name, localeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool option2bool(String option, String value) {
|
||||||
|
bool res;
|
||||||
|
if (option.startsWith("enable-")) {
|
||||||
|
res = value != "N";
|
||||||
|
} else if (option.startsWith("allow-") ||
|
||||||
|
option == "stop-service" ||
|
||||||
|
option == "direct-server" ||
|
||||||
|
option == "stop-rendezvous-service") {
|
||||||
|
res = value == "Y";
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
res = value != "N";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
String bool2option(String option, bool b) {
|
||||||
|
String res;
|
||||||
|
if (option.startsWith('enable-')) {
|
||||||
|
res = b ? '' : 'N';
|
||||||
|
} else if (option.startsWith('allow-') ||
|
||||||
|
option == "stop-service" ||
|
||||||
|
option == "direct-server" ||
|
||||||
|
option == "stop-rendezvous-service") {
|
||||||
|
res = b ? 'Y' : '';
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
res = b ? 'Y' : 'N';
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||||
|
if (searchText.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (peer.id.toLowerCase().contains(searchText)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (peer.hostname.toLowerCase().contains(searchText) ||
|
||||||
|
peer.username.toLowerCase().contains(searchText)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
||||||
|
if (alias.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return alias.toLowerCase().contains(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Peer>>? matchPeers(String searchText, List<Peer> peers) async {
|
||||||
|
searchText = searchText.trim();
|
||||||
|
if (searchText.isEmpty) {
|
||||||
|
return peers;
|
||||||
|
}
|
||||||
|
searchText = searchText.toLowerCase();
|
||||||
|
final matches =
|
||||||
|
await Future.wait(peers.map((peer) => matchPeer(searchText, peer)));
|
||||||
|
final filteredList = List<Peer>.empty(growable: true);
|
||||||
|
for (var i = 0; i < peers.length; i++) {
|
||||||
|
if (matches[i]) {
|
||||||
|
filteredList.add(peers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredList;
|
||||||
|
}
|
||||||
|
|||||||
4
flutter/lib/common/formatter/id_formatter.dart
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// TODO: Divide every 3 number to display ID
|
||||||
|
class IdFormController extends TextEditingController {}
|
||||||
11
flutter/lib/consts.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const double kDesktopRemoteTabBarHeight = 28.0;
|
||||||
|
|
||||||
|
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page'
|
||||||
|
const String kAppTypeMain = "main";
|
||||||
|
const String kAppTypeDesktopRemote = "remote";
|
||||||
|
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||||
|
const String kTabLabelHomePage = "Home";
|
||||||
|
const String kTabLabelSettingPage = "Settings";
|
||||||
|
|
||||||
|
const int kDefaultDisplayWidth = 1280;
|
||||||
|
const int kDefaultDisplayHeight = 720;
|
||||||
1167
flutter/lib/desktop/pages/connection_page.dart
Normal file
126
flutter/lib/desktop/pages/connection_tab_page.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../models/model.dart';
|
||||||
|
|
||||||
|
class ConnectionTabPage extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const ConnectionTabPage({Key? key, required this.params}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ConnectionTabPage> createState() => _ConnectionTabPageState(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||||
|
final tabController = Get.put(DesktopTabController());
|
||||||
|
static final Rx<String> _fullscreenID = "".obs;
|
||||||
|
static final IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
|
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
|
|
||||||
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||||
|
|
||||||
|
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||||
|
if (params['id'] != null) {
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: params['id'],
|
||||||
|
label: params['id'],
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: RemotePage(
|
||||||
|
key: ValueKey(params['id']),
|
||||||
|
id: params['id'],
|
||||||
|
tabBarHeight:
|
||||||
|
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
|
||||||
|
fullscreenID: _fullscreenID,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||||
|
|
||||||
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
|
print(
|
||||||
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
|
// for simplify, just replace connectionId
|
||||||
|
if (call.method == "new_remote_desktop") {
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
window_on_top(windowId());
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: id,
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: RemotePage(
|
||||||
|
key: ValueKey(id),
|
||||||
|
id: id,
|
||||||
|
tabBarHeight: _fullscreenID.value.isNotEmpty
|
||||||
|
? 0
|
||||||
|
: kDesktopRemoteTabBarHeight,
|
||||||
|
fullscreenID: _fullscreenID,
|
||||||
|
)));
|
||||||
|
} else if (call.method == "onDestroy") {
|
||||||
|
tabController.state.value.tabs.forEach((tab) {
|
||||||
|
print("executing onDestroy hook, closing ${tab.label}}");
|
||||||
|
final tag = tab.label;
|
||||||
|
ffi(tag).close().then((_) {
|
||||||
|
Get.delete<FFI>(tag: tag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||||
|
return SubWindowDragToResizeArea(
|
||||||
|
windowId: windowId(),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: Obx(() => DesktopTab(
|
||||||
|
controller: tabController,
|
||||||
|
theme: theme,
|
||||||
|
isMainWindow: false,
|
||||||
|
showTabBar: _fullscreenID.value.isEmpty,
|
||||||
|
tail: AddButton(
|
||||||
|
theme: theme,
|
||||||
|
).paddingOnly(left: 10),
|
||||||
|
pageViewBuilder: (pageView) {
|
||||||
|
WindowController.fromWindowId(windowId())
|
||||||
|
.setFullscreen(_fullscreenID.value.isNotEmpty);
|
||||||
|
return pageView;
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRemoveId(String id) {
|
||||||
|
ffi(id).close();
|
||||||
|
if (tabController.state.value.tabs.length == 0) {
|
||||||
|
WindowController.fromWindowId(windowId()).close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int windowId() {
|
||||||
|
return widget.params["windowId"];
|
||||||
|
}
|
||||||
|
}
|
||||||
1034
flutter/lib/desktop/pages/desktop_home_page.dart
Normal file
1455
flutter/lib/desktop/pages/desktop_setting_page.dart
Normal file
66
flutter/lib/desktop/pages/desktop_tab_page.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
class DesktopTabPage extends StatefulWidget {
|
||||||
|
const DesktopTabPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||||
|
final tabController = DesktopTabController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: kTabLabelHomePage,
|
||||||
|
label: kTabLabelHomePage,
|
||||||
|
selectedIcon: Icons.home_sharp,
|
||||||
|
unselectedIcon: Icons.home_outlined,
|
||||||
|
closable: false,
|
||||||
|
page: DesktopHomePage(
|
||||||
|
key: const ValueKey(kTabLabelHomePage),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final dark = isDarkTheme();
|
||||||
|
return DragToResizeArea(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: DesktopTab(
|
||||||
|
controller: tabController,
|
||||||
|
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||||
|
isMainWindow: true,
|
||||||
|
tail: ActionIcon(
|
||||||
|
message: 'Settings',
|
||||||
|
icon: IconFont.menu,
|
||||||
|
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||||
|
onTap: onAddSetting,
|
||||||
|
is_close: false,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddSetting() {
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: kTabLabelSettingPage,
|
||||||
|
label: kTabLabelSettingPage,
|
||||||
|
selectedIcon: Icons.build_sharp,
|
||||||
|
unselectedIcon: Icons.build_outlined,
|
||||||
|
page: DesktopSettingPage(key: const ValueKey(kTabLabelSettingPage))));
|
||||||
|
}
|
||||||
|
}
|
||||||
867
flutter/lib/desktop/pages/file_manager_page.dart
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
|
||||||
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
|
||||||
|
enum LocationStatus { bread, textField }
|
||||||
|
|
||||||
|
class FileManagerPage extends StatefulWidget {
|
||||||
|
FileManagerPage({Key? key, required this.id}) : super(key: key);
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileManagerPageState extends State<FileManagerPage>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
final _localSelectedItems = SelectedItems();
|
||||||
|
final _remoteSelectedItems = SelectedItems();
|
||||||
|
|
||||||
|
final _locationStatusLocal = LocationStatus.bread.obs;
|
||||||
|
final _locationStatusRemote = LocationStatus.bread.obs;
|
||||||
|
final FocusNode _locationNodeLocal =
|
||||||
|
FocusNode(debugLabel: "locationNodeLocal");
|
||||||
|
final FocusNode _locationNodeRemote =
|
||||||
|
FocusNode(debugLabel: "locationNodeRemote");
|
||||||
|
final _searchTextLocal = "".obs;
|
||||||
|
final _searchTextRemote = "".obs;
|
||||||
|
final _breadCrumbScrollerLocal = ScrollController();
|
||||||
|
final _breadCrumbScrollerRemote = ScrollController();
|
||||||
|
|
||||||
|
final _dropMaskVisible = false.obs;
|
||||||
|
|
||||||
|
ScrollController getBreadCrumbScrollController(bool isLocal) {
|
||||||
|
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
|
||||||
|
}
|
||||||
|
|
||||||
|
late FFI _ffi;
|
||||||
|
|
||||||
|
FileModel get model => _ffi.fileModel;
|
||||||
|
|
||||||
|
SelectedItems getSelectedItem(bool isLocal) {
|
||||||
|
return isLocal ? _localSelectedItems : _remoteSelectedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ffi = FFI();
|
||||||
|
_ffi.connect(widget.id, isFileTransfer: true);
|
||||||
|
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||||
|
// _ffi.ffiModel.updateEventListener(widget.id);
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
Wakelock.enable();
|
||||||
|
}
|
||||||
|
print("init success with id ${widget.id}");
|
||||||
|
// register location listener
|
||||||
|
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
|
||||||
|
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
model.onClose();
|
||||||
|
_ffi.close();
|
||||||
|
_ffi.dialogManager.dismissAll();
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
Wakelock.disable();
|
||||||
|
}
|
||||||
|
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||||
|
_locationNodeLocal.removeListener(onLocalLocationFocusChanged);
|
||||||
|
_locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Overlay(initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) {
|
||||||
|
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: _ffi.fileModel,
|
||||||
|
child: Consumer<FileModel>(builder: (_context, _model, _child) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
if (model.selectMode) {
|
||||||
|
model.toggleSelectMode();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(flex: 3, child: body(isLocal: true)),
|
||||||
|
Flexible(flex: 3, child: body(isLocal: false)),
|
||||||
|
Flexible(flex: 2, child: statusList())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget menu({bool isLocal = false}) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
icon: Icon(Icons.more_vert),
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
model.getCurrentShowHidden(isLocal)
|
||||||
|
? Icons.check_box_outlined
|
||||||
|
: Icons.check_box_outline_blank,
|
||||||
|
color: Colors.black),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(translate("Show Hidden Files"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: "hidden",
|
||||||
|
)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
onSelected: (v) {
|
||||||
|
if (v == "hidden") {
|
||||||
|
model.toggleShowHidden(local: isLocal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget body({bool isLocal = false}) {
|
||||||
|
final fd = model.getCurrentDir(isLocal);
|
||||||
|
final entries = fd.entries;
|
||||||
|
final sortIndex = (SortBy style) {
|
||||||
|
switch (style) {
|
||||||
|
case SortBy.Name:
|
||||||
|
return 1;
|
||||||
|
case SortBy.Type:
|
||||||
|
return 0;
|
||||||
|
case SortBy.Modified:
|
||||||
|
return 2;
|
||||||
|
case SortBy.Size:
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}(model.getSortStyle(isLocal));
|
||||||
|
final sortAscending =
|
||||||
|
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
|
||||||
|
margin: const EdgeInsets.all(16.0),
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: DropTarget(
|
||||||
|
onDragDone: (detail) => handleDragDone(detail, isLocal),
|
||||||
|
onDragEntered: (enter) {
|
||||||
|
_dropMaskVisible.value = true;
|
||||||
|
},
|
||||||
|
onDragExited: (exit) {
|
||||||
|
_dropMaskVisible.value = false;
|
||||||
|
},
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
headTools(isLocal),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: ObxValue<RxString>(
|
||||||
|
(searchText) {
|
||||||
|
final filteredEntries = searchText.isEmpty
|
||||||
|
? entries.where((element) {
|
||||||
|
if (searchText.isEmpty) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return element.name.contains(searchText.value);
|
||||||
|
}
|
||||||
|
}).toList(growable: false)
|
||||||
|
: entries;
|
||||||
|
return DataTable(
|
||||||
|
key: ValueKey(isLocal ? 0 : 1),
|
||||||
|
showCheckboxColumn: true,
|
||||||
|
dataRowHeight: 25,
|
||||||
|
headingRowHeight: 30,
|
||||||
|
columnSpacing: 8,
|
||||||
|
showBottomBorder: true,
|
||||||
|
sortColumnIndex: sortIndex,
|
||||||
|
sortAscending: sortAscending,
|
||||||
|
columns: [
|
||||||
|
DataColumn(label: Text(translate(" "))), // icon
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
translate("Name"),
|
||||||
|
),
|
||||||
|
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())
|
||||||
|
: "";
|
||||||
|
return DataRow(
|
||||||
|
key: ValueKey(entry.name),
|
||||||
|
onSelectChanged: (s) {
|
||||||
|
if (s != null) {
|
||||||
|
if (s) {
|
||||||
|
getSelectedItem(isLocal)
|
||||||
|
.add(isLocal, entry);
|
||||||
|
} else {
|
||||||
|
getSelectedItem(isLocal).remove(entry);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected:
|
||||||
|
getSelectedItem(isLocal).contains(entry),
|
||||||
|
cells: [
|
||||||
|
DataCell(Icon(
|
||||||
|
entry.isFile
|
||||||
|
? Icons.feed_outlined
|
||||||
|
: Icons.folder,
|
||||||
|
size: 25)),
|
||||||
|
DataCell(
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints:
|
||||||
|
BoxConstraints(maxWidth: 100),
|
||||||
|
child: Tooltip(
|
||||||
|
message: entry.name,
|
||||||
|
child: Text(entry.name,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
)), onTap: () {
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
openDirectory(entry.path, isLocal: isLocal);
|
||||||
|
if (isLocal) {
|
||||||
|
_localSelectedItems.clear();
|
||||||
|
} else {
|
||||||
|
_remoteSelectedItems.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Perform file-related tasks.
|
||||||
|
final _selectedItems =
|
||||||
|
getSelectedItem(isLocal);
|
||||||
|
if (_selectedItems.contains(entry)) {
|
||||||
|
_selectedItems.remove(entry);
|
||||||
|
} else {
|
||||||
|
_selectedItems.add(isLocal, entry);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DataCell(Text(
|
||||||
|
entry
|
||||||
|
.lastModified()
|
||||||
|
.toString()
|
||||||
|
.replaceAll(".000", "") +
|
||||||
|
" ",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: MyTheme.darkGray),
|
||||||
|
)),
|
||||||
|
DataCell(Text(
|
||||||
|
sizeStr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: MyTheme.darkGray),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}).toList(growable: false),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isLocal ? _searchTextLocal : _searchTextRemote,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
// Center(child: listTail(isLocal: isLocal)),
|
||||||
|
// Expanded(
|
||||||
|
// child: ListView.builder(
|
||||||
|
// itemCount: entries.length + 1,
|
||||||
|
// itemBuilder: (context, index) {
|
||||||
|
// if (index >= entries.length) {
|
||||||
|
// return listTail(isLocal: isLocal);
|
||||||
|
// }
|
||||||
|
// var selected = false;
|
||||||
|
// if (model.selectMode) {
|
||||||
|
// selected = _selectedItems.contains(entries[index]);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final sizeStr = entries[index].isFile
|
||||||
|
// ? readableFileSize(entries[index].size.toDouble())
|
||||||
|
// : "";
|
||||||
|
// return Card(
|
||||||
|
// child: ListTile(
|
||||||
|
// leading: Icon(
|
||||||
|
// entries[index].isFile ? Icons.feed_outlined : Icons.folder,
|
||||||
|
// size: 40),
|
||||||
|
// title: Text(entries[index].name),
|
||||||
|
// selected: selected,
|
||||||
|
// subtitle: Text(
|
||||||
|
// entries[index]
|
||||||
|
// .lastModified()
|
||||||
|
// .toString()
|
||||||
|
// .replaceAll(".000", "") +
|
||||||
|
// " " +
|
||||||
|
// sizeStr,
|
||||||
|
// style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
|
||||||
|
// ),
|
||||||
|
// trailing: needShowCheckBox()
|
||||||
|
// ? Checkbox(
|
||||||
|
// value: selected,
|
||||||
|
// onChanged: (v) {
|
||||||
|
// if (v == null) return;
|
||||||
|
// if (v && !selected) {
|
||||||
|
// _selectedItems.add(isLocal, entries[index]);
|
||||||
|
// } else if (!v && selected) {
|
||||||
|
// _selectedItems.remove(entries[index]);
|
||||||
|
// }
|
||||||
|
// setState(() {});
|
||||||
|
// })
|
||||||
|
// : PopupMenuButton<String>(
|
||||||
|
// icon: Icon(Icons.more_vert),
|
||||||
|
// itemBuilder: (context) {
|
||||||
|
// return [
|
||||||
|
// PopupMenuItem(
|
||||||
|
// child: Text(translate("Delete")),
|
||||||
|
// value: "delete",
|
||||||
|
// ),
|
||||||
|
// PopupMenuItem(
|
||||||
|
// child: Text(translate("Multi Select")),
|
||||||
|
// value: "multi_select",
|
||||||
|
// ),
|
||||||
|
// PopupMenuItem(
|
||||||
|
// child: Text(translate("Properties")),
|
||||||
|
// value: "properties",
|
||||||
|
// enabled: false,
|
||||||
|
// )
|
||||||
|
// ];
|
||||||
|
// },
|
||||||
|
// onSelected: (v) {
|
||||||
|
// if (v == "delete") {
|
||||||
|
// final items = SelectedItems();
|
||||||
|
// items.add(isLocal, entries[index]);
|
||||||
|
// model.removeAction(items);
|
||||||
|
// } else if (v == "multi_select") {
|
||||||
|
// _selectedItems.clear();
|
||||||
|
// model.toggleSelectMode();
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// onTap: () {
|
||||||
|
// if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
|
||||||
|
// if (selected) {
|
||||||
|
// _selectedItems.remove(entries[index]);
|
||||||
|
// } else {
|
||||||
|
// _selectedItems.add(isLocal, entries[index]);
|
||||||
|
// }
|
||||||
|
// setState(() {});
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (entries[index].isDirectory) {
|
||||||
|
// openDirectory(entries[index].path, isLocal: isLocal);
|
||||||
|
// breadCrumbScrollToEnd(isLocal);
|
||||||
|
// } else {
|
||||||
|
// // Perform file-related tasks.
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// onLongPress: () {
|
||||||
|
// _selectedItems.clear();
|
||||||
|
// model.toggleSelectMode();
|
||||||
|
// if (model.selectMode) {
|
||||||
|
// _selectedItems.add(isLocal, entries[index]);
|
||||||
|
// }
|
||||||
|
// setState(() {});
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ))
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// transfer status list
|
||||||
|
/// watch transfer status
|
||||||
|
Widget statusList() {
|
||||||
|
return PreferredSize(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
|
||||||
|
child: Obx(
|
||||||
|
() => ListView.builder(
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final item = model.jobTable[index];
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Transform.rotate(
|
||||||
|
angle: item.isRemote ? pi : 0,
|
||||||
|
child: Icon(Icons.send)),
|
||||||
|
SizedBox(
|
||||||
|
width: 16.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Tooltip(
|
||||||
|
message: item.jobName,
|
||||||
|
child: Text(
|
||||||
|
'${item.jobName}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)),
|
||||||
|
Wrap(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '),
|
||||||
|
Text(
|
||||||
|
'${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '),
|
||||||
|
Offstage(
|
||||||
|
offstage:
|
||||||
|
item.state != JobState.inProgress,
|
||||||
|
child: Text(
|
||||||
|
'${readableFileSize(item.speed) + "/s"} ')),
|
||||||
|
Offstage(
|
||||||
|
offstage: item.totalSize <= 0,
|
||||||
|
child: Text(
|
||||||
|
'${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: item.state != JobState.paused,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.resumeJob(item.id);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.restart_alt_rounded)),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete),
|
||||||
|
onPressed: () {
|
||||||
|
model.jobTable.removeAt(index);
|
||||||
|
model.cancelJob(item.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 2.0,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: model.jobTable.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
preferredSize: Size(200, double.infinity));
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack({bool? isLocal}) {
|
||||||
|
model.goToParentDirectory(isLocal: isLocal);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget headTools(bool isLocal) {
|
||||||
|
final _locationStatus =
|
||||||
|
isLocal ? _locationStatusLocal : _locationStatusRemote;
|
||||||
|
final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
|
||||||
|
final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
|
||||||
|
return Container(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// symbols
|
||||||
|
PreferredSize(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(color: Colors.blue),
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: FutureBuilder<String>(
|
||||||
|
future: bind.sessionGetPlatform(
|
||||||
|
id: _ffi.id, isRemote: !isLocal),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
||||||
|
return getPlatformImage('${snapshot.data}');
|
||||||
|
} else {
|
||||||
|
return CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
Text(isLocal
|
||||||
|
? translate("Local Computer")
|
||||||
|
: translate("Remote Computer"))
|
||||||
|
.marginOnly(left: 8.0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
preferredSize: Size(double.infinity, 70)),
|
||||||
|
// buttons
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.goHome(isLocal: isLocal);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.home_outlined)),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.arrow_upward),
|
||||||
|
onPressed: () {
|
||||||
|
goBack(isLocal: isLocal);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
menu(isLocal: isLocal),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_locationStatus.value =
|
||||||
|
_locationStatus.value == LocationStatus.bread
|
||||||
|
? LocationStatus.textField
|
||||||
|
: LocationStatus.bread;
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
if (_locationStatus.value == LocationStatus.textField) {
|
||||||
|
_locationFocus.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(border: Border.all(color: Colors.black12)),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() =>
|
||||||
|
_locationStatus.value == LocationStatus.bread
|
||||||
|
? buildBread(isLocal)
|
||||||
|
: buildPathLocation(isLocal))),
|
||||||
|
DropdownButton<String>(
|
||||||
|
isDense: true,
|
||||||
|
underline: Offstage(),
|
||||||
|
items: [
|
||||||
|
// TODO: favourite
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text('/'),
|
||||||
|
value: '/',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
onChanged: (path) {
|
||||||
|
if (path is String && path.isNotEmpty) {
|
||||||
|
openDirectory(path, isLocal: isLocal);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: false,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 200),
|
||||||
|
child: TextField(
|
||||||
|
controller:
|
||||||
|
TextEditingController(text: _searchTextObs.value),
|
||||||
|
autofocus: true,
|
||||||
|
decoration:
|
||||||
|
InputDecoration(prefixIcon: Icon(Icons.search)),
|
||||||
|
onChanged: (searchText) =>
|
||||||
|
onSearchText(searchText, isLocal),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
child: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.refresh(isLocal: isLocal);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.refresh)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final name = TextEditingController();
|
||||||
|
_ffi.dialogManager
|
||||||
|
.show((setState, close) => CustomAlertDialog(
|
||||||
|
title: Text(translate("Create Folder")),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: translate(
|
||||||
|
"Please enter the folder name"),
|
||||||
|
),
|
||||||
|
controller: name,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
ElevatedButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () {
|
||||||
|
if (name.value.text.isNotEmpty) {
|
||||||
|
model.createDir(
|
||||||
|
PathUtil.join(
|
||||||
|
model
|
||||||
|
.getCurrentDir(
|
||||||
|
isLocal)
|
||||||
|
.path,
|
||||||
|
name.value.text,
|
||||||
|
model.getCurrentIsWindows(
|
||||||
|
isLocal)),
|
||||||
|
isLocal: isLocal);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(translate("OK")))
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.create_new_folder_outlined)),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final items = isLocal
|
||||||
|
? _localSelectedItems
|
||||||
|
: _remoteSelectedItems;
|
||||||
|
await (model.removeAction(items, isLocal: isLocal));
|
||||||
|
items.clear();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.delete_forever_outlined)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
final items = getSelectedItem(isLocal);
|
||||||
|
model.sendFiles(items, isRemote: !isLocal);
|
||||||
|
items.clear();
|
||||||
|
},
|
||||||
|
icon: Transform.rotate(
|
||||||
|
angle: isLocal ? 0 : pi,
|
||||||
|
child: Icon(
|
||||||
|
Icons.send,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
isLocal ? translate('Send') : translate('Receive'),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
).marginOnly(top: 8.0)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget listTail({bool isLocal = false}) {
|
||||||
|
final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir;
|
||||||
|
return Container(
|
||||||
|
height: 100,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
|
||||||
|
child: Text(
|
||||||
|
dir.path,
|
||||||
|
style: TextStyle(color: MyTheme.darkGray),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(2),
|
||||||
|
child: Text(
|
||||||
|
"${translate("Total")}: ${dir.entries.length} ${translate("items")}",
|
||||||
|
style: TextStyle(color: MyTheme.darkGray),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
/// Get the image for the current [platform].
|
||||||
|
Widget getPlatformImage(String platform) {
|
||||||
|
platform = platform.toLowerCase();
|
||||||
|
if (platform == 'mac os')
|
||||||
|
platform = 'mac';
|
||||||
|
else if (platform != 'linux' && platform != 'android') platform = 'win';
|
||||||
|
return Image.asset('assets/$platform.png', width: 25, height: 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLocalLocationFocusChanged() {
|
||||||
|
debugPrint("focus changed on local");
|
||||||
|
if (_locationNodeLocal.hasFocus) {
|
||||||
|
// ignore
|
||||||
|
} else {
|
||||||
|
// lost focus, change to bread
|
||||||
|
_locationStatusLocal.value = LocationStatus.bread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRemoteLocationFocusChanged() {
|
||||||
|
debugPrint("focus changed on remote");
|
||||||
|
if (_locationNodeRemote.hasFocus) {
|
||||||
|
// ignore
|
||||||
|
} else {
|
||||||
|
// lost focus, change to bread
|
||||||
|
_locationStatusRemote.value = LocationStatus.bread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBread(bool isLocal) {
|
||||||
|
final items = getPathBreadCrumbItems(isLocal, (list) {
|
||||||
|
var path = "";
|
||||||
|
for (var item in list) {
|
||||||
|
path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal));
|
||||||
|
}
|
||||||
|
openDirectory(path, isLocal: isLocal);
|
||||||
|
});
|
||||||
|
return items.isEmpty
|
||||||
|
? Offstage()
|
||||||
|
: BreadCrumb(
|
||||||
|
items: items,
|
||||||
|
divider: Text("/").paddingSymmetric(horizontal: 4.0),
|
||||||
|
overflow: ScrollableOverflow(
|
||||||
|
controller: getBreadCrumbScrollController(isLocal)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BreadCrumbItem> getPathBreadCrumbItems(
|
||||||
|
bool isLocal, void Function(List<String>) onPressed) {
|
||||||
|
final path = model.getCurrentDir(isLocal).path;
|
||||||
|
final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal));
|
||||||
|
final breadCrumbList = List<BreadCrumbItem>.empty(growable: true);
|
||||||
|
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
|
||||||
|
content: TextButton(
|
||||||
|
child: Text(e.value),
|
||||||
|
style:
|
||||||
|
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
|
||||||
|
onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
|
||||||
|
return breadCrumbList;
|
||||||
|
}
|
||||||
|
|
||||||
|
breadCrumbScrollToEnd(bool isLocal) {
|
||||||
|
Future.delayed(Duration(milliseconds: 200), () {
|
||||||
|
final _breadCrumbScroller = getBreadCrumbScrollController(isLocal);
|
||||||
|
_breadCrumbScroller.animateTo(
|
||||||
|
_breadCrumbScroller.position.maxScrollExtent,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
curve: Curves.fastLinearToSlowEaseIn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPathLocation(bool isLocal) {
|
||||||
|
return TextField(
|
||||||
|
focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
prefix: Padding(padding: EdgeInsets.only(left: 4.0)),
|
||||||
|
),
|
||||||
|
controller:
|
||||||
|
TextEditingController(text: model.getCurrentDir(isLocal).path),
|
||||||
|
onSubmitted: (path) {
|
||||||
|
openDirectory(path, isLocal: isLocal);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchText(String searchText, bool isLocal) {
|
||||||
|
if (isLocal) {
|
||||||
|
_searchTextLocal.value = searchText;
|
||||||
|
} else {
|
||||||
|
_searchTextRemote.value = searchText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openDirectory(String path, {bool isLocal = false}) {
|
||||||
|
model.openDirectory(path, isLocal: isLocal).then((_) {
|
||||||
|
print("scroll");
|
||||||
|
breadCrumbScrollToEnd(isLocal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDragDone(DropDoneDetails details, bool isLocal) {
|
||||||
|
if (isLocal) {
|
||||||
|
// ignore local
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var items = SelectedItems();
|
||||||
|
details.files.forEach((file) {
|
||||||
|
final f = File(file.path);
|
||||||
|
items.add(
|
||||||
|
true,
|
||||||
|
Entry()
|
||||||
|
..path = file.path
|
||||||
|
..name = file.name
|
||||||
|
..size =
|
||||||
|
FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
|
||||||
|
});
|
||||||
|
model.sendFiles(items, isRemote: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
flutter/lib/desktop/pages/file_manager_tab_page.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
/// File Transfer for multi tabs
|
||||||
|
class FileManagerTabPage extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const FileManagerTabPage({Key? key, required this.params}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FileManagerTabPage> createState() => _FileManagerTabPageState(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||||
|
final tabController = Get.put(DesktopTabController());
|
||||||
|
|
||||||
|
static final IconData selectedIcon = Icons.file_copy_sharp;
|
||||||
|
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||||
|
|
||||||
|
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: params['id'],
|
||||||
|
label: params['id'],
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: FileManagerPage(key: ValueKey(params['id']), id: params['id'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||||
|
|
||||||
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
|
print(
|
||||||
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
|
// for simplify, just replace connectionId
|
||||||
|
if (call.method == "new_file_transfer") {
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
window_on_top(windowId());
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: id,
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: FileManagerPage(key: ValueKey(id), id: id)));
|
||||||
|
} else if (call.method == "onDestroy") {
|
||||||
|
tabController.state.value.tabs.forEach((tab) {
|
||||||
|
print("executing onDestroy hook, closing ${tab.label}}");
|
||||||
|
final tag = tab.label;
|
||||||
|
ffi(tag).close().then((_) {
|
||||||
|
Get.delete<FFI>(tag: tag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||||
|
return SubWindowDragToResizeArea(
|
||||||
|
windowId: windowId(),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: DesktopTab(
|
||||||
|
controller: tabController,
|
||||||
|
theme: theme,
|
||||||
|
isMainWindow: false,
|
||||||
|
tail: AddButton(
|
||||||
|
theme: theme,
|
||||||
|
).paddingOnly(left: 10),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRemoveId(String id) {
|
||||||
|
ffi("ft_$id").close();
|
||||||
|
if (tabController.state.value.tabs.length == 0) {
|
||||||
|
WindowController.fromWindowId(windowId()).close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int windowId() {
|
||||||
|
return widget.params["windowId"];
|
||||||
|
}
|
||||||
|
}
|
||||||
1254
flutter/lib/desktop/pages/remote_page.dart
Normal file
574
flutter/lib/desktop/pages/server_page.dart
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/pages/chat_page.dart';
|
||||||
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/server_model.dart';
|
||||||
|
|
||||||
|
class DesktopServerPage extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _DesktopServerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||||
|
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
gFFI.ffiModel.updateEventListener("");
|
||||||
|
windowManager.addListener(this);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowClose() {
|
||||||
|
gFFI.serverModel.closeAll();
|
||||||
|
gFFI.close();
|
||||||
|
super.onWindowClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.serverModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
||||||
|
],
|
||||||
|
child: Consumer<ServerModel>(
|
||||||
|
builder: (context, serverModel, child) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border:
|
||||||
|
Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: ConnectionManager()),
|
||||||
|
SizedBox.fromSize(size: Size(0, 15.0)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionManager extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => ConnectionManagerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionManagerState extends State<ConnectionManager> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
gFFI.serverModel.updateClientState();
|
||||||
|
gFFI.serverModel.tabController.onSelected = (index) =>
|
||||||
|
gFFI.chatModel.changeCurrentID(gFFI.serverModel.clients[index].id);
|
||||||
|
// test
|
||||||
|
// gFFI.serverModel.clients.forEach((client) {
|
||||||
|
// DesktopTabBar.onAdd(
|
||||||
|
// gFFI.serverModel.tabs,
|
||||||
|
// TabInfo(
|
||||||
|
// key: client.id.toString(), label: client.name, closable: false));
|
||||||
|
// });
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
|
return serverModel.clients.isEmpty
|
||||||
|
? Column(
|
||||||
|
children: [
|
||||||
|
buildTitleBar(Offstage()),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(translate("Waiting")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: DesktopTab(
|
||||||
|
theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||||
|
showTitle: false,
|
||||||
|
showMaximize: false,
|
||||||
|
showMinimize: false,
|
||||||
|
controller: serverModel.tabController,
|
||||||
|
isMainWindow: true,
|
||||||
|
pageViewBuilder: (pageView) => Row(children: [
|
||||||
|
Expanded(child: pageView),
|
||||||
|
Consumer<ChatModel>(
|
||||||
|
builder: (_, model, child) => model.isShowChatPage
|
||||||
|
? Expanded(child: Scaffold(body: ChatPage()))
|
||||||
|
: Offstage())
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTitleBar(Widget middle) {
|
||||||
|
return GestureDetector(
|
||||||
|
onPanDown: (d) {
|
||||||
|
windowManager.startDragging();
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_AppIcon(),
|
||||||
|
Expanded(child: middle),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4.0,
|
||||||
|
),
|
||||||
|
_CloseButton()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTab(Client client) {
|
||||||
|
return Tab(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: Text(
|
||||||
|
"${client.name}",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildConnectionCard(Client client) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
key: ValueKey(client.id),
|
||||||
|
children: [
|
||||||
|
_CmHeader(client: client),
|
||||||
|
client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _CmControlPanel(client: client),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
).paddingSymmetric(vertical: 8.0, horizontal: 8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppIcon extends StatelessWidget {
|
||||||
|
const _AppIcon({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/logo.ico',
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CloseButton extends StatelessWidget {
|
||||||
|
const _CloseButton({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Ink(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
windowManager.close();
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 30,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CmHeader extends StatefulWidget {
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
const _CmHeader({Key? key, required this.client}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CmHeader> createState() => _CmHeaderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CmHeaderState extends State<_CmHeader>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
Client get client => widget.client;
|
||||||
|
|
||||||
|
var _time = 0.obs;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||||
|
_time.value = _time.value + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// icon
|
||||||
|
Container(
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(color: str2color(client.name)),
|
||||||
|
child: Text(
|
||||||
|
"${client.name[0]}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: Colors.white, fontSize: 65),
|
||||||
|
),
|
||||||
|
).marginOnly(left: 4.0, right: 8.0),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FittedBox(
|
||||||
|
child: Text(
|
||||||
|
"${client.name}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: MyTheme.cmIdColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
)),
|
||||||
|
FittedBox(
|
||||||
|
child: Text("(${client.peerId})",
|
||||||
|
style:
|
||||||
|
TextStyle(color: MyTheme.cmIdColor, fontSize: 14))),
|
||||||
|
SizedBox(
|
||||||
|
height: 16.0,
|
||||||
|
),
|
||||||
|
FittedBox(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text("${translate("Connected")}").marginOnly(right: 8.0),
|
||||||
|
Obx(() => Text(
|
||||||
|
"${formatDurationToTime(Duration(seconds: _time.value))}"))
|
||||||
|
],
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: client.isFileTransfer,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id),
|
||||||
|
icon: Icon(Icons.message_outlined),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrivilegeBoard extends StatefulWidget {
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
const _PrivilegeBoard({Key? key, required this.client}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _PrivilegeBoardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||||
|
late final client = widget.client;
|
||||||
|
Widget buildPermissionIcon(bool enabled, ImageProvider icon,
|
||||||
|
Function(bool)? onTap, String? tooltip) {
|
||||||
|
return Tooltip(
|
||||||
|
message: tooltip ?? "",
|
||||||
|
child: Ink(
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey),
|
||||||
|
padding: EdgeInsets.all(4.0),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => onTap?.call(!enabled),
|
||||||
|
child: Image(
|
||||||
|
image: icon,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).marginSymmetric(horizontal: 4.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(top: 16.0, bottom: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translate("Permissions"),
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
).marginOnly(left: 4.0),
|
||||||
|
SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
FittedBox(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
buildPermissionIcon(client.keyboard, iconKeyboard, (enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id, name: "keyboard", enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.keyboard = enabled;
|
||||||
|
});
|
||||||
|
}, null),
|
||||||
|
buildPermissionIcon(client.clipboard, iconClipboard, (enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id, name: "clipboard", enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.clipboard = enabled;
|
||||||
|
});
|
||||||
|
}, null),
|
||||||
|
buildPermissionIcon(client.audio, iconAudio, (enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id, name: "audio", enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.audio = enabled;
|
||||||
|
});
|
||||||
|
}, null),
|
||||||
|
buildPermissionIcon(client.file, iconFile, (enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id, name: "file", enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.file = enabled;
|
||||||
|
});
|
||||||
|
}, null),
|
||||||
|
buildPermissionIcon(client.restart, iconRestart, (enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id, name: "restart", enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.restart = enabled;
|
||||||
|
});
|
||||||
|
}, null),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CmControlPanel extends StatelessWidget {
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
const _CmControlPanel({Key? key, required this.client}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ServerModel>(builder: (_, model, child) {
|
||||||
|
return client.authorized
|
||||||
|
? buildAuthorized(context)
|
||||||
|
: buildUnAuthorized(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAuthorized(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Ink(
|
||||||
|
width: 200,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.redAccent, borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => handleDisconnect(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translate("Disconnect"),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildUnAuthorized(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Ink(
|
||||||
|
width: 100,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => handleAccept(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translate("Accept"),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 30,
|
||||||
|
),
|
||||||
|
Ink(
|
||||||
|
width: 100,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: Colors.grey)),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => handleDisconnect(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translate("Cancel"),
|
||||||
|
style: TextStyle(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDisconnect(BuildContext context) {
|
||||||
|
bind.cmCloseConnection(connId: client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAccept(BuildContext context) {
|
||||||
|
final model = Provider.of<ServerModel>(context, listen: false);
|
||||||
|
model.sendLoginResponse(client, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaddingCard extends StatelessWidget {
|
||||||
|
PaddingCard({required this.child, this.title, this.titleIcon});
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
final IconData? titleIcon;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = [child];
|
||||||
|
if (title != null) {
|
||||||
|
children.insert(
|
||||||
|
0,
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
titleIcon != null
|
||||||
|
? Padding(
|
||||||
|
padding: EdgeInsets.only(right: 10),
|
||||||
|
child: Icon(titleIcon,
|
||||||
|
color: MyTheme.accent80, size: 30))
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
Text(
|
||||||
|
title!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'WorkSans',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
color: MyTheme.accent80,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget clientInfo(Client client) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: -1,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 12),
|
||||||
|
child: CircleAvatar(
|
||||||
|
child: Text(client.name[0]),
|
||||||
|
backgroundColor: MyTheme.border))),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(client.name,
|
||||||
|
style: TextStyle(color: MyTheme.idColor, fontSize: 18)),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(client.peerId,
|
||||||
|
style: TextStyle(color: MyTheme.idColor, fontSize: 10))
|
||||||
|
]))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
29
flutter/lib/desktop/screen/desktop_file_transfer_screen.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// multi-tab file transfer remote screen
|
||||||
|
class DesktopFileTransferScreen extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const DesktopFileTransferScreen({Key? key, required this.params})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.imageModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.cursorModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||||
|
],
|
||||||
|
child: Scaffold(
|
||||||
|
body: FileManagerTabPage(
|
||||||
|
params: params,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
flutter/lib/desktop/screen/desktop_remote_screen.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// multi-tab desktop remote screen
|
||||||
|
class DesktopRemoteScreen extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.imageModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.cursorModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||||
|
],
|
||||||
|
child: Scaffold(
|
||||||
|
body: ConnectionTabPage(
|
||||||
|
params: params,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
275
flutter/lib/desktop/widgets/peer_widget.dart
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/peer_model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import 'peercard_widget.dart';
|
||||||
|
|
||||||
|
typedef OffstageFunc = bool Function(Peer peer);
|
||||||
|
typedef PeerCardWidgetFunc = Widget Function(Peer peer);
|
||||||
|
|
||||||
|
/// for peer search text, global obs value
|
||||||
|
final peerSearchText = "".obs;
|
||||||
|
final peerSearchTextController =
|
||||||
|
TextEditingController(text: peerSearchText.value);
|
||||||
|
|
||||||
|
class _PeerWidget extends StatefulWidget {
|
||||||
|
late final _peers;
|
||||||
|
late final OffstageFunc _offstageFunc;
|
||||||
|
late final PeerCardWidgetFunc _peerCardWidgetFunc;
|
||||||
|
|
||||||
|
_PeerWidget(Peers peers, OffstageFunc offstageFunc,
|
||||||
|
PeerCardWidgetFunc peerCardWidgetFunc,
|
||||||
|
{Key? key})
|
||||||
|
: super(key: key) {
|
||||||
|
_peers = peers;
|
||||||
|
_offstageFunc = offstageFunc;
|
||||||
|
_peerCardWidgetFunc = peerCardWidgetFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PeerWidgetState createState() => _PeerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the peer widget.
|
||||||
|
class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
||||||
|
static const int _maxQueryCount = 3;
|
||||||
|
|
||||||
|
var _curPeers = Set<String>();
|
||||||
|
var _lastChangeTime = DateTime.now();
|
||||||
|
var _lastQueryPeers = Set<String>();
|
||||||
|
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
|
||||||
|
var _queryCoun = 0;
|
||||||
|
var _exit = false;
|
||||||
|
|
||||||
|
_PeerWidgetState() {
|
||||||
|
_startCheckOnlines();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
windowManager.addListener(this);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
_exit = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowFocus() {
|
||||||
|
_queryCoun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMinimize() {
|
||||||
|
_queryCoun = _maxQueryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final space = 12.0;
|
||||||
|
return ChangeNotifierProvider<Peers>(
|
||||||
|
create: (context) => super.widget._peers,
|
||||||
|
child: Consumer<Peers>(
|
||||||
|
builder: (context, peers, child) => peers.peers.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(translate("Empty")),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
child: ObxValue<RxString>((searchText) {
|
||||||
|
return FutureBuilder<List<Peer>>(
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final peers = snapshot.data!;
|
||||||
|
final cards = <Widget>[];
|
||||||
|
for (final peer in peers) {
|
||||||
|
cards.add(Offstage(
|
||||||
|
key: ValueKey("off${peer.id}"),
|
||||||
|
offstage: super.widget._offstageFunc(peer),
|
||||||
|
child: Obx(
|
||||||
|
() => SizedBox(
|
||||||
|
width: 220,
|
||||||
|
height:
|
||||||
|
peerCardUiType.value == PeerUiType.grid
|
||||||
|
? 140
|
||||||
|
: 42,
|
||||||
|
child: VisibilityDetector(
|
||||||
|
key: ValueKey(peer.id),
|
||||||
|
onVisibilityChanged: (info) {
|
||||||
|
final peerId =
|
||||||
|
(info.key as ValueKey).value;
|
||||||
|
if (info.visibleFraction > 0.00001) {
|
||||||
|
_curPeers.add(peerId);
|
||||||
|
} else {
|
||||||
|
_curPeers.remove(peerId);
|
||||||
|
}
|
||||||
|
_lastChangeTime = DateTime.now();
|
||||||
|
},
|
||||||
|
child: super
|
||||||
|
.widget
|
||||||
|
._peerCardWidgetFunc(peer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
return Wrap(
|
||||||
|
spacing: space,
|
||||||
|
runSpacing: space,
|
||||||
|
children: cards);
|
||||||
|
} else {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
future: matchPeers(searchText.value, peers.peers),
|
||||||
|
);
|
||||||
|
}, peerSearchText),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: todo
|
||||||
|
// TODO: variables walk through async tasks?
|
||||||
|
void _startCheckOnlines() {
|
||||||
|
() async {
|
||||||
|
while (!_exit) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
||||||
|
if (now.difference(_lastChangeTime) > Duration(seconds: 1)) {
|
||||||
|
if (_curPeers.length > 0) {
|
||||||
|
platformFFI.ffiBind
|
||||||
|
.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
|
_lastQueryPeers = {..._curPeers};
|
||||||
|
_lastQueryTime = DateTime.now();
|
||||||
|
_queryCoun = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_queryCoun < _maxQueryCount) {
|
||||||
|
if (now.difference(_lastQueryTime) > Duration(seconds: 20)) {
|
||||||
|
if (_curPeers.length > 0) {
|
||||||
|
platformFFI.ffiBind
|
||||||
|
.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
|
_lastQueryTime = DateTime.now();
|
||||||
|
_queryCoun += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BasePeerWidget extends StatelessWidget {
|
||||||
|
late final _name;
|
||||||
|
late final _loadEvent;
|
||||||
|
late final OffstageFunc _offstageFunc;
|
||||||
|
late final PeerCardWidgetFunc _peerCardWidgetFunc;
|
||||||
|
late final List<Peer> _initPeers;
|
||||||
|
|
||||||
|
BasePeerWidget({Key? key}) : super(key: key) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc,
|
||||||
|
_peerCardWidgetFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentPeerWidget extends BasePeerWidget {
|
||||||
|
RecentPeerWidget({Key? key}) : super(key: key) {
|
||||||
|
super._name = "recent peer";
|
||||||
|
super._loadEvent = "load_recent_peers";
|
||||||
|
super._offstageFunc = (Peer _peer) => false;
|
||||||
|
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(
|
||||||
|
peer: peer,
|
||||||
|
);
|
||||||
|
super._initPeers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final widget = super.build(context);
|
||||||
|
bind.mainLoadRecentPeers();
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FavoritePeerWidget extends BasePeerWidget {
|
||||||
|
FavoritePeerWidget({Key? key}) : super(key: key) {
|
||||||
|
super._name = "favorite peer";
|
||||||
|
super._loadEvent = "load_fav_peers";
|
||||||
|
super._offstageFunc = (Peer _peer) => false;
|
||||||
|
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer);
|
||||||
|
super._initPeers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final widget = super.build(context);
|
||||||
|
bind.mainLoadFavPeers();
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscoveredPeerWidget extends BasePeerWidget {
|
||||||
|
DiscoveredPeerWidget({Key? key}) : super(key: key) {
|
||||||
|
super._name = "discovered peer";
|
||||||
|
super._loadEvent = "load_lan_peers";
|
||||||
|
super._offstageFunc = (Peer _peer) => false;
|
||||||
|
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer);
|
||||||
|
super._initPeers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final widget = super.build(context);
|
||||||
|
bind.mainLoadLanPeers();
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddressBookPeerWidget extends BasePeerWidget {
|
||||||
|
AddressBookPeerWidget({Key? key}) : super(key: key) {
|
||||||
|
super._name = "address book peer";
|
||||||
|
super._offstageFunc =
|
||||||
|
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags);
|
||||||
|
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer);
|
||||||
|
super._initPeers = _loadPeers();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Peer> _loadPeers() {
|
||||||
|
return gFFI.abModel.peers.map((e) {
|
||||||
|
return Peer.fromJson(e['id'], e);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||||
|
if (selectedTags.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (idents.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (final tag in selectedTags) {
|
||||||
|
if (!idents.contains(tag)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
666
flutter/lib/desktop/widgets/peercard_widget.dart
Normal file
@@ -0,0 +1,666 @@
|
|||||||
|
import 'package:contextmenu/contextmenu.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/model.dart';
|
||||||
|
import '../../models/peer_model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
|
||||||
|
typedef PopupMenuItemsFunc = Future<List<PopupMenuItem<String>>> Function();
|
||||||
|
|
||||||
|
enum PeerType { recent, fav, discovered, ab }
|
||||||
|
|
||||||
|
enum PeerUiType { grid, list }
|
||||||
|
|
||||||
|
final peerCardUiType = PeerUiType.grid.obs;
|
||||||
|
|
||||||
|
class _PeerCard extends StatefulWidget {
|
||||||
|
final Peer peer;
|
||||||
|
final PopupMenuItemsFunc popupMenuItemsFunc;
|
||||||
|
final PeerType type;
|
||||||
|
|
||||||
|
_PeerCard(
|
||||||
|
{required this.peer,
|
||||||
|
required this.popupMenuItemsFunc,
|
||||||
|
Key? key,
|
||||||
|
required this.type})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PeerCardState createState() => _PeerCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the connection page.
|
||||||
|
class _PeerCardState extends State<_PeerCard>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
var _menuPos = RelativeRect.fill;
|
||||||
|
final double _cardRadis = 20;
|
||||||
|
final double _borderWidth = 2;
|
||||||
|
final RxBool _iconMoreHover = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
final peer = super.widget.peer;
|
||||||
|
var deco = Rx<BoxDecoration?>(BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
||||||
|
borderRadius: peerCardUiType.value == PeerUiType.grid
|
||||||
|
? BorderRadius.circular(_cardRadis)
|
||||||
|
: null));
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (evt) {
|
||||||
|
deco.value = BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.button, width: _borderWidth),
|
||||||
|
borderRadius: peerCardUiType.value == PeerUiType.grid
|
||||||
|
? BorderRadius.circular(_cardRadis)
|
||||||
|
: null);
|
||||||
|
},
|
||||||
|
onExit: (evt) {
|
||||||
|
deco.value = BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
||||||
|
borderRadius: peerCardUiType.value == PeerUiType.grid
|
||||||
|
? BorderRadius.circular(_cardRadis)
|
||||||
|
: null);
|
||||||
|
},
|
||||||
|
child: GestureDetector(
|
||||||
|
onDoubleTap: () => _connect(peer.id),
|
||||||
|
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
||||||
|
? _buildPeerCard(context, peer, deco)
|
||||||
|
: _buildPeerTile(context, peer, deco))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPeerTile(
|
||||||
|
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||||
|
final greyStyle =
|
||||||
|
TextStyle(fontSize: 12, color: MyTheme.color(context).lighterText);
|
||||||
|
return Obx(
|
||||||
|
() => Container(
|
||||||
|
foregroundDecoration: deco.value,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: _getPlatformImage('${peer.platform}', 30).paddingAll(6),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0, 4, 4, 4),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 5,
|
||||||
|
backgroundColor: peer.online
|
||||||
|
? Colors.green
|
||||||
|
: Colors.yellow)),
|
||||||
|
Text(
|
||||||
|
'${peer.id}',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w400),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FutureBuilder<String>(
|
||||||
|
future: bind.mainGetPeerOption(
|
||||||
|
id: peer.id, key: 'alias'),
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final name = snapshot.data!.isEmpty
|
||||||
|
? '${peer.username}@${peer.hostname}'
|
||||||
|
: snapshot.data!;
|
||||||
|
return Tooltip(
|
||||||
|
message: name,
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: greyStyle,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// alias has not arrived
|
||||||
|
return Text(
|
||||||
|
'${peer.username}@${peer.hostname}',
|
||||||
|
style: greyStyle,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_actionMore(peer),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 4.0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPeerCard(
|
||||||
|
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||||
|
return Card(
|
||||||
|
color: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Obx(
|
||||||
|
() => Container(
|
||||||
|
foregroundDecoration: deco.value,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(_cardRadis - _borderWidth),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child:
|
||||||
|
_getPlatformImage('${peer.platform}', 60),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder<String>(
|
||||||
|
future: bind.mainGetPeerOption(
|
||||||
|
id: peer.id, key: 'alias'),
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final name = snapshot.data!.isEmpty
|
||||||
|
? '${peer.username}@${peer.hostname}'
|
||||||
|
: snapshot.data!;
|
||||||
|
return Tooltip(
|
||||||
|
message: name,
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// alias has not arrived
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'${peer.username}@${peer.hostname}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingAll(4.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: MyTheme.color(context).bg,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0, 4, 8, 4),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 5,
|
||||||
|
backgroundColor: peer.online
|
||||||
|
? Colors.green
|
||||||
|
: Colors.yellow)),
|
||||||
|
Text('${peer.id}')
|
||||||
|
]).paddingSymmetric(vertical: 8),
|
||||||
|
_actionMore(peer),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12.0),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _actionMore(Peer peer) => Listener(
|
||||||
|
onPointerDown: (e) {
|
||||||
|
final x = e.position.dx;
|
||||||
|
final y = e.position.dy;
|
||||||
|
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
|
},
|
||||||
|
onPointerUp: (_) => _showPeerMenu(context, peer.id),
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) => _iconMoreHover.value = true,
|
||||||
|
onExit: (_) => _iconMoreHover.value = false,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: _iconMoreHover.value
|
||||||
|
? MyTheme.color(context).grayBg!
|
||||||
|
: MyTheme.color(context).bg!,
|
||||||
|
child: Icon(Icons.more_vert,
|
||||||
|
size: 18,
|
||||||
|
color: _iconMoreHover.value
|
||||||
|
? MyTheme.color(context).text
|
||||||
|
: MyTheme.color(context).lightText))));
|
||||||
|
|
||||||
|
/// Connect to a peer with [id].
|
||||||
|
/// If [isFileTransfer], starts a session only for file transfer.
|
||||||
|
void _connect(String id, {bool isFileTransfer = false}) async {
|
||||||
|
if (id == '') return;
|
||||||
|
id = id.replaceAll(' ', '');
|
||||||
|
if (isFileTransfer) {
|
||||||
|
await rustDeskWinManager.new_file_transfer(id);
|
||||||
|
} else {
|
||||||
|
await rustDeskWinManager.new_remote_desktop(id);
|
||||||
|
}
|
||||||
|
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||||
|
if (!currentFocus.hasPrimaryFocus) {
|
||||||
|
currentFocus.unfocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the peer menu and handle user's choice.
|
||||||
|
/// User might remove the peer or send a file to the peer.
|
||||||
|
void _showPeerMenu(BuildContext context, String id) async {
|
||||||
|
var value = await showMenu(
|
||||||
|
context: context,
|
||||||
|
position: _menuPos,
|
||||||
|
items: await super.widget.popupMenuItemsFunc(),
|
||||||
|
elevation: 8,
|
||||||
|
);
|
||||||
|
if (value == 'remove') {
|
||||||
|
await bind.mainRemovePeer(id: id);
|
||||||
|
removePreference(id);
|
||||||
|
Get.forceAppUpdate(); // TODO use inner model / state
|
||||||
|
} else if (value == 'file') {
|
||||||
|
_connect(id, isFileTransfer: true);
|
||||||
|
} else if (value == 'add-fav') {
|
||||||
|
final favs = (await bind.mainGetFav()).toList();
|
||||||
|
if (favs.indexOf(id) < 0) {
|
||||||
|
favs.add(id);
|
||||||
|
bind.mainStoreFav(favs: favs);
|
||||||
|
}
|
||||||
|
} else if (value == 'remove-fav') {
|
||||||
|
final favs = (await bind.mainGetFav()).toList();
|
||||||
|
if (favs.remove(id)) {
|
||||||
|
bind.mainStoreFav(favs: favs);
|
||||||
|
Get.forceAppUpdate(); // TODO use inner model / state
|
||||||
|
}
|
||||||
|
} else if (value == 'connect') {
|
||||||
|
_connect(id, isFileTransfer: false);
|
||||||
|
} else if (value == 'ab-delete') {
|
||||||
|
gFFI.abModel.deletePeer(id);
|
||||||
|
await gFFI.abModel.updateAb();
|
||||||
|
setState(() {});
|
||||||
|
} else if (value == 'ab-edit-tag') {
|
||||||
|
_abEditTag(id);
|
||||||
|
} else if (value == 'rename') {
|
||||||
|
_rename(id);
|
||||||
|
} else if (value == 'unremember-password') {
|
||||||
|
await bind.mainForgetPassword(id: id);
|
||||||
|
} else if (value == 'force-always-relay') {
|
||||||
|
String value;
|
||||||
|
String oldValue =
|
||||||
|
await bind.mainGetPeerOption(id: id, key: 'force-always-relay');
|
||||||
|
if (oldValue.isEmpty) {
|
||||||
|
value = 'Y';
|
||||||
|
} else {
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'force-always-relay', value: value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTag(String tagName, RxList<dynamic> rxTags,
|
||||||
|
{Function()? onTap}) {
|
||||||
|
return ContextMenuArea(
|
||||||
|
width: 100,
|
||||||
|
builder: (context) => [
|
||||||
|
ListTile(
|
||||||
|
title: Text(translate("Delete")),
|
||||||
|
onTap: () {
|
||||||
|
gFFI.abModel.deleteTag(tagName);
|
||||||
|
gFFI.abModel.updateAb();
|
||||||
|
Future.delayed(Duration.zero, () => Get.back());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Obx(
|
||||||
|
() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: rxTags.contains(tagName) ? Colors.blue : null,
|
||||||
|
border: Border.all(color: MyTheme.darkGray),
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
tagName,
|
||||||
|
style: TextStyle(
|
||||||
|
color: rxTags.contains(tagName) ? MyTheme.white : null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the image for the current [platform].
|
||||||
|
Widget _getPlatformImage(String platform, double size) {
|
||||||
|
platform = platform.toLowerCase();
|
||||||
|
if (platform == 'mac os')
|
||||||
|
platform = 'mac';
|
||||||
|
else if (platform != 'linux' && platform != 'android') platform = 'win';
|
||||||
|
return Image.asset('assets/$platform.png', height: size, width: size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _abEditTag(String id) {
|
||||||
|
var isInProgress = false;
|
||||||
|
|
||||||
|
final tags = List.of(gFFI.abModel.tags);
|
||||||
|
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||||
|
|
||||||
|
gFFI.dialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Edit Tag")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Wrap(
|
||||||
|
children: tags
|
||||||
|
.map((e) => _buildTag(e, selectedTag, onTap: () {
|
||||||
|
if (selectedTag.contains(e)) {
|
||||||
|
selectedTag.remove(e);
|
||||||
|
} else {
|
||||||
|
selectedTag.add(e);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.toList(growable: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||||
|
await gFFI.abModel.updateAb();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _rename(String id) async {
|
||||||
|
var isInProgress = false;
|
||||||
|
var name = await bind.mainGetPeerOption(id: id, key: 'alias');
|
||||||
|
if (widget.type == PeerType.ab) {
|
||||||
|
final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']);
|
||||||
|
if (peer == null) {
|
||||||
|
// this should not happen
|
||||||
|
} else {
|
||||||
|
name = peer['alias'] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final k = GlobalKey<FormState>();
|
||||||
|
gFFI.dialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Rename")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Form(
|
||||||
|
key: k,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: TextEditingController(text: name),
|
||||||
|
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||||
|
onChanged: (newStr) {
|
||||||
|
name = newStr;
|
||||||
|
},
|
||||||
|
validator: (s) {
|
||||||
|
if (s == null || s.isEmpty) {
|
||||||
|
return translate("Empty");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onSaved: (s) {
|
||||||
|
name = s ?? "unnamed";
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
if (k.currentState != null) {
|
||||||
|
if (k.currentState!.validate()) {
|
||||||
|
k.currentState!.save();
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'alias', value: name);
|
||||||
|
if (widget.type == PeerType.ab) {
|
||||||
|
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||||
|
await gFFI.abModel.updateAb();
|
||||||
|
} else {
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
this.setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BasePeerCard extends StatelessWidget {
|
||||||
|
final Peer peer;
|
||||||
|
final PeerType type;
|
||||||
|
|
||||||
|
BasePeerCard({required this.peer, required this.type, Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _PeerCard(
|
||||||
|
peer: peer,
|
||||||
|
popupMenuItemsFunc: _getPopupMenuItems,
|
||||||
|
type: type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentPeerCard extends BasePeerCard {
|
||||||
|
RecentPeerCard({required Peer peer, Key? key})
|
||||||
|
: super(peer: peer, key: key, type: PeerType.recent);
|
||||||
|
|
||||||
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Transfer File')), value: 'file'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
|
||||||
|
await _forceAlwaysRelayMenuItem(peer.id),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Unremember Password')),
|
||||||
|
value: 'unremember-password'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FavoritePeerCard extends BasePeerCard {
|
||||||
|
FavoritePeerCard({required Peer peer, Key? key})
|
||||||
|
: super(peer: peer, key: key, type: PeerType.fav);
|
||||||
|
|
||||||
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Transfer File')), value: 'file'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
|
||||||
|
await _forceAlwaysRelayMenuItem(peer.id),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Unremember Password')),
|
||||||
|
value: 'unremember-password'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Remove from Favorites')), value: 'remove-fav'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscoveredPeerCard extends BasePeerCard {
|
||||||
|
DiscoveredPeerCard({required Peer peer, Key? key})
|
||||||
|
: super(peer: peer, key: key, type: PeerType.discovered);
|
||||||
|
|
||||||
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Transfer File')), value: 'file'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
|
||||||
|
await _forceAlwaysRelayMenuItem(peer.id),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Unremember Password')),
|
||||||
|
value: 'unremember-password'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddressBookPeerCard extends BasePeerCard {
|
||||||
|
AddressBookPeerCard({required Peer peer, Key? key})
|
||||||
|
: super(peer: peer, key: key, type: PeerType.ab);
|
||||||
|
|
||||||
|
Future<List<PopupMenuItem<String>>> _getPopupMenuItems() async {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Connect')), value: 'connect'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Transfer File')), value: 'file'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
|
||||||
|
await _forceAlwaysRelayMenuItem(peer.id),
|
||||||
|
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Remove')), value: 'ab-delete'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Unremember Password')),
|
||||||
|
value: 'unremember-password'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Add to Favorites')), value: 'add-fav'),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: Text(translate('Edit Tag')), value: 'ab-edit-tag'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PopupMenuItem<String>> _forceAlwaysRelayMenuItem(String id) async {
|
||||||
|
bool force_always_relay =
|
||||||
|
(await bind.mainGetPeerOption(id: id, key: 'force-always-relay'))
|
||||||
|
.isNotEmpty;
|
||||||
|
return PopupMenuItem<String>(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !force_always_relay,
|
||||||
|
child: Icon(Icons.check),
|
||||||
|
),
|
||||||
|
Text(translate('Always connect via relay')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: 'force-always-relay');
|
||||||
|
}
|
||||||
589
flutter/lib/desktop/widgets/tabbar_widget.dart
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:scroll_pos/scroll_pos.dart';
|
||||||
|
|
||||||
|
import '../../utils/multi_window_manager.dart';
|
||||||
|
|
||||||
|
const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
|
||||||
|
const double _kIconSize = 18;
|
||||||
|
const double _kDividerIndent = 10;
|
||||||
|
const double _kActionIconSize = 12;
|
||||||
|
|
||||||
|
class TabInfo {
|
||||||
|
final String key;
|
||||||
|
final String label;
|
||||||
|
final IconData? selectedIcon;
|
||||||
|
final IconData? unselectedIcon;
|
||||||
|
final bool closable;
|
||||||
|
final Widget page;
|
||||||
|
|
||||||
|
TabInfo(
|
||||||
|
{required this.key,
|
||||||
|
required this.label,
|
||||||
|
this.selectedIcon,
|
||||||
|
this.unselectedIcon,
|
||||||
|
this.closable = true,
|
||||||
|
required this.page});
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesktopTabState {
|
||||||
|
final List<TabInfo> tabs = [];
|
||||||
|
final ScrollPosController scrollController =
|
||||||
|
ScrollPosController(itemCount: 0);
|
||||||
|
final PageController pageController = PageController();
|
||||||
|
int selected = 0;
|
||||||
|
|
||||||
|
DesktopTabState() {
|
||||||
|
scrollController.itemCount = tabs.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesktopTabController {
|
||||||
|
final state = DesktopTabState().obs;
|
||||||
|
|
||||||
|
/// index, key
|
||||||
|
Function(int, String)? onRemove;
|
||||||
|
|
||||||
|
Function(int)? onSelected;
|
||||||
|
|
||||||
|
void add(TabInfo tab) {
|
||||||
|
if (!isDesktop) return;
|
||||||
|
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
|
||||||
|
int toIndex;
|
||||||
|
if (index >= 0) {
|
||||||
|
toIndex = index;
|
||||||
|
} else {
|
||||||
|
state.update((val) {
|
||||||
|
val!.tabs.add(tab);
|
||||||
|
});
|
||||||
|
toIndex = state.value.tabs.length - 1;
|
||||||
|
assert(toIndex >= 0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
jumpTo(toIndex);
|
||||||
|
} catch (e) {
|
||||||
|
// call before binding controller will throw
|
||||||
|
debugPrint("Failed to jumpTo: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(int index) {
|
||||||
|
if (!isDesktop) return;
|
||||||
|
final len = state.value.tabs.length;
|
||||||
|
if (index < 0 || index > len - 1) return;
|
||||||
|
final key = state.value.tabs[index].key;
|
||||||
|
final currentSelected = state.value.selected;
|
||||||
|
int toIndex = 0;
|
||||||
|
if (index == len - 1) {
|
||||||
|
toIndex = max(0, currentSelected - 1);
|
||||||
|
} else if (index < len - 1 && index < currentSelected) {
|
||||||
|
toIndex = max(0, currentSelected - 1);
|
||||||
|
}
|
||||||
|
state.value.tabs.removeAt(index);
|
||||||
|
state.value.scrollController.itemCount = state.value.tabs.length;
|
||||||
|
jumpTo(toIndex);
|
||||||
|
onRemove?.call(index, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void jumpTo(int index) {
|
||||||
|
state.update((val) {
|
||||||
|
val!.selected = index;
|
||||||
|
val.pageController.jumpToPage(index);
|
||||||
|
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||||
|
});
|
||||||
|
onSelected?.call(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeBy(String? key) {
|
||||||
|
if (!isDesktop) return;
|
||||||
|
assert(onRemove != null);
|
||||||
|
if (key == null) {
|
||||||
|
if (state.value.selected < state.value.tabs.length) {
|
||||||
|
remove(state.value.selected);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.value.tabs.indexWhere((tab) => tab.key == key);
|
||||||
|
remove(state.value.selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesktopTab extends StatelessWidget {
|
||||||
|
final Function(String)? onTabClose;
|
||||||
|
final TarBarTheme theme;
|
||||||
|
final bool isMainWindow;
|
||||||
|
final bool showTabBar;
|
||||||
|
final bool showLogo;
|
||||||
|
final bool showTitle;
|
||||||
|
final bool showMinimize;
|
||||||
|
final bool showMaximize;
|
||||||
|
final bool showClose;
|
||||||
|
final Widget Function(Widget pageView)? pageViewBuilder;
|
||||||
|
final Widget? tail;
|
||||||
|
|
||||||
|
final DesktopTabController controller;
|
||||||
|
late final state = controller.state;
|
||||||
|
|
||||||
|
DesktopTab(
|
||||||
|
{required this.controller,
|
||||||
|
required this.isMainWindow,
|
||||||
|
this.theme = const TarBarTheme.light(),
|
||||||
|
this.onTabClose,
|
||||||
|
this.showTabBar = true,
|
||||||
|
this.showLogo = true,
|
||||||
|
this.showTitle = true,
|
||||||
|
this.showMinimize = true,
|
||||||
|
this.showMaximize = true,
|
||||||
|
this.showClose = true,
|
||||||
|
this.pageViewBuilder,
|
||||||
|
this.tail});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !showTabBar,
|
||||||
|
child: Container(
|
||||||
|
height: _kTabBarHeight,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: _kTabBarHeight - 1,
|
||||||
|
child: _buildBar(),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
child: pageViewBuilder != null
|
||||||
|
? pageViewBuilder!(_buildPageView())
|
||||||
|
: _buildPageView())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPageView() {
|
||||||
|
return Obx(() => PageView(
|
||||||
|
controller: state.value.pageController,
|
||||||
|
children:
|
||||||
|
state.value.tabs.map((tab) => tab.page).toList(growable: false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBar() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !showLogo,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/logo.ico',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
)),
|
||||||
|
Offstage(
|
||||||
|
offstage: !showTitle,
|
||||||
|
child: Text(
|
||||||
|
"RustDesk",
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
).marginOnly(left: 2))
|
||||||
|
]).marginOnly(
|
||||||
|
left: 5,
|
||||||
|
right: 10,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onPanStart: (_) {
|
||||||
|
if (isMainWindow) {
|
||||||
|
windowManager.startDragging();
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(windowId!)
|
||||||
|
.startDragging();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: _ListView(
|
||||||
|
controller: controller,
|
||||||
|
onTabClose: onTabClose,
|
||||||
|
theme: theme,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Offstage(offstage: tail == null, child: tail),
|
||||||
|
WindowActionPanel(
|
||||||
|
mainTab: isMainWindow,
|
||||||
|
theme: theme,
|
||||||
|
showMinimize: showMinimize,
|
||||||
|
showMaximize: showMaximize,
|
||||||
|
showClose: showClose,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowActionPanel extends StatelessWidget {
|
||||||
|
final bool mainTab;
|
||||||
|
final TarBarTheme theme;
|
||||||
|
|
||||||
|
final bool showMinimize;
|
||||||
|
final bool showMaximize;
|
||||||
|
final bool showClose;
|
||||||
|
|
||||||
|
const WindowActionPanel(
|
||||||
|
{Key? key,
|
||||||
|
required this.mainTab,
|
||||||
|
required this.theme,
|
||||||
|
this.showMinimize = true,
|
||||||
|
this.showMaximize = true,
|
||||||
|
this.showClose = true})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !showMinimize,
|
||||||
|
child: ActionIcon(
|
||||||
|
message: 'Minimize',
|
||||||
|
icon: IconFont.min,
|
||||||
|
theme: theme,
|
||||||
|
onTap: () {
|
||||||
|
if (mainTab) {
|
||||||
|
windowManager.minimize();
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(windowId!).minimize();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
is_close: false,
|
||||||
|
)),
|
||||||
|
// TODO: drag makes window restore
|
||||||
|
Offstage(
|
||||||
|
offstage: !showMaximize,
|
||||||
|
child: FutureBuilder(builder: (context, snapshot) {
|
||||||
|
RxBool is_maximized = false.obs;
|
||||||
|
if (mainTab) {
|
||||||
|
windowManager.isMaximized().then((maximized) {
|
||||||
|
is_maximized.value = maximized;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
|
wc.isMaximized().then((maximized) {
|
||||||
|
is_maximized.value = maximized;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Obx(
|
||||||
|
() => ActionIcon(
|
||||||
|
message: is_maximized.value ? "Restore" : "Maximize",
|
||||||
|
icon: is_maximized.value ? IconFont.restore : IconFont.max,
|
||||||
|
theme: theme,
|
||||||
|
onTap: () {
|
||||||
|
if (mainTab) {
|
||||||
|
if (is_maximized.value) {
|
||||||
|
windowManager.unmaximize();
|
||||||
|
} else {
|
||||||
|
windowManager.maximize();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: subwindow is maximized but first query result is not maximized.
|
||||||
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
|
if (is_maximized.value) {
|
||||||
|
wc.unmaximize();
|
||||||
|
} else {
|
||||||
|
wc.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_maximized.value = !is_maximized.value;
|
||||||
|
},
|
||||||
|
is_close: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
Offstage(
|
||||||
|
offstage: !showClose,
|
||||||
|
child: ActionIcon(
|
||||||
|
message: 'Close',
|
||||||
|
icon: IconFont.close,
|
||||||
|
theme: theme,
|
||||||
|
onTap: () {
|
||||||
|
if (mainTab) {
|
||||||
|
windowManager.close();
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(windowId!).close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
is_close: true,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class _ListView extends StatelessWidget {
|
||||||
|
final DesktopTabController controller;
|
||||||
|
late final Rx<DesktopTabState> state;
|
||||||
|
final Function(String key)? onTabClose;
|
||||||
|
final TarBarTheme theme;
|
||||||
|
|
||||||
|
_ListView(
|
||||||
|
{required this.controller, required this.onTabClose, required this.theme})
|
||||||
|
: this.state = controller.state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() => ListView(
|
||||||
|
controller: state.value.scrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: BouncingScrollPhysics(),
|
||||||
|
children: state.value.tabs.asMap().entries.map((e) {
|
||||||
|
final index = e.key;
|
||||||
|
final tab = e.value;
|
||||||
|
return _Tab(
|
||||||
|
index: index,
|
||||||
|
label: tab.label,
|
||||||
|
selectedIcon: tab.selectedIcon,
|
||||||
|
unselectedIcon: tab.unselectedIcon,
|
||||||
|
closable: tab.closable,
|
||||||
|
selected: state.value.selected,
|
||||||
|
onClose: () => controller.remove(index),
|
||||||
|
onSelected: () => controller.jumpTo(index),
|
||||||
|
theme: theme,
|
||||||
|
);
|
||||||
|
}).toList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Tab extends StatelessWidget {
|
||||||
|
late final int index;
|
||||||
|
late final String label;
|
||||||
|
late final IconData? selectedIcon;
|
||||||
|
late final IconData? unselectedIcon;
|
||||||
|
late final bool closable;
|
||||||
|
late final int selected;
|
||||||
|
late final Function() onClose;
|
||||||
|
late final Function() onSelected;
|
||||||
|
final RxBool _hover = false.obs;
|
||||||
|
late final TarBarTheme theme;
|
||||||
|
|
||||||
|
_Tab(
|
||||||
|
{Key? key,
|
||||||
|
required this.index,
|
||||||
|
required this.label,
|
||||||
|
this.selectedIcon,
|
||||||
|
this.unselectedIcon,
|
||||||
|
required this.closable,
|
||||||
|
required this.selected,
|
||||||
|
required this.onClose,
|
||||||
|
required this.onSelected,
|
||||||
|
required this.theme})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool show_icon = selectedIcon != null && unselectedIcon != null;
|
||||||
|
bool is_selected = index == selected;
|
||||||
|
bool show_divider = index != selected - 1 && index != selected;
|
||||||
|
return Ink(
|
||||||
|
child: InkWell(
|
||||||
|
onHover: (hover) => _hover.value = hover,
|
||||||
|
onTap: () => onSelected(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: _kTabBarHeight,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !show_icon,
|
||||||
|
child: Icon(
|
||||||
|
is_selected ? selectedIcon : unselectedIcon,
|
||||||
|
size: _kIconSize,
|
||||||
|
color: is_selected
|
||||||
|
? theme.selectedtabIconColor
|
||||||
|
: theme.unSelectedtabIconColor,
|
||||||
|
).paddingOnly(right: 5)),
|
||||||
|
Text(
|
||||||
|
translate(label),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: is_selected
|
||||||
|
? theme.selectedTextColor
|
||||||
|
: theme.unSelectedTextColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !closable,
|
||||||
|
child: Obx((() => _CloseButton(
|
||||||
|
visiable: _hover.value,
|
||||||
|
tabSelected: is_selected,
|
||||||
|
onClose: () => onClose(),
|
||||||
|
theme: theme,
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
])).paddingSymmetric(horizontal: 10),
|
||||||
|
Offstage(
|
||||||
|
offstage: !show_divider,
|
||||||
|
child: VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
indent: _kDividerIndent,
|
||||||
|
endIndent: _kDividerIndent,
|
||||||
|
color: theme.dividerColor,
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CloseButton extends StatelessWidget {
|
||||||
|
final bool visiable;
|
||||||
|
final bool tabSelected;
|
||||||
|
final Function onClose;
|
||||||
|
late final TarBarTheme theme;
|
||||||
|
|
||||||
|
_CloseButton({
|
||||||
|
Key? key,
|
||||||
|
required this.visiable,
|
||||||
|
required this.tabSelected,
|
||||||
|
required this.onClose,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: _kIconSize,
|
||||||
|
child: Offstage(
|
||||||
|
offstage: !visiable,
|
||||||
|
child: InkWell(
|
||||||
|
customBorder: RoundedRectangleBorder(),
|
||||||
|
onTap: () => onClose(),
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: _kIconSize,
|
||||||
|
color: tabSelected
|
||||||
|
? theme.selectedIconColor
|
||||||
|
: theme.unSelectedIconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)).paddingOnly(left: 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionIcon extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
final IconData icon;
|
||||||
|
final TarBarTheme theme;
|
||||||
|
final Function() onTap;
|
||||||
|
final bool is_close;
|
||||||
|
const ActionIcon({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
required this.icon,
|
||||||
|
required this.theme,
|
||||||
|
required this.onTap,
|
||||||
|
required this.is_close,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
RxBool hover = false.obs;
|
||||||
|
return Obx(() => Tooltip(
|
||||||
|
message: translate(message),
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
child: InkWell(
|
||||||
|
hoverColor:
|
||||||
|
is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor,
|
||||||
|
onHover: (value) => hover.value = value,
|
||||||
|
child: Container(
|
||||||
|
height: _kTabBarHeight - 1,
|
||||||
|
width: _kTabBarHeight - 1,
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: hover.value && is_close
|
||||||
|
? Colors.white
|
||||||
|
: theme.unSelectedIconColor,
|
||||||
|
size: _kActionIconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddButton extends StatelessWidget {
|
||||||
|
late final TarBarTheme theme;
|
||||||
|
|
||||||
|
AddButton({
|
||||||
|
Key? key,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ActionIcon(
|
||||||
|
message: 'New Connection',
|
||||||
|
icon: IconFont.add,
|
||||||
|
theme: theme,
|
||||||
|
onTap: () =>
|
||||||
|
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
|
||||||
|
is_close: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TarBarTheme {
|
||||||
|
final Color unSelectedtabIconColor;
|
||||||
|
final Color selectedtabIconColor;
|
||||||
|
final Color selectedTextColor;
|
||||||
|
final Color unSelectedTextColor;
|
||||||
|
final Color selectedIconColor;
|
||||||
|
final Color unSelectedIconColor;
|
||||||
|
final Color dividerColor;
|
||||||
|
final Color hoverColor;
|
||||||
|
|
||||||
|
const TarBarTheme.light()
|
||||||
|
: unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241),
|
||||||
|
selectedtabIconColor = MyTheme.accent,
|
||||||
|
selectedTextColor = const Color.fromARGB(255, 26, 26, 26),
|
||||||
|
unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96),
|
||||||
|
selectedIconColor = const Color.fromARGB(255, 26, 26, 26),
|
||||||
|
unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96),
|
||||||
|
dividerColor = const Color.fromARGB(255, 238, 238, 238),
|
||||||
|
hoverColor = const Color.fromARGB(
|
||||||
|
51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E
|
||||||
|
|
||||||
|
const TarBarTheme.dark()
|
||||||
|
: unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98),
|
||||||
|
selectedtabIconColor = MyTheme.accent,
|
||||||
|
selectedTextColor = const Color.fromARGB(255, 255, 255, 255),
|
||||||
|
unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207),
|
||||||
|
selectedIconColor = const Color.fromARGB(255, 215, 215, 215),
|
||||||
|
unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255),
|
||||||
|
dividerColor = const Color.fromARGB(255, 64, 64, 64),
|
||||||
|
hoverColor = Colors.black26;
|
||||||
|
}
|
||||||
70
flutter/lib/desktop/widgets/titlebar_widget.dart
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const sidebarColor = Color(0xFF0C6AF6);
|
||||||
|
const backgroundStartColor = Color(0xFF0583EA);
|
||||||
|
const backgroundEndColor = Color(0xFF0697EA);
|
||||||
|
|
||||||
|
class DesktopTitleBar extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
const DesktopTitleBar({Key? key, this.child}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [backgroundStartColor, backgroundEndColor],
|
||||||
|
stops: [0.0, 1.0]),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: child ?? Offstage(),
|
||||||
|
)
|
||||||
|
// const WindowButtons()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// final buttonColors = WindowButtonColors(
|
||||||
|
// iconNormal: const Color(0xFF805306),
|
||||||
|
// mouseOver: const Color(0xFFF6A00C),
|
||||||
|
// mouseDown: const Color(0xFF805306),
|
||||||
|
// iconMouseOver: const Color(0xFF805306),
|
||||||
|
// iconMouseDown: const Color(0xFFFFD500));
|
||||||
|
//
|
||||||
|
// final closeButtonColors = WindowButtonColors(
|
||||||
|
// mouseOver: const Color(0xFFD32F2F),
|
||||||
|
// mouseDown: const Color(0xFFB71C1C),
|
||||||
|
// iconNormal: const Color(0xFF805306),
|
||||||
|
// iconMouseOver: Colors.white);
|
||||||
|
//
|
||||||
|
// class WindowButtons extends StatelessWidget {
|
||||||
|
// const WindowButtons({Key? key}) : super(key: key);
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return Row(
|
||||||
|
// children: [
|
||||||
|
// MinimizeWindowButton(colors: buttonColors, onPressed: () {
|
||||||
|
// windowManager.minimize();
|
||||||
|
// },),
|
||||||
|
// MaximizeWindowButton(colors: buttonColors, onPressed: () async {
|
||||||
|
// if (await windowManager.isMaximized()) {
|
||||||
|
// windowManager.restore();
|
||||||
|
// } else {
|
||||||
|
// windowManager.maximize();
|
||||||
|
// }
|
||||||
|
// },),
|
||||||
|
// CloseWindowButton(colors: closeButtonColors, onPressed: () {
|
||||||
|
// windowManager.close();
|
||||||
|
// },),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -1,55 +1,211 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'dart:convert';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
|
||||||
import 'common.dart';
|
|
||||||
import 'models/model.dart';
|
|
||||||
import 'pages/home_page.dart';
|
|
||||||
import 'pages/server_page.dart';
|
|
||||||
import 'pages/settings_page.dart';
|
|
||||||
|
|
||||||
Future<Null> main() async {
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
// import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'consts.dart';
|
||||||
|
import 'mobile/pages/home_page.dart';
|
||||||
|
import 'mobile/pages/server_page.dart';
|
||||||
|
import 'mobile/pages/settings_page.dart';
|
||||||
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
|
int? windowId;
|
||||||
|
|
||||||
|
Future<Null> main(List<String> args) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
var a = FFI.ffiModel.init();
|
print("launch args: $args");
|
||||||
var b = Firebase.initializeApp();
|
|
||||||
await a;
|
if (!isDesktop) {
|
||||||
await b;
|
runMobileApp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// main window
|
||||||
|
if (args.isNotEmpty && args.first == 'multi_window') {
|
||||||
|
windowId = int.parse(args[1]);
|
||||||
|
WindowController.fromWindowId(windowId!).showTitleBar(false);
|
||||||
|
final argument = args[2].isEmpty
|
||||||
|
? Map<String, dynamic>()
|
||||||
|
: jsonDecode(args[2]) as Map<String, dynamic>;
|
||||||
|
int type = argument['type'] ?? -1;
|
||||||
|
argument['windowId'] = windowId;
|
||||||
|
WindowType wType = type.windowType;
|
||||||
|
switch (wType) {
|
||||||
|
case WindowType.RemoteDesktop:
|
||||||
|
runRemoteScreen(argument);
|
||||||
|
break;
|
||||||
|
case WindowType.FileTransfer:
|
||||||
|
runFileTransferScreen(argument);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (args.isNotEmpty && args.first == '--cm') {
|
||||||
|
print("--cm started");
|
||||||
|
await windowManager.ensureInitialized();
|
||||||
|
runConnectionManagerScreen();
|
||||||
|
} else {
|
||||||
|
await windowManager.ensureInitialized();
|
||||||
|
windowManager.setPreventClose(true);
|
||||||
|
runMainApp(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData getCurrentTheme() {
|
||||||
|
return isDarkTheme() ? MyTheme.darkTheme : MyTheme.lightTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initEnv(String appType) async {
|
||||||
|
await platformFFI.init(appType);
|
||||||
|
// global FFI, use this **ONLY** for global configuration
|
||||||
|
// for convenience, use global FFI on mobile platform
|
||||||
|
// focus on multi-ffi on desktop first
|
||||||
|
await initGlobalFFI();
|
||||||
|
// await Firebase.initializeApp();
|
||||||
refreshCurrentUser();
|
refreshCurrentUser();
|
||||||
toAndroidChannelInit();
|
}
|
||||||
|
|
||||||
|
void runMainApp(bool startService) async {
|
||||||
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720));
|
||||||
|
await Future.wait([
|
||||||
|
initEnv(kAppTypeMain),
|
||||||
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
await windowManager.show();
|
||||||
|
await windowManager.focus();
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
if (startService) {
|
||||||
|
// await windowManager.ensureInitialized();
|
||||||
|
// disable tray
|
||||||
|
// initTray();
|
||||||
|
gFFI.serverModel.startService();
|
||||||
|
}
|
||||||
runApp(App());
|
runApp(App());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void runMobileApp() async {
|
||||||
|
await initEnv(kAppTypeMain);
|
||||||
|
if (isAndroid) androidChannelInit();
|
||||||
|
runApp(App());
|
||||||
|
}
|
||||||
|
|
||||||
|
void runRemoteScreen(Map<String, dynamic> argument) async {
|
||||||
|
await initEnv(kAppTypeDesktopRemote);
|
||||||
|
runApp(GetMaterialApp(
|
||||||
|
navigatorKey: globalKey,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
title: 'RustDesk - Remote Desktop',
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
home: DesktopRemoteScreen(
|
||||||
|
params: argument,
|
||||||
|
),
|
||||||
|
navigatorObservers: [
|
||||||
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
|
],
|
||||||
|
builder: _keepScaleBuilder(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void runFileTransferScreen(Map<String, dynamic> argument) async {
|
||||||
|
await initEnv(kAppTypeDesktopFileTransfer);
|
||||||
|
runApp(
|
||||||
|
GetMaterialApp(
|
||||||
|
navigatorKey: globalKey,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
title: 'RustDesk - File Transfer',
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
home: DesktopFileTransferScreen(params: argument),
|
||||||
|
navigatorObservers: [
|
||||||
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
|
],
|
||||||
|
builder: _keepScaleBuilder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void runConnectionManagerScreen() async {
|
||||||
|
// initialize window
|
||||||
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
|
||||||
|
await Future.wait([
|
||||||
|
initEnv(kAppTypeMain),
|
||||||
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
await windowManager.setAlignment(Alignment.topRight);
|
||||||
|
await windowManager.show();
|
||||||
|
await windowManager.focus();
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
runApp(GetMaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
home: DesktopServerPage(),
|
||||||
|
builder: _keepScaleBuilder()));
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowOptions getHiddenTitleBarWindowOptions(Size size) {
|
||||||
|
return WindowOptions(
|
||||||
|
size: size,
|
||||||
|
center: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
skipTaskbar: false,
|
||||||
|
titleBarStyle: TitleBarStyle.hidden,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final analytics = FirebaseAnalytics.instance;
|
// final analytics = FirebaseAnalytics.instance;
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider.value(value: FFI.ffiModel),
|
// global configuration
|
||||||
ChangeNotifierProvider.value(value: FFI.imageModel),
|
// use session related FFI when in remote control or file transfer page
|
||||||
ChangeNotifierProvider.value(value: FFI.cursorModel),
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
ChangeNotifierProvider.value(value: FFI.canvasModel),
|
ChangeNotifierProvider.value(value: gFFI.imageModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.cursorModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.abModel),
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.userModel),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: GetMaterialApp(
|
||||||
navigatorKey: globalKey,
|
navigatorKey: globalKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'RustDesk',
|
title: 'RustDesk',
|
||||||
theme: ThemeData(
|
theme: getCurrentTheme(),
|
||||||
primarySwatch: Colors.blue,
|
home: isDesktop
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
? DesktopTabPage()
|
||||||
),
|
: !isAndroid
|
||||||
home: !isAndroid ? WebHomePage() : HomePage(key: homeKey),
|
? WebHomePage()
|
||||||
navigatorObservers: [
|
: HomePage(),
|
||||||
FirebaseAnalyticsObserver(analytics: analytics),
|
navigatorObservers: [
|
||||||
FlutterSmartDialog.observer
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
],
|
],
|
||||||
builder: FlutterSmartDialog.init(
|
builder: isAndroid
|
||||||
builder: isAndroid
|
? (_, child) => AccessibilityListener(
|
||||||
? (_, child) => AccessibilityListener(
|
child: child,
|
||||||
child: child,
|
)
|
||||||
)
|
: _keepScaleBuilder(),
|
||||||
: null)),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_keepScaleBuilder() {
|
||||||
|
return (BuildContext context, Widget? child) {
|
||||||
|
return MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
textScaleFactor: 1.0,
|
||||||
|
),
|
||||||
|
child: child ?? Container(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../models/model.dart';
|
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
|
|
||||||
class ChatPage extends StatelessWidget implements PageShape {
|
class ChatPage extends StatelessWidget implements PageShape {
|
||||||
|
late final ChatModel chatModel;
|
||||||
|
|
||||||
|
ChatPage({ChatModel? chatModel}) {
|
||||||
|
this.chatModel = chatModel ?? gFFI.chatModel;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final title = translate("Chat");
|
final title = translate("Chat");
|
||||||
|
|
||||||
@@ -18,7 +24,8 @@ class ChatPage extends StatelessWidget implements PageShape {
|
|||||||
PopupMenuButton<int>(
|
PopupMenuButton<int>(
|
||||||
icon: Icon(Icons.group),
|
icon: Icon(Icons.group),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
final chatModel = FFI.chatModel;
|
// only mobile need [appBarActions], just bind gFFI.chatModel
|
||||||
|
final chatModel = gFFI.chatModel;
|
||||||
return chatModel.messages.entries.map((entry) {
|
return chatModel.messages.entries.map((entry) {
|
||||||
final id = entry.key;
|
final id = entry.key;
|
||||||
final user = entry.value.chatUser;
|
final user = entry.value.chatUser;
|
||||||
@@ -29,40 +36,43 @@ class ChatPage extends StatelessWidget implements PageShape {
|
|||||||
}).toList();
|
}).toList();
|
||||||
},
|
},
|
||||||
onSelected: (id) {
|
onSelected: (id) {
|
||||||
FFI.chatModel.changeCurrentID(id);
|
gFFI.chatModel.changeCurrentID(id);
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider.value(
|
return ChangeNotifierProvider.value(
|
||||||
value: FFI.chatModel,
|
value: chatModel,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: MyTheme.grayBg,
|
color: MyTheme.grayBg,
|
||||||
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
||||||
final currentUser = chatModel.currentUser;
|
final currentUser = chatModel.currentUser;
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
DashChat(
|
LayoutBuilder(builder: (context, constraints) {
|
||||||
onSend: (chatMsg) {
|
return DashChat(
|
||||||
chatModel.send(chatMsg);
|
onSend: (chatMsg) {
|
||||||
},
|
chatModel.send(chatMsg);
|
||||||
currentUser: chatModel.me,
|
},
|
||||||
messages:
|
currentUser: chatModel.me,
|
||||||
chatModel.messages[chatModel.currentID]?.chatMessages ??
|
messages: chatModel
|
||||||
[],
|
.messages[chatModel.currentID]?.chatMessages ??
|
||||||
messageOptions: MessageOptions(
|
[],
|
||||||
showOtherUsersAvatar: false,
|
messageOptions: MessageOptions(
|
||||||
showTime: true,
|
showOtherUsersAvatar: false,
|
||||||
messageDecorationBuilder: (_, __, ___) =>
|
showTime: true,
|
||||||
defaultMessageDecoration(
|
maxWidth: constraints.maxWidth * 0.7,
|
||||||
color: MyTheme.accent80,
|
messageDecorationBuilder: (_, __, ___) =>
|
||||||
borderTopLeft: 8,
|
defaultMessageDecoration(
|
||||||
borderTopRight: 8,
|
color: MyTheme.accent80,
|
||||||
borderBottomRight: 8,
|
borderTopLeft: 8,
|
||||||
borderBottomLeft: 8,
|
borderTopRight: 8,
|
||||||
)),
|
borderBottomRight: 8,
|
||||||
),
|
borderBottomLeft: 8,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
chatModel.currentID == ChatModel.clientModeID
|
chatModel.currentID == ChatModel.clientModeID
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: Padding(
|
: Padding(
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/pages/file_manager_page.dart';
|
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'dart:async';
|
|
||||||
import '../common.dart';
|
import '../../common.dart';
|
||||||
import '../models/model.dart';
|
import '../../models/model.dart';
|
||||||
|
import '../../models/peer_model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import 'remote_page.dart';
|
import 'remote_page.dart';
|
||||||
import 'settings_page.dart';
|
|
||||||
import 'scan_page.dart';
|
import 'scan_page.dart';
|
||||||
|
import 'settings_page.dart';
|
||||||
|
|
||||||
|
/// Connection page for connecting to a remote peer.
|
||||||
class ConnectionPage extends StatefulWidget implements PageShape {
|
class ConnectionPage extends StatefulWidget implements PageShape {
|
||||||
ConnectionPage({Key? key}) : super(key: key);
|
ConnectionPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -26,17 +31,32 @@ class ConnectionPage extends StatefulWidget implements PageShape {
|
|||||||
_ConnectionPageState createState() => _ConnectionPageState();
|
_ConnectionPageState createState() => _ConnectionPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// State for the connection page.
|
||||||
class _ConnectionPageState extends State<ConnectionPage> {
|
class _ConnectionPageState extends State<ConnectionPage> {
|
||||||
|
/// Controller for the id input bar.
|
||||||
final _idController = TextEditingController();
|
final _idController = TextEditingController();
|
||||||
|
|
||||||
|
/// Update url. If it's not null, means an update is available.
|
||||||
var _updateUrl = '';
|
var _updateUrl = '';
|
||||||
var _menuPos;
|
var _menuPos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (_idController.text.isEmpty) {
|
||||||
|
() async {
|
||||||
|
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||||
|
if (lastRemoteId != _idController.text) {
|
||||||
|
setState(() {
|
||||||
|
_idController.text = lastRemoteId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
Timer(Duration(seconds: 5), () {
|
Timer(Duration(seconds: 5), () async {
|
||||||
_updateUrl = FFI.getByName('software_update_url');
|
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
|
;
|
||||||
if (_updateUrl.isNotEmpty) setState(() {});
|
if (_updateUrl.isNotEmpty) setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -45,7 +65,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Provider.of<FfiModel>(context);
|
Provider.of<FfiModel>(context);
|
||||||
if (_idController.text.isEmpty) _idController.text = FFI.getId();
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@@ -60,11 +79,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Callback for the connect button.
|
||||||
|
/// Connects to the selected peer.
|
||||||
void onConnect() {
|
void onConnect() {
|
||||||
var id = _idController.text.trim();
|
var id = _idController.text.trim();
|
||||||
connect(id);
|
connect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connect to a peer with [id].
|
||||||
|
/// If [isFileTransfer], starts a session only for file transfer.
|
||||||
void connect(String id, {bool isFileTransfer = false}) async {
|
void connect(String id, {bool isFileTransfer = false}) async {
|
||||||
if (id == '') return;
|
if (id == '') return;
|
||||||
id = id.replaceAll(' ', '');
|
id = id.replaceAll(' ', '');
|
||||||
@@ -94,6 +117,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// UI for software update.
|
||||||
|
/// If [_updateUrl] is not empty, shows a button to update the software.
|
||||||
Widget getUpdateUI() {
|
Widget getUpdateUI() {
|
||||||
return _updateUrl.isEmpty
|
return _updateUrl.isEmpty
|
||||||
? SizedBox(height: 0)
|
? SizedBox(height: 0)
|
||||||
@@ -114,6 +139,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
color: Colors.white, fontWeight: FontWeight.bold))));
|
color: Colors.white, fontWeight: FontWeight.bold))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// UI for the search bar.
|
||||||
|
/// Search for a peer and connect to it if the id exists.
|
||||||
Widget getSearchBarUI() {
|
Widget getSearchBarUI() {
|
||||||
var w = Padding(
|
var w = Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0),
|
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0),
|
||||||
@@ -187,6 +214,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the image for the current [platform].
|
||||||
Widget getPlatformImage(String platform) {
|
Widget getPlatformImage(String platform) {
|
||||||
platform = platform.toLowerCase();
|
platform = platform.toLowerCase();
|
||||||
if (platform == 'mac os')
|
if (platform == 'mac os')
|
||||||
@@ -195,55 +223,66 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
return Image.asset('assets/$platform.png', width: 24, height: 24);
|
return Image.asset('assets/$platform.png', width: 24, height: 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all the saved peers.
|
||||||
Widget getPeers() {
|
Widget getPeers() {
|
||||||
final size = MediaQuery.of(context).size;
|
final windowWidth = MediaQuery.of(context).size.width;
|
||||||
final space = 8.0;
|
final space = 8.0;
|
||||||
var width = size.width - 2 * space;
|
var width = windowWidth - 2 * space;
|
||||||
final minWidth = 320.0;
|
final minWidth = 320.0;
|
||||||
if (size.width > minWidth + 2 * space) {
|
if (windowWidth > minWidth + 2 * space) {
|
||||||
final n = (size.width / (minWidth + 2 * space)).floor();
|
final n = (windowWidth / (minWidth + 2 * space)).floor();
|
||||||
width = size.width / n - 2 * space;
|
width = windowWidth / n - 2 * space;
|
||||||
}
|
}
|
||||||
final cards = <Widget>[];
|
return FutureBuilder<List<Peer>>(
|
||||||
var peers = FFI.peers();
|
future: gFFI.peers(),
|
||||||
peers.forEach((p) {
|
builder: (context, snapshot) {
|
||||||
cards.add(Container(
|
final cards = <Widget>[];
|
||||||
width: width,
|
if (snapshot.hasData) {
|
||||||
child: Card(
|
final peers = snapshot.data!;
|
||||||
child: GestureDetector(
|
peers.forEach((p) {
|
||||||
onTap: !isDesktop ? () => connect('${p.id}') : null,
|
cards.add(Container(
|
||||||
onDoubleTap: isDesktop ? () => connect('${p.id}') : null,
|
width: width,
|
||||||
onLongPressStart: (details) {
|
child: Card(
|
||||||
final x = details.globalPosition.dx;
|
child: GestureDetector(
|
||||||
final y = details.globalPosition.dy;
|
onTap:
|
||||||
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
!isWebDesktop ? () => connect('${p.id}') : null,
|
||||||
showPeerMenu(context, p.id);
|
onDoubleTap:
|
||||||
},
|
isWebDesktop ? () => connect('${p.id}') : null,
|
||||||
child: ListTile(
|
onLongPressStart: (details) {
|
||||||
contentPadding: const EdgeInsets.only(left: 12),
|
final x = details.globalPosition.dx;
|
||||||
subtitle: Text('${p.username}@${p.hostname}'),
|
final y = details.globalPosition.dy;
|
||||||
title: Text('${p.id}'),
|
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
leading: Container(
|
showPeerMenu(context, p.id);
|
||||||
padding: const EdgeInsets.all(6),
|
},
|
||||||
child: getPlatformImage('${p.platform}'),
|
child: ListTile(
|
||||||
color: str2color('${p.id}${p.platform}', 0x7f)),
|
contentPadding: const EdgeInsets.only(left: 12),
|
||||||
trailing: InkWell(
|
subtitle: Text('${p.username}@${p.hostname}'),
|
||||||
child: Padding(
|
title: Text('${p.id}'),
|
||||||
padding: const EdgeInsets.all(12),
|
leading: Container(
|
||||||
child: Icon(Icons.more_vert)),
|
padding: const EdgeInsets.all(6),
|
||||||
onTapDown: (e) {
|
child: getPlatformImage('${p.platform}'),
|
||||||
final x = e.globalPosition.dx;
|
color: str2color('${p.id}${p.platform}', 0x7f)),
|
||||||
final y = e.globalPosition.dy;
|
trailing: InkWell(
|
||||||
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
child: Padding(
|
||||||
},
|
padding: const EdgeInsets.all(12),
|
||||||
onTap: () {
|
child: Icon(Icons.more_vert)),
|
||||||
showPeerMenu(context, p.id);
|
onTapDown: (e) {
|
||||||
}),
|
final x = e.globalPosition.dx;
|
||||||
)))));
|
final y = e.globalPosition.dy;
|
||||||
});
|
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
return Wrap(children: cards, spacing: space, runSpacing: space);
|
},
|
||||||
|
onTap: () {
|
||||||
|
showPeerMenu(context, p.id);
|
||||||
|
}),
|
||||||
|
)))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Wrap(children: cards, spacing: space, runSpacing: space);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show the peer menu and handle user's choice.
|
||||||
|
/// User might remove the peer or send a file to the peer.
|
||||||
void showPeerMenu(BuildContext context, String id) async {
|
void showPeerMenu(BuildContext context, String id) async {
|
||||||
var value = await showMenu(
|
var value = await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -261,7 +300,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
if (value == 'remove') {
|
if (value == 'remove') {
|
||||||
setState(() => FFI.setByName('remove', '$id'));
|
setState(() => bind.mainRemovePeer(id: id));
|
||||||
() async {
|
() async {
|
||||||
removePreference(id);
|
removePreference(id);
|
||||||
}();
|
}();
|
||||||
@@ -277,10 +316,34 @@ class WebMenu extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _WebMenuState extends State<WebMenu> {
|
class _WebMenuState extends State<WebMenu> {
|
||||||
|
String? username;
|
||||||
|
String url = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
() async {
|
||||||
|
final usernameRes = await getUsername();
|
||||||
|
final urlRes = await getUrl();
|
||||||
|
var update = false;
|
||||||
|
if (usernameRes != username) {
|
||||||
|
username = usernameRes;
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
if (urlRes != url) {
|
||||||
|
url = urlRes;
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Provider.of<FfiModel>(context);
|
Provider.of<FfiModel>(context);
|
||||||
final username = getUsername();
|
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: Icon(Icons.more_vert),
|
icon: Icon(Icons.more_vert),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
@@ -298,7 +361,7 @@ class _WebMenuState extends State<WebMenu> {
|
|||||||
value: "server",
|
value: "server",
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
(getUrl().contains('admin.rustdesk.com')
|
(url.contains('admin.rustdesk.com')
|
||||||
? <PopupMenuItem<String>>[]
|
? <PopupMenuItem<String>>[]
|
||||||
: [
|
: [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -317,16 +380,16 @@ class _WebMenuState extends State<WebMenu> {
|
|||||||
},
|
},
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'server') {
|
if (value == 'server') {
|
||||||
showServerSettings();
|
showServerSettings(gFFI.dialogManager);
|
||||||
}
|
}
|
||||||
if (value == 'about') {
|
if (value == 'about') {
|
||||||
showAbout();
|
showAbout(gFFI.dialogManager);
|
||||||
}
|
}
|
||||||
if (value == 'login') {
|
if (value == 'login') {
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
showLogin();
|
showLogin(gFFI.dialogManager);
|
||||||
} else {
|
} else {
|
||||||
logout();
|
logout(gFFI.dialogManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (value == 'scan') {
|
if (value == 'scan') {
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hbb/models/file_model.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
|
||||||
import 'package:wakelock/wakelock.dart';
|
|
||||||
import 'package:toggle_switch/toggle_switch.dart';
|
|
||||||
|
|
||||||
import '../common.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/model.dart';
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||||
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
import '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
|
|
||||||
class FileManagerPage extends StatefulWidget {
|
class FileManagerPage extends StatefulWidget {
|
||||||
@@ -20,31 +19,34 @@ class FileManagerPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FileManagerPageState extends State<FileManagerPage> {
|
class _FileManagerPageState extends State<FileManagerPage> {
|
||||||
final model = FFI.fileModel;
|
final model = gFFI.fileModel;
|
||||||
final _selectedItems = SelectedItems();
|
final _selectedItems = SelectedItems();
|
||||||
final _breadCrumbScroller = ScrollController();
|
final _breadCrumbScroller = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
FFI.connect(widget.id, isFileTransfer: true);
|
gFFI.connect(widget.id, isFileTransfer: true);
|
||||||
showLoading(translate('Connecting...'));
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
FFI.ffiModel.updateEventListener(widget.id);
|
gFFI.dialogManager
|
||||||
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
|
});
|
||||||
|
gFFI.ffiModel.updateEventListener(widget.id);
|
||||||
Wakelock.enable();
|
Wakelock.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
model.onClose();
|
model.onClose();
|
||||||
FFI.close();
|
gFFI.close();
|
||||||
SmartDialog.dismiss();
|
gFFI.dialogManager.dismissAll();
|
||||||
Wakelock.disable();
|
Wakelock.disable();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
||||||
value: FFI.fileModel,
|
value: gFFI.fileModel,
|
||||||
child: Consumer<FileModel>(builder: (_context, _model, _child) {
|
child: Consumer<FileModel>(builder: (_context, _model, _child) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
@@ -59,7 +61,9 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
backgroundColor: MyTheme.grayBg,
|
backgroundColor: MyTheme.grayBg,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: Row(children: [
|
leading: Row(children: [
|
||||||
IconButton(icon: Icon(Icons.close), onPressed: clientClose),
|
IconButton(
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
onPressed: () => clientClose(gFFI.dialogManager)),
|
||||||
]),
|
]),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: ToggleSwitch(
|
title: ToggleSwitch(
|
||||||
@@ -140,8 +144,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
model.toggleSelectMode();
|
model.toggleSelectMode();
|
||||||
} else if (v == "folder") {
|
} else if (v == "folder") {
|
||||||
final name = TextEditingController();
|
final name = TextEditingController();
|
||||||
DialogManager.show(
|
gFFI.dialogManager
|
||||||
(setState, close) => CustomAlertDialog(
|
.show((setState, close) => CustomAlertDialog(
|
||||||
title: Text(translate("Create Folder")),
|
title: Text(translate("Create Folder")),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/pages/chat_page.dart';
|
import 'package:flutter_hbb/mobile/pages/chat_page.dart';
|
||||||
import 'package:flutter_hbb/pages/server_page.dart';
|
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
||||||
import 'package:flutter_hbb/pages/settings_page.dart';
|
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||||
import '../common.dart';
|
import '../../common.dart';
|
||||||
import '../widgets/overlay.dart';
|
|
||||||
import 'connection_page.dart';
|
import 'connection_page.dart';
|
||||||
|
|
||||||
abstract class PageShape extends Widget {
|
abstract class PageShape extends Widget {
|
||||||
@@ -12,10 +11,10 @@ abstract class PageShape extends Widget {
|
|||||||
final List<Widget> appBarActions = [];
|
final List<Widget> appBarActions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final homeKey = GlobalKey<_HomePageState>();
|
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
HomePage({Key? key}) : super(key: key);
|
static final homeKey = GlobalKey<_HomePageState>();
|
||||||
|
|
||||||
|
HomePage() : super(key: homeKey);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_HomePageState createState() => _HomePageState();
|
_HomePageState createState() => _HomePageState();
|
||||||
@@ -79,8 +78,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
onTap: (index) => setState(() {
|
onTap: (index) => setState(() {
|
||||||
// close chat overlay when go chat page
|
// close chat overlay when go chat page
|
||||||
if (index == 1 && _selectedIndex != index) {
|
if (index == 1 && _selectedIndex != index) {
|
||||||
hideChatIconOverlay();
|
gFFI.chatModel.hideChatIconOverlay();
|
||||||
hideChatWindowOverlay();
|
gFFI.chatModel.hideChatWindowOverlay();
|
||||||
}
|
}
|
||||||
_selectedIndex = index;
|
_selectedIndex = index;
|
||||||
}),
|
}),
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
|
||||||
import 'package:flutter_hbb/widgets/gesture_help.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'dart:ui' as ui;
|
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||||
import 'dart:async';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
import '../common.dart';
|
|
||||||
import '../widgets/gestures.dart';
|
import '../../common.dart';
|
||||||
import '../models/model.dart';
|
import '../../models/model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
import '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
|
import '../widgets/gestures.dart';
|
||||||
import '../widgets/overlay.dart';
|
import '../widgets/overlay.dart';
|
||||||
|
|
||||||
final initText = '\1' * 1024;
|
final initText = '\1' * 1024;
|
||||||
@@ -28,7 +30,7 @@ class RemotePage extends StatefulWidget {
|
|||||||
class _RemotePageState extends State<RemotePage> {
|
class _RemotePageState extends State<RemotePage> {
|
||||||
Timer? _interval;
|
Timer? _interval;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _showBar = !isDesktop;
|
bool _showBar = !isWebDesktop;
|
||||||
double _bottom = 0;
|
double _bottom = 0;
|
||||||
String _value = '';
|
String _value = '';
|
||||||
double _scale = 1;
|
double _scale = 1;
|
||||||
@@ -45,30 +47,32 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
FFI.connect(widget.id);
|
gFFI.connect(widget.id);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||||
showLoading(translate('Connecting...'));
|
gFFI.dialogManager
|
||||||
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
_interval =
|
_interval =
|
||||||
Timer.periodic(Duration(milliseconds: 30), (timer) => interval());
|
Timer.periodic(Duration(milliseconds: 30), (timer) => interval());
|
||||||
});
|
});
|
||||||
Wakelock.enable();
|
Wakelock.enable();
|
||||||
_physicalFocusNode.requestFocus();
|
_physicalFocusNode.requestFocus();
|
||||||
FFI.ffiModel.updateEventListener(widget.id);
|
gFFI.ffiModel.updateEventListener(widget.id);
|
||||||
FFI.listenToMouse(true);
|
gFFI.listenToMouse(true);
|
||||||
|
gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
hideMobileActionsOverlay();
|
hideMobileActionsOverlay();
|
||||||
FFI.listenToMouse(false);
|
gFFI.listenToMouse(false);
|
||||||
FFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
_mobileFocusNode.dispose();
|
_mobileFocusNode.dispose();
|
||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
FFI.close();
|
gFFI.close();
|
||||||
_interval?.cancel();
|
_interval?.cancel();
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
SmartDialog.dismiss();
|
gFFI.dialogManager.dismissAll();
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
Wakelock.disable();
|
Wakelock.disable();
|
||||||
@@ -76,7 +80,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetTool() {
|
void resetTool() {
|
||||||
FFI.resetModifiers();
|
gFFI.resetModifiers();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isKeyboardShown() {
|
bool isKeyboardShown() {
|
||||||
@@ -93,10 +97,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
if (v < 100) {
|
if (v < 100) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
overlays: []);
|
overlays: []);
|
||||||
// [pi.version.isNotEmpty] -> check ready or not,avoid login without soft-keyboard
|
// [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
|
||||||
if (chatWindowOverlayEntry == null &&
|
if (gFFI.chatModel.chatWindowOverlayEntry == null &&
|
||||||
FFI.ffiModel.pi.version.isNotEmpty) {
|
gFFI.ffiModel.pi.version.isNotEmpty) {
|
||||||
FFI.invokeMethod("enable_soft_keyboard", false);
|
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,12 +132,12 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
newValue[common] == oldValue[common];
|
newValue[common] == oldValue[common];
|
||||||
++common) {}
|
++common) {}
|
||||||
for (i = 0; i < oldValue.length - common; ++i) {
|
for (i = 0; i < oldValue.length - common; ++i) {
|
||||||
FFI.inputKey('VK_BACK');
|
gFFI.inputKey('VK_BACK');
|
||||||
}
|
}
|
||||||
if (newValue.length > common) {
|
if (newValue.length > common) {
|
||||||
var s = newValue.substring(common);
|
var s = newValue.substring(common);
|
||||||
if (s.length > 1) {
|
if (s.length > 1) {
|
||||||
FFI.setByName('input_string', s);
|
bind.sessionInputString(id: widget.id, value: s);
|
||||||
} else {
|
} else {
|
||||||
inputChar(s);
|
inputChar(s);
|
||||||
}
|
}
|
||||||
@@ -151,7 +155,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
// ?
|
// ?
|
||||||
} else if (newValue.length < oldValue.length) {
|
} else if (newValue.length < oldValue.length) {
|
||||||
final char = 'VK_BACK';
|
final char = 'VK_BACK';
|
||||||
FFI.inputKey(char);
|
gFFI.inputKey(char);
|
||||||
} else {
|
} else {
|
||||||
final content = newValue.substring(oldValue.length);
|
final content = newValue.substring(oldValue.length);
|
||||||
if (content.length > 1) {
|
if (content.length > 1) {
|
||||||
@@ -167,11 +171,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
content == '()' ||
|
content == '()' ||
|
||||||
content == '【】')) {
|
content == '【】')) {
|
||||||
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
|
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
|
||||||
FFI.setByName('input_string', content);
|
bind.sessionInputString(id: widget.id, value: content);
|
||||||
openKeyboard();
|
openKeyboard();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
FFI.setByName('input_string', content);
|
bind.sessionInputString(id: widget.id, value: content);
|
||||||
} else {
|
} else {
|
||||||
inputChar(content);
|
inputChar(content);
|
||||||
}
|
}
|
||||||
@@ -184,11 +188,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
} else if (char == ' ') {
|
} else if (char == ' ') {
|
||||||
char = 'VK_SPACE';
|
char = 'VK_SPACE';
|
||||||
}
|
}
|
||||||
FFI.inputKey(char);
|
gFFI.inputKey(char);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openKeyboard() {
|
void openKeyboard() {
|
||||||
FFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
// destroy first, so that our _value trick can work
|
// destroy first, so that our _value trick can work
|
||||||
_value = initText;
|
_value = initText;
|
||||||
setState(() => _showEdit = false);
|
setState(() => _showEdit = false);
|
||||||
@@ -211,7 +215,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
final label = _logicalKeyMap[e.logicalKey.keyId] ??
|
final label = _logicalKeyMap[e.logicalKey.keyId] ??
|
||||||
_physicalKeyMap[e.physicalKey.usbHidUsage] ??
|
_physicalKeyMap[e.physicalKey.usbHidUsage] ??
|
||||||
e.logicalKey.keyLabel;
|
e.logicalKey.keyLabel;
|
||||||
FFI.inputKey(label, down: down, press: press ?? false);
|
gFFI.inputKey(label, down: down, press: press ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -219,11 +223,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
final pi = Provider.of<FfiModel>(context).pi;
|
final pi = Provider.of<FfiModel>(context).pi;
|
||||||
final hideKeyboard = isKeyboardShown() && _showEdit;
|
final hideKeyboard = isKeyboardShown() && _showEdit;
|
||||||
final showActionButton = !_showBar || hideKeyboard;
|
final showActionButton = !_showBar || hideKeyboard;
|
||||||
final keyboard = FFI.ffiModel.permissions['keyboard'] != false;
|
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
clientClose();
|
clientClose(gFFI.dialogManager);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: getRawPointerAndKeyBody(
|
child: getRawPointerAndKeyBody(
|
||||||
@@ -241,7 +245,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
if (hideKeyboard) {
|
if (hideKeyboard) {
|
||||||
_showEdit = false;
|
_showEdit = false;
|
||||||
FFI.invokeMethod("enable_soft_keyboard", false);
|
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||||
_mobileFocusNode.unfocus();
|
_mobileFocusNode.unfocus();
|
||||||
_physicalFocusNode.requestFocus();
|
_physicalFocusNode.requestFocus();
|
||||||
} else {
|
} else {
|
||||||
@@ -257,7 +261,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: isDesktop
|
child: isWebDesktop
|
||||||
? getBodyForDesktopWithListener(keyboard)
|
? getBodyForDesktopWithListener(keyboard)
|
||||||
: SafeArea(child:
|
: SafeArea(child:
|
||||||
OrientationBuilder(builder: (ctx, orientation) {
|
OrientationBuilder(builder: (ctx, orientation) {
|
||||||
@@ -265,7 +269,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
Timer(Duration(milliseconds: 200), () {
|
Timer(Duration(milliseconds: 200), () {
|
||||||
resetMobileActionsOverlay();
|
resetMobileActionsOverlay();
|
||||||
_currentOrientation = orientation;
|
_currentOrientation = orientation;
|
||||||
FFI.canvasModel.updateViewStyle();
|
gFFI.canvasModel.updateViewStyle();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
@@ -290,7 +294,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
FFI.handleMouse(getEvent(e, 'mousemove'));
|
gFFI.handleMouse(getEvent(e, 'mousemove'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerDown: (e) {
|
onPointerDown: (e) {
|
||||||
@@ -302,19 +306,19 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
FFI.handleMouse(getEvent(e, 'mousedown'));
|
gFFI.handleMouse(getEvent(e, 'mousedown'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerUp: (e) {
|
onPointerUp: (e) {
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
FFI.handleMouse(getEvent(e, 'mouseup'));
|
gFFI.handleMouse(getEvent(e, 'mouseup'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerMove: (e) {
|
onPointerMove: (e) {
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
FFI.handleMouse(getEvent(e, 'mousemove'));
|
gFFI.handleMouse(getEvent(e, 'mousemove'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerSignal: (e) {
|
onPointerSignal: (e) {
|
||||||
@@ -327,8 +331,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
if (dy > 0)
|
if (dy > 0)
|
||||||
dy = -1;
|
dy = -1;
|
||||||
else if (dy < 0) dy = 1;
|
else if (dy < 0) dy = 1;
|
||||||
FFI.setByName(
|
bind.sessionSendMouse(
|
||||||
'send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
id: widget.id,
|
||||||
|
msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
@@ -350,14 +355,14 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
sendRawKey(e, press: true);
|
sendRawKey(e, press: true);
|
||||||
} else {
|
} else {
|
||||||
sendRawKey(e, down: true);
|
sendRawKey(e, down: true);
|
||||||
if (e.isAltPressed && !FFI.alt) {
|
if (e.isAltPressed && !gFFI.alt) {
|
||||||
FFI.alt = true;
|
gFFI.alt = true;
|
||||||
} else if (e.isControlPressed && !FFI.ctrl) {
|
} else if (e.isControlPressed && !gFFI.ctrl) {
|
||||||
FFI.ctrl = true;
|
gFFI.ctrl = true;
|
||||||
} else if (e.isShiftPressed && !FFI.shift) {
|
} else if (e.isShiftPressed && !gFFI.shift) {
|
||||||
FFI.shift = true;
|
gFFI.shift = true;
|
||||||
} else if (e.isMetaPressed && !FFI.command) {
|
} else if (e.isMetaPressed && !gFFI.command) {
|
||||||
FFI.command = true;
|
gFFI.command = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,16 +370,16 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
if (!_showEdit && e is RawKeyUpEvent) {
|
if (!_showEdit && e is RawKeyUpEvent) {
|
||||||
if (key == LogicalKeyboardKey.altLeft ||
|
if (key == LogicalKeyboardKey.altLeft ||
|
||||||
key == LogicalKeyboardKey.altRight) {
|
key == LogicalKeyboardKey.altRight) {
|
||||||
FFI.alt = false;
|
gFFI.alt = false;
|
||||||
} else if (key == LogicalKeyboardKey.controlLeft ||
|
} else if (key == LogicalKeyboardKey.controlLeft ||
|
||||||
key == LogicalKeyboardKey.controlRight) {
|
key == LogicalKeyboardKey.controlRight) {
|
||||||
FFI.ctrl = false;
|
gFFI.ctrl = false;
|
||||||
} else if (key == LogicalKeyboardKey.shiftRight ||
|
} else if (key == LogicalKeyboardKey.shiftRight ||
|
||||||
key == LogicalKeyboardKey.shiftLeft) {
|
key == LogicalKeyboardKey.shiftLeft) {
|
||||||
FFI.shift = false;
|
gFFI.shift = false;
|
||||||
} else if (key == LogicalKeyboardKey.metaLeft ||
|
} else if (key == LogicalKeyboardKey.metaLeft ||
|
||||||
key == LogicalKeyboardKey.metaRight) {
|
key == LogicalKeyboardKey.metaRight) {
|
||||||
FFI.command = false;
|
gFFI.command = false;
|
||||||
}
|
}
|
||||||
sendRawKey(e);
|
sendRawKey(e);
|
||||||
}
|
}
|
||||||
@@ -397,7 +402,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.clear),
|
icon: Icon(Icons.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
clientClose();
|
clientClose(gFFI.dialogManager);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
@@ -407,13 +412,13 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
icon: Icon(Icons.tv),
|
icon: Icon(Icons.tv),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _showEdit = false);
|
setState(() => _showEdit = false);
|
||||||
showOptions();
|
showOptions(widget.id, gFFI.dialogManager);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
(isDesktop
|
(isWebDesktop
|
||||||
? []
|
? []
|
||||||
: FFI.ffiModel.isPeerAndroid
|
: gFFI.ffiModel.isPeerAndroid
|
||||||
? [
|
? [
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -434,7 +439,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
onPressed: openKeyboard),
|
onPressed: openKeyboard),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(FFI.ffiModel.touchMode
|
icon: Icon(gFFI.ffiModel.touchMode
|
||||||
? Icons.touch_app
|
? Icons.touch_app
|
||||||
: Icons.mouse),
|
: Icons.mouse),
|
||||||
onPressed: changeTouchMode,
|
onPressed: changeTouchMode,
|
||||||
@@ -447,9 +452,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.message),
|
icon: Icon(Icons.message),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentID(ChatModel.clientModeID);
|
.changeCurrentID(ChatModel.clientModeID);
|
||||||
toggleChatOverlay();
|
gFFI.chatModel.toggleChatOverlay();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]) +
|
]) +
|
||||||
@@ -459,7 +464,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
icon: Icon(Icons.more_vert),
|
icon: Icon(Icons.more_vert),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _showEdit = false);
|
setState(() => _showEdit = false);
|
||||||
showActions();
|
showActions(widget.id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -486,101 +491,102 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
|
|
||||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||||
Widget getBodyForMobileWithGesture() {
|
Widget getBodyForMobileWithGesture() {
|
||||||
final touchMode = FFI.ffiModel.touchMode;
|
final touchMode = gFFI.ffiModel.touchMode;
|
||||||
return getMixinGestureDetector(
|
return getMixinGestureDetector(
|
||||||
child: getBodyForMobile(),
|
child: getBodyForMobile(),
|
||||||
onTapUp: (d) {
|
onTapUp: (d) {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
FFI.cursorModel.touch(
|
gFFI.cursorModel.touch(
|
||||||
d.localPosition.dx, d.localPosition.dy, MouseButtons.left);
|
d.localPosition.dx, d.localPosition.dy, MouseButtons.left);
|
||||||
} else {
|
} else {
|
||||||
FFI.tap(MouseButtons.left);
|
gFFI.tap(MouseButtons.left);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDoubleTapDown: (d) {
|
onDoubleTapDown: (d) {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDoubleTap: () {
|
onDoubleTap: () {
|
||||||
FFI.tap(MouseButtons.left);
|
gFFI.tap(MouseButtons.left);
|
||||||
FFI.tap(MouseButtons.left);
|
gFFI.tap(MouseButtons.left);
|
||||||
},
|
},
|
||||||
onLongPressDown: (d) {
|
onLongPressDown: (d) {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
|
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
_cacheLongPressPosition = d.localPosition;
|
_cacheLongPressPosition = d.localPosition;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
FFI.cursorModel
|
gFFI.cursorModel
|
||||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||||
}
|
}
|
||||||
FFI.tap(MouseButtons.right);
|
gFFI.tap(MouseButtons.right);
|
||||||
},
|
},
|
||||||
onDoubleFinerTap: (d) {
|
onDoubleFinerTap: (d) {
|
||||||
if (!touchMode) {
|
if (!touchMode) {
|
||||||
FFI.tap(MouseButtons.right);
|
gFFI.tap(MouseButtons.right);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHoldDragStart: (d) {
|
onHoldDragStart: (d) {
|
||||||
if (!touchMode) {
|
if (!touchMode) {
|
||||||
FFI.sendMouse('down', MouseButtons.left);
|
gFFI.sendMouse('down', MouseButtons.left);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHoldDragUpdate: (d) {
|
onHoldDragUpdate: (d) {
|
||||||
if (!touchMode) {
|
if (!touchMode) {
|
||||||
FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode);
|
gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHoldDragEnd: (_) {
|
onHoldDragEnd: (_) {
|
||||||
if (!touchMode) {
|
if (!touchMode) {
|
||||||
FFI.sendMouse('up', MouseButtons.left);
|
gFFI.sendMouse('up', MouseButtons.left);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOneFingerPanStart: (d) {
|
onOneFingerPanStart: (d) {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
FFI.sendMouse('down', MouseButtons.left);
|
gFFI.sendMouse('down', MouseButtons.left);
|
||||||
} else {
|
} else {
|
||||||
final cursorX = FFI.cursorModel.x;
|
final cursorX = gFFI.cursorModel.x;
|
||||||
final cursorY = FFI.cursorModel.y;
|
final cursorY = gFFI.cursorModel.y;
|
||||||
final visible =
|
final visible =
|
||||||
FFI.cursorModel.getVisibleRect().inflate(1); // extend edges
|
gFFI.cursorModel.getVisibleRect().inflate(1); // extend edges
|
||||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
if (!visible.contains(Offset(cursorX, cursorY))) {
|
if (!visible.contains(Offset(cursorX, cursorY))) {
|
||||||
FFI.cursorModel.move(size.width / 2, size.height / 2);
|
gFFI.cursorModel.move(size.width / 2, size.height / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOneFingerPanUpdate: (d) {
|
onOneFingerPanUpdate: (d) {
|
||||||
FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode);
|
gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode);
|
||||||
},
|
},
|
||||||
onOneFingerPanEnd: (d) {
|
onOneFingerPanEnd: (d) {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
FFI.sendMouse('up', MouseButtons.left);
|
gFFI.sendMouse('up', MouseButtons.left);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// scale + pan event
|
// scale + pan event
|
||||||
onTwoFingerScaleUpdate: (d) {
|
onTwoFingerScaleUpdate: (d) {
|
||||||
FFI.canvasModel.updateScale(d.scale / _scale);
|
gFFI.canvasModel.updateScale(d.scale / _scale);
|
||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
FFI.canvasModel.panX(d.focalPointDelta.dx);
|
gFFI.canvasModel.panX(d.focalPointDelta.dx);
|
||||||
FFI.canvasModel.panY(d.focalPointDelta.dy);
|
gFFI.canvasModel.panY(d.focalPointDelta.dy);
|
||||||
},
|
},
|
||||||
onTwoFingerScaleEnd: (d) {
|
onTwoFingerScaleEnd: (d) {
|
||||||
_scale = 1;
|
_scale = 1;
|
||||||
FFI.setByName('peer_option', '{"name": "view-style", "value": ""}');
|
bind.sessionPeerOption(id: widget.id, name: "view-style", value: "");
|
||||||
},
|
},
|
||||||
onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid
|
onThreeFingerVerticalDragUpdate: gFFI.ffiModel.isPeerAndroid
|
||||||
? null
|
? null
|
||||||
: (d) {
|
: (d) {
|
||||||
_mouseScrollIntegral += d.delta.dy / 4;
|
_mouseScrollIntegral += d.delta.dy / 4;
|
||||||
if (_mouseScrollIntegral > 1) {
|
if (_mouseScrollIntegral > 1) {
|
||||||
FFI.scroll(1);
|
gFFI.scroll(1);
|
||||||
_mouseScrollIntegral = 0;
|
_mouseScrollIntegral = 0;
|
||||||
} else if (_mouseScrollIntegral < -1) {
|
} else if (_mouseScrollIntegral < -1) {
|
||||||
FFI.scroll(-1);
|
gFFI.scroll(-1);
|
||||||
_mouseScrollIntegral = 0;
|
_mouseScrollIntegral = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -617,8 +623,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
|
|
||||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
Widget getBodyForDesktopWithListener(bool keyboard) {
|
||||||
var paints = <Widget>[ImagePaint()];
|
var paints = <Widget>[ImagePaint()];
|
||||||
if (keyboard ||
|
final cursor = bind.sessionGetToggleOptionSync(
|
||||||
FFI.getByName('toggle_option', 'show-remote-cursor') == 'true') {
|
id: widget.id, arg: 'show-remote-cursor');
|
||||||
|
if (keyboard || cursor) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint());
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
@@ -626,15 +633,16 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int lastMouseDownButtons = 0;
|
int lastMouseDownButtons = 0;
|
||||||
|
|
||||||
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['type'] = type;
|
||||||
out['x'] = evt.position.dx;
|
out['x'] = evt.position.dx;
|
||||||
out['y'] = evt.position.dy;
|
out['y'] = evt.position.dy;
|
||||||
if (FFI.alt) out['alt'] = 'true';
|
if (gFFI.alt) out['alt'] = 'true';
|
||||||
if (FFI.shift) out['shift'] = 'true';
|
if (gFFI.shift) out['shift'] = 'true';
|
||||||
if (FFI.ctrl) out['ctrl'] = 'true';
|
if (gFFI.ctrl) out['ctrl'] = 'true';
|
||||||
if (FFI.command) out['command'] = 'true';
|
if (gFFI.command) out['command'] = 'true';
|
||||||
out['buttons'] = evt
|
out['buttons'] = evt
|
||||||
.buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right)
|
.buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right)
|
||||||
if (evt.buttons != 0) {
|
if (evt.buttons != 0) {
|
||||||
@@ -645,13 +653,13 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showActions() {
|
void showActions(String id) async {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final x = 120.0;
|
final x = 120.0;
|
||||||
final y = size.height;
|
final y = size.height;
|
||||||
final more = <PopupMenuItem<String>>[];
|
final more = <PopupMenuItem<String>>[];
|
||||||
final pi = FFI.ffiModel.pi;
|
final pi = gFFI.ffiModel.pi;
|
||||||
final perms = FFI.ffiModel.permissions;
|
final perms = gFFI.ffiModel.permissions;
|
||||||
if (pi.version.isNotEmpty) {
|
if (pi.version.isNotEmpty) {
|
||||||
more.add(PopupMenuItem<String>(
|
more.add(PopupMenuItem<String>(
|
||||||
child: Text(translate('Refresh')), value: 'refresh'));
|
child: Text(translate('Refresh')), value: 'refresh'));
|
||||||
@@ -663,14 +671,13 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
showSetOSPassword(id, false, gFFI.dialogManager);
|
||||||
showSetOSPassword(false);
|
|
||||||
},
|
},
|
||||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
child: Icon(Icons.edit, color: MyTheme.accent),
|
||||||
)
|
)
|
||||||
])),
|
])),
|
||||||
value: 'enter_os_password'));
|
value: 'enter_os_password'));
|
||||||
if (!isDesktop) {
|
if (!isWebDesktop) {
|
||||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||||
more.add(PopupMenuItem<String>(
|
more.add(PopupMenuItem<String>(
|
||||||
child: Text(translate('Paste')), value: 'paste'));
|
child: Text(translate('Paste')), value: 'paste'));
|
||||||
@@ -687,14 +694,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
more.add(PopupMenuItem<String>(
|
more.add(PopupMenuItem<String>(
|
||||||
child: Text(translate('Insert Lock')), value: 'lock'));
|
child: Text(translate('Insert Lock')), value: 'lock'));
|
||||||
if (pi.platform == 'Windows' &&
|
if (pi.platform == 'Windows' &&
|
||||||
FFI.getByName('toggle_option', 'privacy-mode') != 'true') {
|
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
|
||||||
|
true) {
|
||||||
more.add(PopupMenuItem<String>(
|
more.add(PopupMenuItem<String>(
|
||||||
child: Text(translate(
|
child: Text(translate((gFFI.ffiModel.inputBlocked ? 'Unb' : 'B') +
|
||||||
(FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
|
'lock user input')),
|
||||||
value: 'block-input'));
|
value: 'block-input'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (FFI.ffiModel.permissions["restart"] != false &&
|
if (gFFI.ffiModel.permissions["restart"] != false &&
|
||||||
(pi.platform == "Linux" ||
|
(pi.platform == "Linux" ||
|
||||||
pi.platform == "Windows" ||
|
pi.platform == "Windows" ||
|
||||||
pi.platform == "Mac OS")) {
|
pi.platform == "Mac OS")) {
|
||||||
@@ -709,33 +717,37 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
if (value == 'cad') {
|
if (value == 'cad') {
|
||||||
FFI.setByName('ctrl_alt_del');
|
bind.sessionCtrlAltDel(id: widget.id);
|
||||||
} else if (value == 'lock') {
|
} else if (value == 'lock') {
|
||||||
FFI.setByName('lock_screen');
|
bind.sessionLockScreen(id: widget.id);
|
||||||
} else if (value == 'block-input') {
|
} else if (value == 'block-input') {
|
||||||
FFI.setByName('toggle_option',
|
bind.sessionToggleOption(
|
||||||
(FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input');
|
id: widget.id,
|
||||||
FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked;
|
value: (gFFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input');
|
||||||
|
gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked;
|
||||||
} else if (value == 'refresh') {
|
} else if (value == 'refresh') {
|
||||||
FFI.setByName('refresh');
|
bind.sessionRefresh(id: widget.id);
|
||||||
} else if (value == 'paste') {
|
} else if (value == 'paste') {
|
||||||
() async {
|
() async {
|
||||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
if (data != null && data.text != null) {
|
if (data != null && data.text != null) {
|
||||||
FFI.setByName('input_string', '${data.text}');
|
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
} else if (value == 'enter_os_password') {
|
} else if (value == 'enter_os_password') {
|
||||||
var password = FFI.getByName('peer_option', "os-password");
|
// FIXME:
|
||||||
if (password != "") {
|
// null means no session of id
|
||||||
FFI.setByName('input_os_password', password);
|
// empty string means no password
|
||||||
|
var password = await bind.sessionGetOption(id: id, arg: "os-password");
|
||||||
|
if (password != null) {
|
||||||
|
bind.sessionInputOsPassword(id: widget.id, value: password);
|
||||||
} else {
|
} else {
|
||||||
showSetOSPassword(true);
|
showSetOSPassword(id, true, gFFI.dialogManager);
|
||||||
}
|
}
|
||||||
} else if (value == 'reset_canvas') {
|
} else if (value == 'reset_canvas') {
|
||||||
FFI.cursorModel.reset();
|
gFFI.cursorModel.reset();
|
||||||
} else if (value == 'restart') {
|
} else if (value == 'restart') {
|
||||||
showRestartRemoteDevice(pi, widget.id);
|
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
@@ -754,12 +766,12 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: EdgeInsets.symmetric(vertical: 10),
|
padding: EdgeInsets.symmetric(vertical: 10),
|
||||||
child: GestureHelp(
|
child: GestureHelp(
|
||||||
touchMode: FFI.ffiModel.touchMode,
|
touchMode: gFFI.ffiModel.touchMode,
|
||||||
onTouchModeChange: (t) {
|
onTouchModeChange: (t) {
|
||||||
FFI.ffiModel.toggleTouchMode();
|
gFFI.ffiModel.toggleTouchMode();
|
||||||
final v = FFI.ffiModel.touchMode ? 'Y' : '';
|
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||||
FFI.setByName('peer_option',
|
bind.sessionPeerOption(
|
||||||
'{"name": "touch-mode", "value": "$v"}');
|
id: widget.id, name: "touch", value: v);
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -790,21 +802,21 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
style: TextStyle(color: Colors.white, fontSize: 11)),
|
style: TextStyle(color: Colors.white, fontSize: 11)),
|
||||||
onPressed: onPressed);
|
onPressed: onPressed);
|
||||||
};
|
};
|
||||||
final pi = FFI.ffiModel.pi;
|
final pi = gFFI.ffiModel.pi;
|
||||||
final isMac = pi.platform == "Mac OS";
|
final isMac = pi.platform == "Mac OS";
|
||||||
final modifiers = <Widget>[
|
final modifiers = <Widget>[
|
||||||
wrap('Ctrl ', () {
|
wrap('Ctrl ', () {
|
||||||
setState(() => FFI.ctrl = !FFI.ctrl);
|
setState(() => gFFI.ctrl = !gFFI.ctrl);
|
||||||
}, FFI.ctrl),
|
}, gFFI.ctrl),
|
||||||
wrap(' Alt ', () {
|
wrap(' Alt ', () {
|
||||||
setState(() => FFI.alt = !FFI.alt);
|
setState(() => gFFI.alt = !gFFI.alt);
|
||||||
}, FFI.alt),
|
}, gFFI.alt),
|
||||||
wrap('Shift', () {
|
wrap('Shift', () {
|
||||||
setState(() => FFI.shift = !FFI.shift);
|
setState(() => gFFI.shift = !gFFI.shift);
|
||||||
}, FFI.shift),
|
}, gFFI.shift),
|
||||||
wrap(isMac ? ' Cmd ' : ' Win ', () {
|
wrap(isMac ? ' Cmd ' : ' Win ', () {
|
||||||
setState(() => FFI.command = !FFI.command);
|
setState(() => gFFI.command = !gFFI.command);
|
||||||
}, FFI.command),
|
}, gFFI.command),
|
||||||
];
|
];
|
||||||
final keys = <Widget>[
|
final keys = <Widget>[
|
||||||
wrap(
|
wrap(
|
||||||
@@ -836,44 +848,44 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
for (var i = 1; i <= 12; ++i) {
|
for (var i = 1; i <= 12; ++i) {
|
||||||
final name = 'F' + i.toString();
|
final name = 'F' + i.toString();
|
||||||
fn.add(wrap(name, () {
|
fn.add(wrap(name, () {
|
||||||
FFI.inputKey('VK_' + name);
|
gFFI.inputKey('VK_' + name);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
final more = <Widget>[
|
final more = <Widget>[
|
||||||
SizedBox(width: 9999),
|
SizedBox(width: 9999),
|
||||||
wrap('Esc', () {
|
wrap('Esc', () {
|
||||||
FFI.inputKey('VK_ESCAPE');
|
gFFI.inputKey('VK_ESCAPE');
|
||||||
}),
|
}),
|
||||||
wrap('Tab', () {
|
wrap('Tab', () {
|
||||||
FFI.inputKey('VK_TAB');
|
gFFI.inputKey('VK_TAB');
|
||||||
}),
|
}),
|
||||||
wrap('Home', () {
|
wrap('Home', () {
|
||||||
FFI.inputKey('VK_HOME');
|
gFFI.inputKey('VK_HOME');
|
||||||
}),
|
}),
|
||||||
wrap('End', () {
|
wrap('End', () {
|
||||||
FFI.inputKey('VK_END');
|
gFFI.inputKey('VK_END');
|
||||||
}),
|
}),
|
||||||
wrap('Del', () {
|
wrap('Del', () {
|
||||||
FFI.inputKey('VK_DELETE');
|
gFFI.inputKey('VK_DELETE');
|
||||||
}),
|
}),
|
||||||
wrap('PgUp', () {
|
wrap('PgUp', () {
|
||||||
FFI.inputKey('VK_PRIOR');
|
gFFI.inputKey('VK_PRIOR');
|
||||||
}),
|
}),
|
||||||
wrap('PgDn', () {
|
wrap('PgDn', () {
|
||||||
FFI.inputKey('VK_NEXT');
|
gFFI.inputKey('VK_NEXT');
|
||||||
}),
|
}),
|
||||||
SizedBox(width: 9999),
|
SizedBox(width: 9999),
|
||||||
wrap('', () {
|
wrap('', () {
|
||||||
FFI.inputKey('VK_LEFT');
|
gFFI.inputKey('VK_LEFT');
|
||||||
}, false, Icons.keyboard_arrow_left),
|
}, false, Icons.keyboard_arrow_left),
|
||||||
wrap('', () {
|
wrap('', () {
|
||||||
FFI.inputKey('VK_UP');
|
gFFI.inputKey('VK_UP');
|
||||||
}, false, Icons.keyboard_arrow_up),
|
}, false, Icons.keyboard_arrow_up),
|
||||||
wrap('', () {
|
wrap('', () {
|
||||||
FFI.inputKey('VK_DOWN');
|
gFFI.inputKey('VK_DOWN');
|
||||||
}, false, Icons.keyboard_arrow_down),
|
}, false, Icons.keyboard_arrow_down),
|
||||||
wrap('', () {
|
wrap('', () {
|
||||||
FFI.inputKey('VK_RIGHT');
|
gFFI.inputKey('VK_RIGHT');
|
||||||
}, false, Icons.keyboard_arrow_right),
|
}, false, Icons.keyboard_arrow_right),
|
||||||
wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () {
|
wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () {
|
||||||
sendPrompt(isMac, 'VK_C');
|
sendPrompt(isMac, 'VK_C');
|
||||||
@@ -906,7 +918,7 @@ class ImagePaint extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<ImageModel>(context);
|
final m = Provider.of<ImageModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
final adjust = FFI.cursorModel.adjustForKeyboard();
|
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||||
var s = c.scale;
|
var s = c.scale;
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: new ImagePainter(
|
painter: new ImagePainter(
|
||||||
@@ -920,7 +932,7 @@ class CursorPaint extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<CursorModel>(context);
|
final m = Provider.of<CursorModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
final adjust = FFI.cursorModel.adjustForKeyboard();
|
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||||
var s = c.scale;
|
var s = c.scale;
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: new ImagePainter(
|
painter: new ImagePainter(
|
||||||
@@ -961,7 +973,7 @@ class ImagePainter extends CustomPainter {
|
|||||||
class QualityMonitor extends StatelessWidget {
|
class QualityMonitor extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
||||||
value: FFI.qualityMonitorModel,
|
value: gFFI.qualityMonitorModel,
|
||||||
child: Consumer<QualityMonitorModel>(
|
child: Consumer<QualityMonitorModel>(
|
||||||
builder: (context, qualityMonitorModel, child) => Positioned(
|
builder: (context, qualityMonitorModel, child) => Positioned(
|
||||||
top: 10,
|
top: 10,
|
||||||
@@ -974,23 +986,23 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Speed: ${qualityMonitorModel.data.speed}",
|
"Speed: ${qualityMonitorModel.data.speed ?? ''}",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"FPS: ${qualityMonitorModel.data.fps}",
|
"FPS: ${qualityMonitorModel.data.fps ?? ''}",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Delay: ${qualityMonitorModel.data.delay} ms",
|
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate}kb",
|
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Codec: ${qualityMonitorModel.data.codecFormat}",
|
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -999,29 +1011,14 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
: SizedBox.shrink())));
|
: SizedBox.shrink())));
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckboxListTile getToggle(
|
void showOptions(String id, OverlayDialogManager dialogManager) async {
|
||||||
void Function(void Function()) setState, option, name) {
|
String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced';
|
||||||
return CheckboxListTile(
|
|
||||||
value: FFI.getByName('toggle_option', option) == 'true',
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
FFI.setByName('toggle_option', option);
|
|
||||||
});
|
|
||||||
if (option == "show-quality-monitor") {
|
|
||||||
FFI.qualityMonitorModel.checkShowQualityMonitor();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dense: true,
|
|
||||||
title: Text(translate(name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void showOptions() {
|
|
||||||
String quality = FFI.getByName('image_quality');
|
|
||||||
if (quality == '') quality = 'balanced';
|
if (quality == '') quality = 'balanced';
|
||||||
String viewStyle = FFI.getByName('peer_option', 'view-style');
|
String viewStyle =
|
||||||
|
await bind.sessionGetOption(id: id, arg: 'view-style') ?? '';
|
||||||
var displays = <Widget>[];
|
var displays = <Widget>[];
|
||||||
final pi = FFI.ffiModel.pi;
|
final pi = gFFI.ffiModel.pi;
|
||||||
final image = FFI.ffiModel.getConnectionImage();
|
final image = gFFI.ffiModel.getConnectionImage();
|
||||||
if (image != null)
|
if (image != null)
|
||||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
||||||
if (pi.displays.length > 1) {
|
if (pi.displays.length > 1) {
|
||||||
@@ -1031,8 +1028,8 @@ void showOptions() {
|
|||||||
children.add(InkWell(
|
children.add(InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (i == cur) return;
|
if (i == cur) return;
|
||||||
FFI.setByName('switch_display', i.toString());
|
bind.sessionSwitchDisplay(id: id, value: i);
|
||||||
SmartDialog.dismiss();
|
gFFI.dialogManager.dismissAll();
|
||||||
},
|
},
|
||||||
child: Ink(
|
child: Ink(
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -1055,36 +1052,36 @@ void showOptions() {
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
displays.add(Divider(color: MyTheme.border));
|
displays.add(Divider(color: MyTheme.border));
|
||||||
}
|
}
|
||||||
final perms = FFI.ffiModel.permissions;
|
final perms = gFFI.ffiModel.permissions;
|
||||||
|
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
final more = <Widget>[];
|
final more = <Widget>[];
|
||||||
if (perms['audio'] != false) {
|
if (perms['audio'] != false) {
|
||||||
more.add(getToggle(setState, 'disable-audio', 'Mute'));
|
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
|
||||||
}
|
}
|
||||||
if (perms['keyboard'] != false) {
|
if (perms['keyboard'] != false) {
|
||||||
if (perms['clipboard'] != false)
|
if (perms['clipboard'] != false)
|
||||||
more.add(getToggle(setState, 'disable-clipboard', 'Disable clipboard'));
|
more.add(
|
||||||
|
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
|
||||||
more.add(getToggle(
|
more.add(getToggle(
|
||||||
setState, 'lock-after-session-end', 'Lock after session end'));
|
id, setState, 'lock-after-session-end', 'Lock after session end'));
|
||||||
if (pi.platform == 'Windows') {
|
if (pi.platform == 'Windows') {
|
||||||
more.add(getToggle(setState, 'privacy-mode', 'Privacy mode'));
|
more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var setQuality = (String? value) {
|
var setQuality = (String? value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
quality = value;
|
quality = value;
|
||||||
FFI.setByName('image_quality', value);
|
bind.sessionSetImageQuality(id: id, value: value);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
var setViewStyle = (String? value) {
|
var setViewStyle = (String? value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
viewStyle = value;
|
viewStyle = value;
|
||||||
FFI.setByName(
|
bind.sessionPeerOption(id: id, name: "view-style", value: value);
|
||||||
'peer_option', '{"name": "view-style", "value": "$value"}');
|
gFFI.canvasModel.updateViewStyle();
|
||||||
FFI.canvasModel.updateViewStyle();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
@@ -1101,9 +1098,10 @@ void showOptions() {
|
|||||||
getRadio('Balanced', 'balanced', quality, setQuality),
|
getRadio('Balanced', 'balanced', quality, setQuality),
|
||||||
getRadio('Optimize reaction time', 'low', quality, setQuality),
|
getRadio('Optimize reaction time', 'low', quality, setQuality),
|
||||||
Divider(color: MyTheme.border),
|
Divider(color: MyTheme.border),
|
||||||
getToggle(setState, 'show-remote-cursor', 'Show remote cursor'),
|
|
||||||
getToggle(
|
getToggle(
|
||||||
setState, 'show-quality-monitor', 'Show quality monitor'),
|
id, setState, 'show-remote-cursor', 'Show remote cursor'),
|
||||||
|
getToggle(id, setState, 'show-quality-monitor',
|
||||||
|
'Show quality monitor'),
|
||||||
] +
|
] +
|
||||||
more),
|
more),
|
||||||
actions: [],
|
actions: [],
|
||||||
@@ -1112,33 +1110,13 @@ void showOptions() {
|
|||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showRestartRemoteDevice(PeerInfo pi, String id) async {
|
void showSetOSPassword(
|
||||||
final res =
|
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||||
await DialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
|
||||||
title: Row(children: [
|
|
||||||
Icon(Icons.warning_amber_sharp,
|
|
||||||
color: Colors.redAccent, size: 28),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Text(translate("Restart Remote Device")),
|
|
||||||
]),
|
|
||||||
content: Text(
|
|
||||||
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => close(), child: Text(translate("Cancel"))),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => close(true), child: Text(translate("OK"))),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
if (res == true) FFI.setByName('restart_remote_device');
|
|
||||||
}
|
|
||||||
|
|
||||||
void showSetOSPassword(bool login) {
|
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
var password = FFI.getByName('peer_option', "os-password");
|
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
|
||||||
var autoLogin = FFI.getByName('peer_option', "auto-login") != "";
|
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||||
controller.text = password;
|
controller.text = password;
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('OS Password')),
|
title: Text(translate('OS Password')),
|
||||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
@@ -1169,12 +1147,11 @@ void showSetOSPassword(bool login) {
|
|||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var text = controller.text.trim();
|
var text = controller.text.trim();
|
||||||
FFI.setByName(
|
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||||
'peer_option', '{"name": "os-password", "value": "$text"}');
|
bind.sessionPeerOption(
|
||||||
FFI.setByName('peer_option',
|
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||||
'{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}');
|
|
||||||
if (text != "" && login) {
|
if (text != "" && login) {
|
||||||
FFI.setByName('input_os_password', text);
|
bind.sessionInputOsPassword(id: id, value: text);
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
@@ -1185,17 +1162,17 @@ void showSetOSPassword(bool login) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendPrompt(bool isMac, String key) {
|
void sendPrompt(bool isMac, String key) {
|
||||||
final old = isMac ? FFI.command : FFI.ctrl;
|
final old = isMac ? gFFI.command : gFFI.ctrl;
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
FFI.command = true;
|
gFFI.command = true;
|
||||||
} else {
|
} else {
|
||||||
FFI.ctrl = true;
|
gFFI.ctrl = true;
|
||||||
}
|
}
|
||||||
FFI.inputKey(key);
|
gFFI.inputKey(key);
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
FFI.command = old;
|
gFFI.command = old;
|
||||||
} else {
|
} else {
|
||||||
FFI.ctrl = old;
|
gFFI.ctrl = old;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:zxing2/qrcode.dart';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import '../common.dart';
|
import 'dart:io';
|
||||||
import '../models/model.dart';
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||||
|
import 'package:zxing2/qrcode.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
|
||||||
class ScanPage extends StatefulWidget {
|
class ScanPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@@ -119,7 +121,7 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
|
|
||||||
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
|
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
|
||||||
if (!p) {
|
if (!p) {
|
||||||
showToast('No permisssion');
|
showToast('No permission');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +132,7 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showServerSettingFromQr(String data) async {
|
void showServerSettingFromQr(String data) async {
|
||||||
backToHome();
|
closeConnection();
|
||||||
await controller?.pauseCamera();
|
await controller?.pauseCamera();
|
||||||
if (!data.startsWith('config=')) {
|
if (!data.startsWith('config=')) {
|
||||||
showToast('Invalid QR code');
|
showToast('Invalid QR code');
|
||||||
@@ -142,7 +144,7 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
var key = values['key'] != null ? values['key'] as String : '';
|
var key = values['key'] != null ? values['key'] as String : '';
|
||||||
var api = values['api'] != null ? values['api'] as String : '';
|
var api = values['api'] != null ? values['api'] as String : '';
|
||||||
Timer(Duration(milliseconds: 60), () {
|
Timer(Duration(milliseconds: 60), () {
|
||||||
showServerSettingsWithValue(host, '', key, api);
|
showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Invalid QR code');
|
showToast('Invalid QR code');
|
||||||
@@ -150,55 +152,81 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showServerSettingsWithValue(
|
void showServerSettingsWithValue(String id, String relay, String key,
|
||||||
String id, String relay, String key, String api) {
|
String api, OverlayDialogManager dialogManager) async {
|
||||||
final formKey = GlobalKey<FormState>();
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||||
final id0 = FFI.getByName('option', 'custom-rendezvous-server');
|
String id0 = oldOptions['custom-rendezvous-server'] ?? "";
|
||||||
final relay0 = FFI.getByName('option', 'relay-server');
|
String relay0 = oldOptions['relay-server'] ?? "";
|
||||||
final api0 = FFI.getByName('option', 'api-server');
|
String api0 = oldOptions['api-server'] ?? "";
|
||||||
final key0 = FFI.getByName('option', 'key');
|
String key0 = oldOptions['key'] ?? "";
|
||||||
DialogManager.show((setState, close) {
|
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(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('ID/Relay Server')),
|
title: Text(translate('ID/Relay Server')),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: id,
|
controller: idController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: translate('ID Server'),
|
labelText: translate('ID Server'),
|
||||||
),
|
errorText: idServerMsg),
|
||||||
validator: validate,
|
|
||||||
onSaved: (String? value) {
|
|
||||||
if (value != null) id = value.trim();
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
(isAndroid
|
(isAndroid
|
||||||
? [
|
? [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: relay,
|
controller: relayController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: translate('Relay Server'),
|
labelText: translate('Relay Server'),
|
||||||
),
|
errorText: relayServerMsg),
|
||||||
validator: validate,
|
|
||||||
onSaved: (String? value) {
|
|
||||||
if (value != null) relay = value.trim();
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
: []) +
|
: []) +
|
||||||
[
|
[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: api,
|
controller: apiController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: translate('API Server'),
|
labelText: translate('API Server'),
|
||||||
),
|
),
|
||||||
validator: validate,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
onSaved: (String? value) {
|
validator: (v) {
|
||||||
if (value != null) api = value.trim();
|
if (v != null && v.length > 0) {
|
||||||
|
if (!(v.startsWith('http://') ||
|
||||||
|
v.startsWith("https://"))) {
|
||||||
|
return translate("invalid_http");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apiServerMsg;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -206,11 +234,13 @@ void showServerSettingsWithValue(
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Key',
|
labelText: 'Key',
|
||||||
),
|
),
|
||||||
validator: null,
|
onChanged: (String? value) {
|
||||||
onSaved: (String? value) {
|
|
||||||
if (value != null) key = value.trim();
|
if (value != null) key = value.trim();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress,
|
||||||
|
child: LinearProgressIndicator())
|
||||||
])),
|
])),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -222,24 +252,28 @@ void showServerSettingsWithValue(
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (formKey.currentState != null &&
|
setState(() {
|
||||||
formKey.currentState!.validate()) {
|
idServerMsg = null;
|
||||||
formKey.currentState!.save();
|
relayServerMsg = null;
|
||||||
if (id != id0)
|
apiServerMsg = null;
|
||||||
FFI.setByName('option',
|
isInProgress = true;
|
||||||
'{"name": "custom-rendezvous-server", "value": "$id"}');
|
});
|
||||||
|
if (await validate()) {
|
||||||
|
if (id != id0) {
|
||||||
|
bind.mainSetOption(key: "custom-rendezvous-server", value: id);
|
||||||
|
}
|
||||||
if (relay != relay0)
|
if (relay != relay0)
|
||||||
FFI.setByName(
|
bind.mainSetOption(key: "relay-server", value: relay);
|
||||||
'option', '{"name": "relay-server", "value": "$relay"}');
|
if (key != key0) bind.mainSetOption(key: "key", value: key);
|
||||||
if (key != key0)
|
|
||||||
FFI.setByName('option', '{"name": "key", "value": "$key"}');
|
|
||||||
if (api != api0)
|
if (api != api0)
|
||||||
FFI.setByName(
|
bind.mainSetOption(key: "api-server", value: api);
|
||||||
'option', '{"name": "api-server", "value": "$api"}');
|
gFFI.ffiModel.updateUser();
|
||||||
FFI.ffiModel.updateUser();
|
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: Text(translate('OK')),
|
child: Text(translate('OK')),
|
||||||
),
|
),
|
||||||
@@ -248,11 +282,11 @@ void showServerSettingsWithValue(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String? validate(value) {
|
Future<String?> validateAsync(String value) async {
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
if (value.isEmpty) {
|
if (value.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final res = FFI.getByName('test_if_valid_server', value);
|
final res = await bind.mainTestIfValidServer(server: value);
|
||||||
return res.isEmpty ? null : res;
|
return res.isEmpty ? null : res;
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/widgets/dialog.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../../common.dart';
|
||||||
import '../models/server_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/server_model.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import '../models/model.dart';
|
|
||||||
|
|
||||||
class ServerPage extends StatelessWidget implements PageShape {
|
class ServerPage extends StatefulWidget implements PageShape {
|
||||||
@override
|
@override
|
||||||
final title = translate("Share Screen");
|
final title = translate("Share Screen");
|
||||||
|
|
||||||
@@ -35,14 +31,14 @@ class ServerPage extends StatelessWidget implements PageShape {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
value: "setPermanentPassword",
|
value: "setPermanentPassword",
|
||||||
enabled:
|
enabled:
|
||||||
FFI.serverModel.verificationMethod != kUseTemporaryPassword,
|
gFFI.serverModel.verificationMethod != kUseTemporaryPassword,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Text(translate("Set temporary password length")),
|
child: Text(translate("Set temporary password length")),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
value: "setTemporaryPasswordLength",
|
value: "setTemporaryPasswordLength",
|
||||||
enabled:
|
enabled:
|
||||||
FFI.serverModel.verificationMethod != kUsePermanentPassword,
|
gFFI.serverModel.verificationMethod != kUsePermanentPassword,
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -53,7 +49,7 @@ class ServerPage extends StatelessWidget implements PageShape {
|
|||||||
title: Text(translate("Use temporary password")),
|
title: Text(translate("Use temporary password")),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
color: FFI.serverModel.verificationMethod ==
|
color: gFFI.serverModel.verificationMethod ==
|
||||||
kUseTemporaryPassword
|
kUseTemporaryPassword
|
||||||
? null
|
? null
|
||||||
: Color(0xFFFFFFFF),
|
: Color(0xFFFFFFFF),
|
||||||
@@ -66,7 +62,7 @@ class ServerPage extends StatelessWidget implements PageShape {
|
|||||||
title: Text(translate("Use permanent password")),
|
title: Text(translate("Use permanent password")),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
color: FFI.serverModel.verificationMethod ==
|
color: gFFI.serverModel.verificationMethod ==
|
||||||
kUsePermanentPassword
|
kUsePermanentPassword
|
||||||
? null
|
? null
|
||||||
: Color(0xFFFFFFFF),
|
: Color(0xFFFFFFFF),
|
||||||
@@ -79,9 +75,9 @@ class ServerPage extends StatelessWidget implements PageShape {
|
|||||||
title: Text(translate("Use both passwords")),
|
title: Text(translate("Use both passwords")),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
color: FFI.serverModel.verificationMethod !=
|
color: gFFI.serverModel.verificationMethod !=
|
||||||
kUseTemporaryPassword &&
|
kUseTemporaryPassword &&
|
||||||
FFI.serverModel.verificationMethod !=
|
gFFI.serverModel.verificationMethod !=
|
||||||
kUsePermanentPassword
|
kUsePermanentPassword
|
||||||
? null
|
? null
|
||||||
: Color(0xFFFFFFFF),
|
: Color(0xFFFFFFFF),
|
||||||
@@ -93,29 +89,37 @@ class ServerPage extends StatelessWidget implements PageShape {
|
|||||||
if (value == "changeID") {
|
if (value == "changeID") {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (value == "setPermanentPassword") {
|
} else if (value == "setPermanentPassword") {
|
||||||
setPermanentPasswordDialog();
|
setPermanentPasswordDialog(gFFI.dialogManager);
|
||||||
} else if (value == "setTemporaryPasswordLength") {
|
} else if (value == "setTemporaryPasswordLength") {
|
||||||
setTemporaryPasswordLengthDialog();
|
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
||||||
} else if (value == kUsePermanentPassword ||
|
} else if (value == kUsePermanentPassword ||
|
||||||
value == kUseTemporaryPassword ||
|
value == kUseTemporaryPassword ||
|
||||||
value == kUseBothPasswords) {
|
value == kUseBothPasswords) {
|
||||||
Map<String, String> msg = Map()
|
bind.mainSetOption(key: "verification-method", value: value);
|
||||||
..["name"] = "verification-method"
|
gFFI.serverModel.updatePasswordModel();
|
||||||
..["value"] = value;
|
|
||||||
FFI.setByName('option', jsonEncode(msg));
|
|
||||||
FFI.serverModel.updatePasswordModel();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ServerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServerPageState extends State<ServerPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
gFFI.serverModel.checkAndroidPermission();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
checkService();
|
checkService();
|
||||||
return ChangeNotifierProvider.value(
|
return ChangeNotifierProvider.value(
|
||||||
value: FFI.serverModel,
|
value: gFFI.serverModel,
|
||||||
child: Consumer<ServerModel>(
|
child: Consumer<ServerModel>(
|
||||||
builder: (context, serverModel, child) => SingleChildScrollView(
|
builder: (context, serverModel, child) => SingleChildScrollView(
|
||||||
controller: FFI.serverModel.controller,
|
controller: gFFI.serverModel.controller,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@@ -132,16 +136,16 @@ class ServerPage extends StatelessWidget implements PageShape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void checkService() async {
|
void checkService() async {
|
||||||
FFI.invokeMethod("check_service"); // jvm
|
gFFI.invokeMethod("check_service"); // jvm
|
||||||
// for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page
|
// for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page
|
||||||
if (PermissionManager.isWaitingFile() && !FFI.serverModel.fileOk) {
|
if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
|
||||||
PermissionManager.complete("file", await PermissionManager.check("file"));
|
PermissionManager.complete("file", await PermissionManager.check("file"));
|
||||||
debugPrint("file permission finished");
|
debugPrint("file permission finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerInfo extends StatelessWidget {
|
class ServerInfo extends StatelessWidget {
|
||||||
final model = FFI.serverModel;
|
final model = gFFI.serverModel;
|
||||||
final emptyController = TextEditingController(text: "-");
|
final emptyController = TextEditingController(text: "-");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -183,9 +187,8 @@ class ServerInfo extends StatelessWidget {
|
|||||||
? null
|
? null
|
||||||
: IconButton(
|
: IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () {
|
onPressed: () =>
|
||||||
FFI.setByName("temporary_password");
|
bind.mainUpdateTemporaryPassword())),
|
||||||
})),
|
|
||||||
onSaved: (String? value) {},
|
onSaved: (String? value) {},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -356,12 +359,12 @@ class ConnectionManager extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
return Column(
|
return Column(
|
||||||
children: serverModel.clients.entries
|
children: serverModel.clients
|
||||||
.map((entry) => PaddingCard(
|
.map((client) => PaddingCard(
|
||||||
title: translate(entry.value.isFileTransfer
|
title: translate(client.isFileTransfer
|
||||||
? "File Connection"
|
? "File Connection"
|
||||||
: "Screen Connection"),
|
: "Screen Connection"),
|
||||||
titleIcon: entry.value.isFileTransfer
|
titleIcon: client.isFileTransfer
|
||||||
? Icons.folder_outlined
|
? Icons.folder_outlined
|
||||||
: Icons.mobile_screen_share,
|
: Icons.mobile_screen_share,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -370,16 +373,14 @@ class ConnectionManager extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: clientInfo(entry.value)),
|
Expanded(child: clientInfo(client)),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: -1,
|
flex: -1,
|
||||||
child: entry.value.isFileTransfer ||
|
child: client.isFileTransfer || !client.authorized
|
||||||
!entry.value.authorized
|
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: IconButton(
|
: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FFI.chatModel
|
gFFI.chatModel.changeCurrentID(client.id);
|
||||||
.changeCurrentID(entry.value.id);
|
|
||||||
final bar =
|
final bar =
|
||||||
navigationBarKey.currentWidget;
|
navigationBarKey.currentWidget;
|
||||||
if (bar != null) {
|
if (bar != null) {
|
||||||
@@ -393,37 +394,35 @@ class ConnectionManager extends StatelessWidget {
|
|||||||
)))
|
)))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entry.value.authorized
|
client.authorized
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: Text(
|
: Text(
|
||||||
translate("android_new_connection_tip"),
|
translate("android_new_connection_tip"),
|
||||||
style: TextStyle(color: Colors.black54),
|
style: TextStyle(color: Colors.black54),
|
||||||
),
|
),
|
||||||
entry.value.authorized
|
client.authorized
|
||||||
? ElevatedButton.icon(
|
? ElevatedButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
MaterialStateProperty.all(Colors.red)),
|
MaterialStateProperty.all(Colors.red)),
|
||||||
icon: Icon(Icons.close),
|
icon: Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FFI.setByName("close_conn", entry.key.toString());
|
bind.cmCloseConnection(connId: client.id);
|
||||||
FFI.invokeMethod(
|
gFFI.invokeMethod(
|
||||||
"cancel_notification", entry.key);
|
"cancel_notification", client.id);
|
||||||
},
|
},
|
||||||
label: Text(translate("Close")))
|
label: Text(translate("Close")))
|
||||||
: Row(children: [
|
: Row(children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(translate("Dismiss")),
|
child: Text(translate("Dismiss")),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
serverModel.sendLoginResponse(
|
serverModel.sendLoginResponse(client, false);
|
||||||
entry.value, false);
|
|
||||||
}),
|
}),
|
||||||
SizedBox(width: 20),
|
SizedBox(width: 20),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Text(translate("Accept")),
|
child: Text(translate("Accept")),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
serverModel.sendLoginResponse(
|
serverModel.sendLoginResponse(client, true);
|
||||||
entry.value, true);
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
@@ -511,15 +510,15 @@ Widget clientInfo(Client client) {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toAndroidChannelInit() {
|
void androidChannelInit() {
|
||||||
FFI.setMethodCallHandler((method, arguments) {
|
gFFI.setMethodCallHandler((method, arguments) {
|
||||||
debugPrint("flutter got android msg,$method,$arguments");
|
debugPrint("flutter got android msg,$method,$arguments");
|
||||||
try {
|
try {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "start_capture":
|
case "start_capture":
|
||||||
{
|
{
|
||||||
SmartDialog.dismiss();
|
gFFI.dialogManager.dismissAll();
|
||||||
FFI.serverModel.updateClientState();
|
gFFI.serverModel.updateClientState();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "on_state_changed":
|
case "on_state_changed":
|
||||||
@@ -527,7 +526,7 @@ void toAndroidChannelInit() {
|
|||||||
var name = arguments["name"] as String;
|
var name = arguments["name"] as String;
|
||||||
var value = arguments["value"] as String == "true";
|
var value = arguments["value"] as String == "true";
|
||||||
debugPrint("from jvm:on_state_changed,$name:$value");
|
debugPrint("from jvm:on_state_changed,$name:$value");
|
||||||
FFI.serverModel.changeStatue(name, value);
|
gFFI.serverModel.changeStatue(name, value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "on_android_permission_result":
|
case "on_android_permission_result":
|
||||||
@@ -539,7 +538,7 @@ void toAndroidChannelInit() {
|
|||||||
}
|
}
|
||||||
case "on_media_projection_canceled":
|
case "on_media_projection_canceled":
|
||||||
{
|
{
|
||||||
FFI.serverModel.stopService();
|
gFFI.serverModel.stopService();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import '../common.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
import '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
import '../models/model.dart';
|
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import 'scan_page.dart';
|
import 'scan_page.dart';
|
||||||
|
|
||||||
@@ -29,15 +31,38 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
|||||||
const url = 'https://rustdesk.com/';
|
const url = 'https://rustdesk.com/';
|
||||||
final _hasIgnoreBattery = androidVersion >= 26;
|
final _hasIgnoreBattery = androidVersion >= 26;
|
||||||
var _ignoreBatteryOpt = false;
|
var _ignoreBatteryOpt = false;
|
||||||
|
var _enableAbr = false;
|
||||||
|
|
||||||
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||||
|
String? username;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
if (_hasIgnoreBattery) {
|
|
||||||
updateIgnoreBatteryStatus();
|
() async {
|
||||||
}
|
var update = false;
|
||||||
|
if (_hasIgnoreBattery) {
|
||||||
|
update = await updateIgnoreBatteryStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
final usernameRes = await getUsername();
|
||||||
|
if (usernameRes != username) {
|
||||||
|
update = true;
|
||||||
|
username = usernameRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N";
|
||||||
|
if (enableAbrRes != _enableAbr) {
|
||||||
|
update = true;
|
||||||
|
_enableAbr = enableAbrRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,16 +74,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
updateIgnoreBatteryStatus();
|
() async {
|
||||||
|
if (await updateIgnoreBatteryStatus()) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> updateIgnoreBatteryStatus() async {
|
Future<bool> updateIgnoreBatteryStatus() async {
|
||||||
final res = await PermissionManager.check("ignore_battery_optimizations");
|
final res = await PermissionManager.check("ignore_battery_optimizations");
|
||||||
if (_ignoreBatteryOpt != res) {
|
if (_ignoreBatteryOpt != res) {
|
||||||
setState(() {
|
_ignoreBatteryOpt = res;
|
||||||
_ignoreBatteryOpt = res;
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@@ -68,21 +95,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Provider.of<FfiModel>(context);
|
Provider.of<FfiModel>(context);
|
||||||
final username = getUsername();
|
|
||||||
final enableAbr = FFI.getByName("option", "enable-abr") != 'N';
|
|
||||||
final enhancementsTiles = [
|
final enhancementsTiles = [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Adaptive Bitrate') + '(beta)'),
|
title: Text(translate('Adaptive Bitrate') + ' (beta)'),
|
||||||
initialValue: enableAbr,
|
initialValue: _enableAbr,
|
||||||
onToggle: (v) {
|
onToggle: (v) {
|
||||||
final msg = Map()
|
bind.mainSetOption(key: "enable-abr", value: v ? "" : "N");
|
||||||
..["name"] = "enable-abr"
|
setState(() {
|
||||||
..["value"] = "";
|
_enableAbr = !_enableAbr;
|
||||||
if (!v) {
|
});
|
||||||
msg["value"] = "N";
|
|
||||||
}
|
|
||||||
FFI.setByName("option", json.encode(msg));
|
|
||||||
setState(() {});
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
@@ -98,8 +119,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
if (v) {
|
if (v) {
|
||||||
PermissionManager.request("ignore_battery_optimizations");
|
PermissionManager.request("ignore_battery_optimizations");
|
||||||
} else {
|
} else {
|
||||||
final res = await DialogManager.show<bool>(
|
final res = await gFFI.dialogManager
|
||||||
(setState, close) => CustomAlertDialog(
|
.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
title: Text(translate("Open System Setting")),
|
title: Text(translate("Open System Setting")),
|
||||||
content: Text(translate(
|
content: Text(translate(
|
||||||
"android_open_battery_optimizations_tip")),
|
"android_open_battery_optimizations_tip")),
|
||||||
@@ -132,9 +153,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
leading: Icon(Icons.person),
|
leading: Icon(Icons.person),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
showLogin();
|
showLogin(gFFI.dialogManager);
|
||||||
} else {
|
} else {
|
||||||
logout();
|
logout(gFFI.dialogManager);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -145,13 +166,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
title: Text(translate('ID/Relay Server')),
|
title: Text(translate('ID/Relay Server')),
|
||||||
leading: Icon(Icons.cloud),
|
leading: Icon(Icons.cloud),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showServerSettings();
|
showServerSettings(gFFI.dialogManager);
|
||||||
}),
|
}),
|
||||||
SettingsTile.navigation(
|
SettingsTile.navigation(
|
||||||
title: Text(translate('Language')),
|
title: Text(translate('Language')),
|
||||||
leading: Icon(Icons.translate),
|
leading: Icon(Icons.translate),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showLanguageSettings();
|
showLanguageSettings(gFFI.dialogManager);
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
@@ -183,29 +204,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showServerSettings() {
|
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||||
final id = FFI.getByName('option', 'custom-rendezvous-server');
|
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
|
||||||
final relay = FFI.getByName('option', 'relay-server');
|
String id = options['custom-rendezvous-server'] ?? "";
|
||||||
final api = FFI.getByName('option', 'api-server');
|
String relay = options['relay-server'] ?? "";
|
||||||
final key = FFI.getByName('option', 'key');
|
String api = options['api-server'] ?? "";
|
||||||
showServerSettingsWithValue(id, relay, key, api);
|
String key = options['key'] ?? "";
|
||||||
|
showServerSettingsWithValue(id, relay, key, api, dialogManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLanguageSettings() {
|
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||||
try {
|
try {
|
||||||
final langs = json.decode(FFI.getByName('langs')) as List<dynamic>;
|
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||||
var lang = FFI.getByName('local_option', 'lang');
|
var lang = await bind.mainGetLocalOption(key: "lang");
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
final setLang = (v) {
|
final setLang = (v) {
|
||||||
if (lang != v) {
|
if (lang != v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
lang = v;
|
lang = v;
|
||||||
});
|
});
|
||||||
final msg = Map()
|
bind.mainSetLocalOption(key: "lang", value: v);
|
||||||
..['name'] = 'lang'
|
HomePage.homeKey.currentState?.refreshPages();
|
||||||
..['value'] = v;
|
|
||||||
FFI.setByName('local_option', json.encode(msg));
|
|
||||||
homeKey.currentState?.refreshPages();
|
|
||||||
Future.delayed(Duration(milliseconds: 200), close);
|
Future.delayed(Duration(milliseconds: 200), close);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -227,8 +246,8 @@ void showLanguageSettings() {
|
|||||||
} catch (_e) {}
|
} catch (_e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showAbout() {
|
void showAbout(OverlayDialogManager dialogManager) {
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('About') + ' RustDesk'),
|
title: Text(translate('About') + ' RustDesk'),
|
||||||
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
||||||
@@ -276,8 +295,8 @@ fetch('http://localhost:21114/api/login', {
|
|||||||
final body = {
|
final body = {
|
||||||
'username': name,
|
'username': name,
|
||||||
'password': pass,
|
'password': pass,
|
||||||
'id': FFI.getByName('server_id'),
|
'id': bind.mainGetMyId(),
|
||||||
'uuid': FFI.getByName('uuid')
|
'uuid': bind.mainGetUuid()
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
final response = await http.post(Uri.parse('$url/api/login'),
|
final response = await http.post(Uri.parse('$url/api/login'),
|
||||||
@@ -297,25 +316,22 @@ String parseResp(String body) {
|
|||||||
}
|
}
|
||||||
final token = data['access_token'];
|
final token = data['access_token'];
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
FFI.setByName('option', '{"name": "access_token", "value": "$token"}');
|
bind.mainSetOption(key: "access_token", value: token);
|
||||||
}
|
}
|
||||||
final info = data['user'];
|
final info = data['user'];
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
final value = json.encode(info);
|
final value = json.encode(info);
|
||||||
FFI.setByName('option', json.encode({"name": "user_info", "value": value}));
|
bind.mainSetOption(key: "user_info", value: value);
|
||||||
FFI.ffiModel.updateUser();
|
gFFI.ffiModel.updateUser();
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshCurrentUser() async {
|
void refreshCurrentUser() async {
|
||||||
final token = FFI.getByName("option", "access_token");
|
final token = await bind.mainGetOption(key: "access_token");
|
||||||
if (token == '') return;
|
if (token == '') return;
|
||||||
final url = getUrl();
|
final url = getUrl();
|
||||||
final body = {
|
final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()};
|
||||||
'id': FFI.getByName('server_id'),
|
|
||||||
'uuid': FFI.getByName('uuid')
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
final response = await http.post(Uri.parse('$url/api/currentUser'),
|
final response = await http.post(Uri.parse('$url/api/currentUser'),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -334,14 +350,11 @@ void refreshCurrentUser() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void logout() async {
|
void logout(OverlayDialogManager dialogManager) async {
|
||||||
final token = FFI.getByName("option", "access_token");
|
final token = await bind.mainGetOption(key: "access_token");
|
||||||
if (token == '') return;
|
if (token == '') return;
|
||||||
final url = getUrl();
|
final url = getUrl();
|
||||||
final body = {
|
final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()};
|
||||||
'id': FFI.getByName('server_id'),
|
|
||||||
'uuid': FFI.getByName('uuid')
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
await http.post(Uri.parse('$url/api/logout'),
|
await http.post(Uri.parse('$url/api/logout'),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -355,16 +368,16 @@ void logout() async {
|
|||||||
resetToken();
|
resetToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetToken() {
|
void resetToken() async {
|
||||||
FFI.setByName('option', '{"name": "access_token", "value": ""}');
|
await bind.mainSetOption(key: "access_token", value: "");
|
||||||
FFI.setByName('option', '{"name": "user_info", "value": ""}');
|
await bind.mainSetOption(key: "user_info", value: "");
|
||||||
FFI.ffiModel.updateUser();
|
gFFI.ffiModel.updateUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getUrl() {
|
Future<String> getUrl() async {
|
||||||
var url = FFI.getByName('option', 'api-server');
|
var url = await bind.mainGetOption(key: "api-server");
|
||||||
if (url == '') {
|
if (url == '') {
|
||||||
url = FFI.getByName('option', 'custom-rendezvous-server');
|
url = await bind.mainGetOption(key: "custom-rendezvous-server");
|
||||||
if (url != '') {
|
if (url != '') {
|
||||||
if (url.contains(':')) {
|
if (url.contains(':')) {
|
||||||
final tmp = url.split(':');
|
final tmp = url.split(':');
|
||||||
@@ -383,12 +396,12 @@ String getUrl() {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLogin() {
|
void showLogin(OverlayDialogManager dialogManager) {
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
var loading = false;
|
var loading = false;
|
||||||
var error = '';
|
var error = '';
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('Login')),
|
title: Text(translate('Login')),
|
||||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
@@ -453,11 +466,11 @@ void showLogin() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getUsername() {
|
Future<String?> getUsername() async {
|
||||||
final token = FFI.getByName("option", "access_token");
|
final token = await bind.mainGetOption(key: "access_token");
|
||||||
String? username;
|
String? username;
|
||||||
if (token != "") {
|
if (token != "") {
|
||||||
final info = FFI.getByName("option", "user_info");
|
final info = await bind.mainGetOption(key: "user_info");
|
||||||
if (info != "") {
|
if (info != "") {
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic> tmp = json.decode(info);
|
Map<String, dynamic> tmp = json.decode(info);
|
||||||
@@ -1,33 +1,51 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import '../common.dart';
|
|
||||||
import '../models/model.dart';
|
|
||||||
|
|
||||||
void clientClose() {
|
import '../../common.dart';
|
||||||
msgBox('', 'Close', 'Are you sure to close the connection?');
|
import '../../models/model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
|
||||||
|
void clientClose(OverlayDialogManager dialogManager) {
|
||||||
|
msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEC1 = Duration(seconds: 1);
|
void showSuccess() {
|
||||||
void showSuccess({Duration duration = SEC1}) {
|
showToast(translate("Successful"));
|
||||||
SmartDialog.dismiss();
|
|
||||||
showToast(translate("Successful"), duration: SEC1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showError({Duration duration = SEC1}) {
|
void showError() {
|
||||||
SmartDialog.dismiss();
|
showToast(translate("Error"));
|
||||||
showToast(translate("Error"), duration: SEC1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPermanentPasswordDialog() {
|
void showRestartRemoteDevice(
|
||||||
final pw = FFI.getByName("permanent_password");
|
PeerInfo pi, String id, OverlayDialogManager dialogManager) async {
|
||||||
|
final res =
|
||||||
|
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
|
title: Row(children: [
|
||||||
|
Icon(Icons.warning_amber_sharp,
|
||||||
|
color: Colors.redAccent, size: 28),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(translate("Restart Remote Device")),
|
||||||
|
]),
|
||||||
|
content: Text(
|
||||||
|
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(), child: Text(translate("Cancel"))),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => close(true), child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
if (res == true) bind.sessionRestartRemoteDevice(id: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||||
|
final pw = await bind.mainGetPermanentPassword();
|
||||||
final p0 = TextEditingController(text: pw);
|
final p0 = TextEditingController(text: pw);
|
||||||
final p1 = TextEditingController(text: pw);
|
final p1 = TextEditingController(text: pw);
|
||||||
var validateLength = false;
|
var validateLength = false;
|
||||||
var validateSame = false;
|
var validateSame = false;
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('Set your own password')),
|
title: Text(translate('Set your own password')),
|
||||||
content: Form(
|
content: Form(
|
||||||
@@ -87,10 +105,12 @@ void setPermanentPasswordDialog() {
|
|||||||
onPressed: (validateLength && validateSame)
|
onPressed: (validateLength && validateSame)
|
||||||
? () async {
|
? () async {
|
||||||
close();
|
close();
|
||||||
showLoading(translate("Waiting"));
|
dialogManager.showLoading(translate("Waiting"));
|
||||||
if (await FFI.serverModel.setPermanentPassword(p0.text)) {
|
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
|
||||||
|
dialogManager.dismissAll();
|
||||||
showSuccess();
|
showSuccess();
|
||||||
} else {
|
} else {
|
||||||
|
dialogManager.dismissAll();
|
||||||
showError();
|
showError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,24 +122,22 @@ void setPermanentPasswordDialog() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTemporaryPasswordLengthDialog() {
|
void setTemporaryPasswordLengthDialog(
|
||||||
|
OverlayDialogManager dialogManager) async {
|
||||||
List<String> lengths = ['6', '8', '10'];
|
List<String> lengths = ['6', '8', '10'];
|
||||||
String length = FFI.getByName('option', 'temporary-password-length');
|
String length = await bind.mainGetOption(key: "temporary-password-length");
|
||||||
var index = lengths.indexOf(length);
|
var index = lengths.indexOf(length);
|
||||||
if (index < 0) index = 0;
|
if (index < 0) index = 0;
|
||||||
length = lengths[index];
|
length = lengths[index];
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
final setLength = (newValue) {
|
final setLength = (newValue) {
|
||||||
final oldValue = length;
|
final oldValue = length;
|
||||||
if (oldValue == newValue) return;
|
if (oldValue == newValue) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
length = newValue;
|
length = newValue;
|
||||||
});
|
});
|
||||||
Map<String, String> msg = Map()
|
bind.mainSetOption(key: "temporary-password-length", value: newValue);
|
||||||
..["name"] = "temporary-password-length"
|
bind.mainUpdateTemporaryPassword();
|
||||||
..["value"] = newValue;
|
|
||||||
FFI.setByName("option", jsonEncode(msg));
|
|
||||||
FFI.setByName("temporary_password");
|
|
||||||
Future.delayed(Duration(milliseconds: 200), () {
|
Future.delayed(Duration(milliseconds: 200), () {
|
||||||
close();
|
close();
|
||||||
showSuccess();
|
showSuccess();
|
||||||
@@ -137,10 +155,11 @@ void setTemporaryPasswordLengthDialog() {
|
|||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterPasswordDialog(String id) {
|
void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
var remember = FFI.getByName('remember', id) == 'true';
|
var remember = await bind.sessionGetRemember(id: id) ?? false;
|
||||||
DialogManager.show((setState, close) {
|
dialogManager.dismissAll();
|
||||||
|
dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('Password Required')),
|
title: Text(translate('Password Required')),
|
||||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
@@ -165,7 +184,7 @@ void enterPasswordDialog(String id) {
|
|||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
close();
|
close();
|
||||||
backToHome();
|
closeConnection();
|
||||||
},
|
},
|
||||||
child: Text(translate('Cancel')),
|
child: Text(translate('Cancel')),
|
||||||
),
|
),
|
||||||
@@ -174,9 +193,10 @@ void enterPasswordDialog(String id) {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
var text = controller.text.trim();
|
var text = controller.text.trim();
|
||||||
if (text == '') return;
|
if (text == '') return;
|
||||||
FFI.login(text, remember);
|
gFFI.login(id, text, remember);
|
||||||
close();
|
close();
|
||||||
showLoading(translate('Logging in...'));
|
dialogManager.showLoading(translate('Logging in...'),
|
||||||
|
onCancel: closeConnection);
|
||||||
},
|
},
|
||||||
child: Text(translate('OK')),
|
child: Text(translate('OK')),
|
||||||
),
|
),
|
||||||
@@ -185,8 +205,8 @@ void enterPasswordDialog(String id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void wrongPasswordDialog(String id) {
|
void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) {
|
||||||
DialogManager.show((setState, close) => CustomAlertDialog(
|
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
title: Text(translate('Wrong Password')),
|
title: Text(translate('Wrong Password')),
|
||||||
content: Text(translate('Do you want to enter again?')),
|
content: Text(translate('Do you want to enter again?')),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -194,14 +214,14 @@ void wrongPasswordDialog(String id) {
|
|||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
close();
|
close();
|
||||||
backToHome();
|
closeConnection();
|
||||||
},
|
},
|
||||||
child: Text(translate('Cancel')),
|
child: Text(translate('Cancel')),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
enterPasswordDialog(id);
|
enterPasswordDialog(id, dialogManager);
|
||||||
},
|
},
|
||||||
child: Text(translate('Retry')),
|
child: Text(translate('Retry')),
|
||||||
),
|
),
|
||||||
@@ -243,8 +263,8 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
|||||||
//This will obscure text dynamically
|
//This will obscure text dynamically
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: Translator.call('Password'),
|
labelText: translate('Password'),
|
||||||
hintText: Translator.call('Enter your password'),
|
hintText: translate('Enter your password'),
|
||||||
// Here is key idea
|
// Here is key idea
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:toggle_switch/toggle_switch.dart';
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
import '../models/model.dart';
|
import '../../models/model.dart';
|
||||||
|
|
||||||
class GestureIcons {
|
class GestureIcons {
|
||||||
static const String _family = 'gestureicons';
|
static const String _family = 'gestureicons';
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
|
||||||
import '../models/model.dart';
|
import '../../models/chat_model.dart';
|
||||||
|
import '../../models/model.dart';
|
||||||
import '../pages/chat_page.dart';
|
import '../pages/chat_page.dart';
|
||||||
|
|
||||||
OverlayEntry? chatIconOverlayEntry;
|
|
||||||
OverlayEntry? chatWindowOverlayEntry;
|
|
||||||
|
|
||||||
OverlayEntry? mobileActionsOverlayEntry;
|
OverlayEntry? mobileActionsOverlayEntry;
|
||||||
|
|
||||||
class DraggableChatWindow extends StatelessWidget {
|
class DraggableChatWindow extends StatelessWidget {
|
||||||
DraggableChatWindow(
|
DraggableChatWindow(
|
||||||
{this.position = Offset.zero, required this.width, required this.height});
|
{this.position = Offset.zero,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
required this.chatModel});
|
||||||
|
|
||||||
final Offset position;
|
final Offset position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
|
final ChatModel chatModel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -27,7 +28,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
height: height,
|
height: height,
|
||||||
builder: (_, onPanUpdate) {
|
builder: (_, onPanUpdate) {
|
||||||
return isIOS
|
return isIOS
|
||||||
? ChatPage()
|
? ChatPage(chatModel: chatModel)
|
||||||
: Scaffold(
|
: Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
@@ -53,13 +54,13 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
hideChatWindowOverlay();
|
chatModel.hideChatWindowOverlay();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.keyboard_arrow_down)),
|
icon: Icon(Icons.keyboard_arrow_down)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
hideChatWindowOverlay();
|
chatModel.hideChatWindowOverlay();
|
||||||
hideChatIconOverlay();
|
chatModel.hideChatIconOverlay();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.close))
|
icon: Icon(Icons.close))
|
||||||
],
|
],
|
||||||
@@ -68,7 +69,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ChatPage(),
|
body: ChatPage(chatModel: chatModel),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,81 +92,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
Size get preferredSize => new Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => new Size.fromHeight(kToolbarHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
|
|
||||||
if (chatIconOverlayEntry != null) {
|
|
||||||
chatIconOverlayEntry!.remove();
|
|
||||||
}
|
|
||||||
if (globalKey.currentState == null || globalKey.currentState!.overlay == null)
|
|
||||||
return;
|
|
||||||
final bar = navigationBarKey.currentWidget;
|
|
||||||
if (bar != null) {
|
|
||||||
if ((bar as BottomNavigationBar).currentIndex == 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final globalOverlayState = globalKey.currentState!.overlay!;
|
|
||||||
|
|
||||||
final overlay = OverlayEntry(builder: (context) {
|
|
||||||
return DraggableFloatWidget(
|
|
||||||
config: DraggableFloatWidgetBaseConfig(
|
|
||||||
initPositionYInTop: false,
|
|
||||||
initPositionYMarginBorder: 100,
|
|
||||||
borderTopContainTopBar: true,
|
|
||||||
),
|
|
||||||
child: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (chatWindowOverlayEntry == null) {
|
|
||||||
showChatWindowOverlay();
|
|
||||||
} else {
|
|
||||||
hideChatWindowOverlay();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Icon(Icons.message)));
|
|
||||||
});
|
|
||||||
globalOverlayState.insert(overlay);
|
|
||||||
chatIconOverlayEntry = overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideChatIconOverlay() {
|
|
||||||
if (chatIconOverlayEntry != null) {
|
|
||||||
chatIconOverlayEntry!.remove();
|
|
||||||
chatIconOverlayEntry = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showChatWindowOverlay() {
|
|
||||||
if (chatWindowOverlayEntry != null) return;
|
|
||||||
if (globalKey.currentState == null || globalKey.currentState!.overlay == null)
|
|
||||||
return;
|
|
||||||
final globalOverlayState = globalKey.currentState!.overlay!;
|
|
||||||
|
|
||||||
final overlay = OverlayEntry(builder: (context) {
|
|
||||||
return DraggableChatWindow(
|
|
||||||
position: Offset(20, 80), width: 250, height: 350);
|
|
||||||
});
|
|
||||||
globalOverlayState.insert(overlay);
|
|
||||||
chatWindowOverlayEntry = overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideChatWindowOverlay() {
|
|
||||||
if (chatWindowOverlayEntry != null) {
|
|
||||||
chatWindowOverlayEntry!.remove();
|
|
||||||
chatWindowOverlayEntry = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleChatOverlay() {
|
|
||||||
if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) {
|
|
||||||
FFI.invokeMethod("enable_soft_keyboard", true);
|
|
||||||
showChatIconOverlay();
|
|
||||||
showChatWindowOverlay();
|
|
||||||
} else {
|
|
||||||
hideChatIconOverlay();
|
|
||||||
hideChatWindowOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// floating buttons of back/home/recent actions for android
|
/// floating buttons of back/home/recent actions for android
|
||||||
class DraggableMobileActions extends StatelessWidget {
|
class DraggableMobileActions extends StatelessWidget {
|
||||||
DraggableMobileActions(
|
DraggableMobileActions(
|
||||||
@@ -254,12 +180,12 @@ showMobileActionsOverlay() {
|
|||||||
position: Offset(left, top),
|
position: Offset(left, top),
|
||||||
width: overlayW,
|
width: overlayW,
|
||||||
height: overlayH,
|
height: overlayH,
|
||||||
onBackPressed: () => FFI.tap(MouseButtons.right),
|
onBackPressed: () => gFFI.tap(MouseButtons.right),
|
||||||
onHomePressed: () => FFI.tap(MouseButtons.wheel),
|
onHomePressed: () => gFFI.tap(MouseButtons.wheel),
|
||||||
onRecentPressed: () async {
|
onRecentPressed: () async {
|
||||||
FFI.sendMouse('down', MouseButtons.wheel);
|
gFFI.sendMouse('down', MouseButtons.wheel);
|
||||||
await Future.delayed(Duration(milliseconds: 500));
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
FFI.sendMouse('up', MouseButtons.wheel);
|
gFFI.sendMouse('up', MouseButtons.wheel);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
160
flutter/lib/models/ab_model.dart
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
class AbModel with ChangeNotifier {
|
||||||
|
var abLoading = false;
|
||||||
|
var abError = "";
|
||||||
|
var tags = [].obs;
|
||||||
|
var peers = [].obs;
|
||||||
|
|
||||||
|
var selectedTags = List<String>.empty(growable: true).obs;
|
||||||
|
|
||||||
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
AbModel(this.parent);
|
||||||
|
|
||||||
|
FFI? get _ffi => parent.target;
|
||||||
|
|
||||||
|
Future<dynamic> getAb() async {
|
||||||
|
abLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
// request
|
||||||
|
final api = "${await getApiServer()}/api/ab/get";
|
||||||
|
try {
|
||||||
|
final resp =
|
||||||
|
await http.post(Uri.parse(api), headers: await _getHeaders());
|
||||||
|
Map<String, dynamic> json = jsonDecode(resp.body);
|
||||||
|
if (json.containsKey('error')) {
|
||||||
|
abError = json['error'];
|
||||||
|
} else if (json.containsKey('data')) {
|
||||||
|
final data = jsonDecode(json['data']);
|
||||||
|
tags.value = data['tags'];
|
||||||
|
peers.value = data['peers'];
|
||||||
|
}
|
||||||
|
return resp.body;
|
||||||
|
} catch (err) {
|
||||||
|
abError = err.toString();
|
||||||
|
} finally {
|
||||||
|
abLoading = false;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getApiServer() async {
|
||||||
|
return await bind.mainGetApiServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
tags.clear();
|
||||||
|
peers.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>>? _getHeaders() {
|
||||||
|
return _ffi?.getHttpHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void addId(String id) async {
|
||||||
|
if (idContainBy(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peers.add({"id": id});
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTag(String tag) async {
|
||||||
|
if (tagContainBy(tag)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tags.add(tag);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeTagForPeer(String id, List<dynamic> tags) {
|
||||||
|
final it = peers.where((element) => element['id'] == id);
|
||||||
|
if (it.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it.first['tags'] = tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAb() async {
|
||||||
|
abLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
final api = "${await getApiServer()}/api/ab";
|
||||||
|
var authHeaders = await _getHeaders() ?? Map<String, String>();
|
||||||
|
authHeaders['Content-Type'] = "application/json";
|
||||||
|
final body = jsonEncode({
|
||||||
|
"data": jsonEncode({"tags": tags, "peers": peers})
|
||||||
|
});
|
||||||
|
final resp =
|
||||||
|
await http.post(Uri.parse(api), headers: authHeaders, body: body);
|
||||||
|
abLoading = false;
|
||||||
|
await getAb();
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint("resp: ${resp.body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool idContainBy(String id) {
|
||||||
|
return peers.where((element) => element['id'] == id).isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tagContainBy(String tag) {
|
||||||
|
return tags.where((element) => element == tag).isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deletePeer(String id) {
|
||||||
|
peers.removeWhere((element) => element['id'] == id);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteTag(String tag) {
|
||||||
|
tags.removeWhere((element) => element == tag);
|
||||||
|
for (var peer in peers) {
|
||||||
|
if (peer['tags'] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (((peer['tags']) as List<dynamic>).contains(tag)) {
|
||||||
|
((peer['tags']) as List<dynamic>).remove(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void unsetSelectedTags() {
|
||||||
|
selectedTags.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic> getPeerTags(String id) {
|
||||||
|
final it = peers.where((p0) => p0['id'] == id);
|
||||||
|
if (it.isEmpty) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return it.first['tags'] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPeerOption(String id, String key, String value) {
|
||||||
|
final it = peers.where((p0) => p0['id'] == id);
|
||||||
|
if (it.isEmpty) {
|
||||||
|
debugPrint("${id} is not exists");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
it.first[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
peers.clear();
|
||||||
|
tags.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dash_chat_2/dash_chat_2.dart';
|
import 'package:dash_chat_2/dash_chat_2.dart';
|
||||||
|
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import '../widgets/overlay.dart';
|
import '../../mobile/widgets/overlay.dart';
|
||||||
|
import '../common.dart';
|
||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
|
|
||||||
class MessageBody {
|
class MessageBody {
|
||||||
@@ -23,6 +25,14 @@ class MessageBody {
|
|||||||
class ChatModel with ChangeNotifier {
|
class ChatModel with ChangeNotifier {
|
||||||
static final clientModeID = -1;
|
static final clientModeID = -1;
|
||||||
|
|
||||||
|
/// _overlayState:
|
||||||
|
/// Desktop: store session overlay by using [setOverlayState].
|
||||||
|
/// Mobile: always null, use global overlay.
|
||||||
|
/// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay]
|
||||||
|
OverlayState? _overlayState;
|
||||||
|
OverlayEntry? chatIconOverlayEntry;
|
||||||
|
OverlayEntry? chatWindowOverlayEntry;
|
||||||
|
|
||||||
final ChatUser me = ChatUser(
|
final ChatUser me = ChatUser(
|
||||||
id: "",
|
id: "",
|
||||||
firstName: "Me",
|
firstName: "Me",
|
||||||
@@ -32,11 +42,19 @@ class ChatModel with ChangeNotifier {
|
|||||||
..[clientModeID] = MessageBody(me, []);
|
..[clientModeID] = MessageBody(me, []);
|
||||||
|
|
||||||
var _currentID = clientModeID;
|
var _currentID = clientModeID;
|
||||||
|
late bool _isShowChatPage = false;
|
||||||
|
|
||||||
Map<int, MessageBody> get messages => _messages;
|
Map<int, MessageBody> get messages => _messages;
|
||||||
|
|
||||||
int get currentID => _currentID;
|
int get currentID => _currentID;
|
||||||
|
|
||||||
|
bool get isShowChatPage => _isShowChatPage;
|
||||||
|
|
||||||
|
WeakReference<FFI> _ffi;
|
||||||
|
|
||||||
|
/// Constructor
|
||||||
|
ChatModel(this._ffi);
|
||||||
|
|
||||||
ChatUser get currentUser {
|
ChatUser get currentUser {
|
||||||
final user = messages[currentID]?.chatUser;
|
final user = messages[currentID]?.chatUser;
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
@@ -47,12 +65,117 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOverlayState(OverlayState? os) {
|
||||||
|
_overlayState = os;
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayState? _getOverlayState() {
|
||||||
|
if (_overlayState == null) {
|
||||||
|
if (globalKey.currentState == null ||
|
||||||
|
globalKey.currentState!.overlay == null) return null;
|
||||||
|
return globalKey.currentState!.overlay;
|
||||||
|
} else {
|
||||||
|
return _overlayState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
|
||||||
|
if (chatIconOverlayEntry != null) {
|
||||||
|
chatIconOverlayEntry!.remove();
|
||||||
|
}
|
||||||
|
// mobile check navigationBar
|
||||||
|
final bar = navigationBarKey.currentWidget;
|
||||||
|
if (bar != null) {
|
||||||
|
if ((bar as BottomNavigationBar).currentIndex == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final overlayState = _getOverlayState();
|
||||||
|
if (overlayState == null) return;
|
||||||
|
|
||||||
|
final overlay = OverlayEntry(builder: (context) {
|
||||||
|
return DraggableFloatWidget(
|
||||||
|
config: DraggableFloatWidgetBaseConfig(
|
||||||
|
initPositionYInTop: false,
|
||||||
|
initPositionYMarginBorder: 100,
|
||||||
|
borderTopContainTopBar: true,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (chatWindowOverlayEntry == null) {
|
||||||
|
showChatWindowOverlay();
|
||||||
|
} else {
|
||||||
|
hideChatWindowOverlay();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Icon(Icons.message)));
|
||||||
|
});
|
||||||
|
overlayState.insert(overlay);
|
||||||
|
chatIconOverlayEntry = overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideChatIconOverlay() {
|
||||||
|
if (chatIconOverlayEntry != null) {
|
||||||
|
chatIconOverlayEntry!.remove();
|
||||||
|
chatIconOverlayEntry = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showChatWindowOverlay() {
|
||||||
|
if (chatWindowOverlayEntry != null) return;
|
||||||
|
final overlayState = _getOverlayState();
|
||||||
|
if (overlayState == null) return;
|
||||||
|
final overlay = OverlayEntry(builder: (context) {
|
||||||
|
return DraggableChatWindow(
|
||||||
|
position: Offset(20, 80), width: 250, height: 350, chatModel: this);
|
||||||
|
});
|
||||||
|
overlayState.insert(overlay);
|
||||||
|
chatWindowOverlayEntry = overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideChatWindowOverlay() {
|
||||||
|
if (chatWindowOverlayEntry != null) {
|
||||||
|
chatWindowOverlayEntry!.remove();
|
||||||
|
chatWindowOverlayEntry = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChatOverlay() {
|
||||||
|
if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) {
|
||||||
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
|
showChatIconOverlay();
|
||||||
|
showChatWindowOverlay();
|
||||||
|
} else {
|
||||||
|
hideChatIconOverlay();
|
||||||
|
hideChatWindowOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCMChatPage(int id) async {
|
||||||
|
if (gFFI.chatModel.currentID != id) {
|
||||||
|
gFFI.chatModel.changeCurrentID(id);
|
||||||
|
}
|
||||||
|
if (_isShowChatPage) {
|
||||||
|
_isShowChatPage = !_isShowChatPage;
|
||||||
|
notifyListeners();
|
||||||
|
await windowManager.setSize(Size(400, 600));
|
||||||
|
} else {
|
||||||
|
await windowManager.setSize(Size(800, 600));
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
_isShowChatPage = !_isShowChatPage;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changeCurrentID(int id) {
|
changeCurrentID(int id) {
|
||||||
if (_messages.containsKey(id)) {
|
if (_messages.containsKey(id)) {
|
||||||
_currentID = id;
|
_currentID = id;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
final client = FFI.serverModel.clients[id];
|
final client = _ffi.target?.serverModel.clients
|
||||||
|
.firstWhere((client) => client.id == id);
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return debugPrint(
|
return debugPrint(
|
||||||
"Failed to changeCurrentID,remote user doesn't exist");
|
"Failed to changeCurrentID,remote user doesn't exist");
|
||||||
@@ -67,20 +190,26 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
receive(int id, String text) {
|
receive(int id, String text) async {
|
||||||
if (text.isEmpty) return;
|
if (text.isEmpty) return;
|
||||||
// first message show overlay icon
|
// mobile: first message show overlay icon
|
||||||
if (chatIconOverlayEntry == null) {
|
if (chatIconOverlayEntry == null) {
|
||||||
showChatIconOverlay();
|
showChatIconOverlay();
|
||||||
}
|
}
|
||||||
|
// desktop: show chat page
|
||||||
|
if (!_isShowChatPage) {
|
||||||
|
toggleCMChatPage(id);
|
||||||
|
}
|
||||||
|
_ffi.target?.serverModel.jumpTo(id);
|
||||||
|
|
||||||
late final chatUser;
|
late final chatUser;
|
||||||
if (id == clientModeID) {
|
if (id == clientModeID) {
|
||||||
chatUser = ChatUser(
|
chatUser = ChatUser(
|
||||||
firstName: FFI.ffiModel.pi.username,
|
firstName: _ffi.target?.ffiModel.pi.username,
|
||||||
id: FFI.getId(),
|
id: await bind.mainGetLastRemoteId(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final client = FFI.serverModel.clients[id];
|
final client = _ffi.target?.serverModel.clients[id];
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return debugPrint("Failed to receive msg,user doesn't exist");
|
return debugPrint("Failed to receive msg,user doesn't exist");
|
||||||
}
|
}
|
||||||
@@ -100,12 +229,11 @@ class ChatModel with ChangeNotifier {
|
|||||||
if (message.text.isNotEmpty) {
|
if (message.text.isNotEmpty) {
|
||||||
_messages[_currentID]?.insert(message);
|
_messages[_currentID]?.insert(message);
|
||||||
if (_currentID == clientModeID) {
|
if (_currentID == clientModeID) {
|
||||||
FFI.setByName("chat_client_mode", message.text);
|
if (_ffi.target != null) {
|
||||||
|
bind.sessionSendChat(id: _ffi.target!.id, text: message.text);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final msg = Map()
|
bind.cmSendChat(connId: _currentID, msg: message.text);
|
||||||
..["id"] = _currentID
|
|
||||||
..["text"] = message.text;
|
|
||||||
FFI.setByName("chat_server_mode", jsonEncode(msg));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -114,6 +242,7 @@ class ChatModel with ChangeNotifier {
|
|||||||
close() {
|
close() {
|
||||||
hideChatIconOverlay();
|
hideChatIconOverlay();
|
||||||
hideChatWindowOverlay();
|
hideChatWindowOverlay();
|
||||||
|
_overlayState = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter_hbb/common.dart';
|
|
||||||
import 'package:flutter_hbb/pages/file_manager_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as Path;
|
import 'package:path/path.dart' as Path;
|
||||||
|
|
||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
|
import 'platform_model.dart';
|
||||||
|
|
||||||
enum SortBy { Name, Type, Modified, Size }
|
enum SortBy { Name, Type, Modified, Size }
|
||||||
|
|
||||||
@@ -21,6 +23,11 @@ class FileModel extends ChangeNotifier {
|
|||||||
|
|
||||||
var _jobProgress = JobProgress(); // from rust update
|
var _jobProgress = JobProgress(); // from rust update
|
||||||
|
|
||||||
|
/// JobTable <jobId, JobProgress>
|
||||||
|
final _jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||||
|
|
||||||
|
RxList<JobProgress> get jobTable => _jobTable;
|
||||||
|
|
||||||
bool get isLocal => _isLocal;
|
bool get isLocal => _isLocal;
|
||||||
|
|
||||||
bool get selectMode => _selectMode;
|
bool get selectMode => _selectMode;
|
||||||
@@ -33,6 +40,20 @@ class FileModel extends ChangeNotifier {
|
|||||||
|
|
||||||
SortBy get sortStyle => _sortStyle;
|
SortBy get sortStyle => _sortStyle;
|
||||||
|
|
||||||
|
SortBy _localSortStyle = SortBy.Name;
|
||||||
|
|
||||||
|
bool _localSortAscending = true;
|
||||||
|
|
||||||
|
bool _remoteSortAscending = true;
|
||||||
|
|
||||||
|
SortBy _remoteSortStyle = SortBy.Name;
|
||||||
|
|
||||||
|
bool get localSortAscending => _localSortAscending;
|
||||||
|
|
||||||
|
SortBy getSortStyle(bool isLocal) {
|
||||||
|
return isLocal ? _localSortStyle : _remoteSortStyle;
|
||||||
|
}
|
||||||
|
|
||||||
FileDirectory _currentLocalDir = FileDirectory();
|
FileDirectory _currentLocalDir = FileDirectory();
|
||||||
|
|
||||||
FileDirectory get currentLocalDir => _currentLocalDir;
|
FileDirectory get currentLocalDir => _currentLocalDir;
|
||||||
@@ -43,8 +64,36 @@ class FileModel extends ChangeNotifier {
|
|||||||
|
|
||||||
FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
|
FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
|
||||||
|
|
||||||
|
FileDirectory getCurrentDir(bool isLocal) {
|
||||||
|
return isLocal ? currentLocalDir : currentRemoteDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCurrentShortPath(bool isLocal) {
|
||||||
|
final currentDir = getCurrentDir(isLocal);
|
||||||
|
final currentHome = getCurrentHome(isLocal);
|
||||||
|
if (currentDir.path.startsWith(currentHome)) {
|
||||||
|
var path = currentDir.path.replaceFirst(currentHome, "");
|
||||||
|
if (path.length == 0) return "";
|
||||||
|
if (path[0] == "/" || path[0] == "\\") {
|
||||||
|
// remove more '/' or '\'
|
||||||
|
path = path.replaceFirst(path[0], "");
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return currentDir.path.replaceFirst(currentHome, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
|
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
|
||||||
|
|
||||||
|
String getCurrentHome(bool isLocal) {
|
||||||
|
return isLocal ? _localOption.home : _remoteOption.home;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getJob(int id) {
|
||||||
|
return jobTable.indexWhere((element) => element.id == id);
|
||||||
|
}
|
||||||
|
|
||||||
String get currentShortPath {
|
String get currentShortPath {
|
||||||
if (currentDir.path.startsWith(currentHome)) {
|
if (currentDir.path.startsWith(currentHome)) {
|
||||||
var path = currentDir.path.replaceFirst(currentHome, "");
|
var path = currentDir.path.replaceFirst(currentHome, "");
|
||||||
@@ -59,16 +108,43 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String shortPath(bool isLocal) {
|
||||||
|
final dir = isLocal ? currentLocalDir : currentRemoteDir;
|
||||||
|
if (dir.path.startsWith(currentHome)) {
|
||||||
|
var path = dir.path.replaceFirst(currentHome, "");
|
||||||
|
if (path.length == 0) return "";
|
||||||
|
if (path[0] == "/" || path[0] == "\\") {
|
||||||
|
// remove more '/' or '\'
|
||||||
|
path = path.replaceFirst(path[0], "");
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return dir.path.replaceFirst(currentHome, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool get currentShowHidden =>
|
bool get currentShowHidden =>
|
||||||
_isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
_isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
|
||||||
|
bool getCurrentShowHidden(bool isLocal) {
|
||||||
|
return isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
}
|
||||||
|
|
||||||
bool get currentIsWindows =>
|
bool get currentIsWindows =>
|
||||||
_isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
_isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
|
||||||
|
bool getCurrentIsWindows(bool isLocal) {
|
||||||
|
return isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
}
|
||||||
|
|
||||||
final _fileFetcher = FileFetcher();
|
final _fileFetcher = FileFetcher();
|
||||||
|
|
||||||
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
|
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||||
|
|
||||||
|
final WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
FileModel(this.parent);
|
||||||
|
|
||||||
toggleSelectMode() {
|
toggleSelectMode() {
|
||||||
if (jobState == JobState.inProgress) {
|
if (jobState == JobState.inProgress) {
|
||||||
return;
|
return;
|
||||||
@@ -89,16 +165,29 @@ class FileModel extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
_remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden;
|
_remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden;
|
||||||
}
|
}
|
||||||
refresh();
|
refresh(isLocal: local);
|
||||||
}
|
}
|
||||||
|
|
||||||
tryUpdateJobProgress(Map<String, dynamic> evt) {
|
tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
int id = int.parse(evt['id']);
|
int id = int.parse(evt['id']);
|
||||||
_jobProgress.id = id;
|
if (!isDesktop) {
|
||||||
_jobProgress.fileNum = int.parse(evt['file_num']);
|
_jobProgress.id = id;
|
||||||
_jobProgress.speed = double.parse(evt['speed']);
|
_jobProgress.fileNum = int.parse(evt['file_num']);
|
||||||
_jobProgress.finishedSize = int.parse(evt['finished_size']);
|
_jobProgress.speed = double.parse(evt['speed']);
|
||||||
|
_jobProgress.finishedSize = int.parse(evt['finished_size']);
|
||||||
|
} else {
|
||||||
|
// Desktop uses jobTable
|
||||||
|
// id = index + 1
|
||||||
|
final jobIndex = getJob(id);
|
||||||
|
if (jobIndex >= 0 && _jobTable.length > jobIndex) {
|
||||||
|
final job = _jobTable[jobIndex];
|
||||||
|
job.fileNum = int.parse(evt['file_num']);
|
||||||
|
job.speed = double.parse(evt['speed']);
|
||||||
|
job.finishedSize = int.parse(evt['finished_size']);
|
||||||
|
debugPrint("update job ${id} with ${evt}");
|
||||||
|
}
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
|
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
|
||||||
@@ -106,63 +195,107 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiveFileDir(Map<String, dynamic> evt) {
|
receiveFileDir(Map<String, dynamic> evt) {
|
||||||
if (_remoteOption.home.isEmpty && evt['is_local'] == "false") {
|
debugPrint("recv file dir:${evt}");
|
||||||
|
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 {
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
||||||
fd.format(_remoteOption.isWindows, sort: _sortStyle);
|
fd.format(_remoteOption.isWindows, sort: _sortStyle);
|
||||||
_remoteOption.home = fd.path;
|
if (fd.id > 0) {
|
||||||
debugPrint("init remote home:${fd.path}");
|
final jobIndex = getJob(fd.id);
|
||||||
_currentRemoteDir = fd;
|
if (jobIndex != -1) {
|
||||||
notifyListeners();
|
final job = jobTable[jobIndex];
|
||||||
return;
|
var totalSize = 0;
|
||||||
|
var fileCount = fd.entries.length;
|
||||||
|
fd.entries.forEach((element) {
|
||||||
|
totalSize += element.size;
|
||||||
|
});
|
||||||
|
job.totalSize = totalSize;
|
||||||
|
job.fileCount = fileCount;
|
||||||
|
debugPrint("update receive details:${fd.path}");
|
||||||
|
}
|
||||||
|
} else if (_remoteOption.home.isEmpty) {
|
||||||
|
_remoteOption.home = fd.path;
|
||||||
|
debugPrint("init remote home:${fd.path}");
|
||||||
|
_currentRemoteDir = fd;
|
||||||
|
}
|
||||||
} finally {}
|
} finally {}
|
||||||
}
|
}
|
||||||
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
jobDone(Map<String, dynamic> evt) {
|
jobDone(Map<String, dynamic> evt) async {
|
||||||
if (_jobResultListener.isListening) {
|
if (_jobResultListener.isListening) {
|
||||||
_jobResultListener.complete(evt);
|
_jobResultListener.complete(evt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_selectMode = false;
|
if (!isDesktop) {
|
||||||
_jobProgress.state = JobState.done;
|
_selectMode = false;
|
||||||
refresh();
|
_jobProgress.state = JobState.done;
|
||||||
|
} else {
|
||||||
|
int id = int.parse(evt['id']);
|
||||||
|
final jobIndex = getJob(id);
|
||||||
|
if (jobIndex != -1) {
|
||||||
|
final job = jobTable[jobIndex];
|
||||||
|
job.finishedSize = job.totalSize;
|
||||||
|
job.state = JobState.done;
|
||||||
|
job.fileNum = int.parse(evt['file_num']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Future.wait([
|
||||||
|
refresh(isLocal: false),
|
||||||
|
refresh(isLocal: true),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
jobError(Map<String, dynamic> evt) {
|
jobError(Map<String, dynamic> evt) {
|
||||||
if (_jobResultListener.isListening) {
|
if (!isDesktop) {
|
||||||
_jobResultListener.complete(evt);
|
if (_jobResultListener.isListening) {
|
||||||
return;
|
_jobResultListener.complete(evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectMode = false;
|
||||||
|
_jobProgress.clear();
|
||||||
|
_jobProgress.state = JobState.error;
|
||||||
|
} else {
|
||||||
|
int jobIndex = getJob(int.parse(evt['id']));
|
||||||
|
if (jobIndex != -1) {
|
||||||
|
final job = jobTable[jobIndex];
|
||||||
|
job.state = JobState.error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint("jobError $evt");
|
debugPrint("jobError $evt");
|
||||||
_selectMode = false;
|
|
||||||
_jobProgress.clear();
|
|
||||||
_jobProgress.state = JobState.error;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideFileConfirm(Map<String, dynamic> evt) async {
|
overrideFileConfirm(Map<String, dynamic> evt) async {
|
||||||
final resp = await showFileConfirmDialog(
|
final resp = await showFileConfirmDialog(
|
||||||
translate("Overwrite"), "${evt['read_path']}", true);
|
translate("Overwrite"), "${evt['read_path']}", true);
|
||||||
|
final id = int.tryParse(evt['id']) ?? 0;
|
||||||
if (false == resp) {
|
if (false == resp) {
|
||||||
cancelJob(int.tryParse(evt['id']) ?? 0);
|
final jobIndex = getJob(id);
|
||||||
|
if (jobIndex != -1) {
|
||||||
|
cancelJob(id);
|
||||||
|
final job = jobTable[jobIndex];
|
||||||
|
job.state = JobState.done;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var msg = Map()
|
var need_override = false;
|
||||||
..['id'] = evt['id']
|
|
||||||
..['file_num'] = evt['file_num']
|
|
||||||
..['is_upload'] = evt['is_upload']
|
|
||||||
..['remember'] = fileConfirmCheckboxRemember.toString();
|
|
||||||
if (resp == null) {
|
if (resp == null) {
|
||||||
// skip
|
// skip
|
||||||
msg['need_override'] = 'false';
|
need_override = false;
|
||||||
} else {
|
} else {
|
||||||
// overwrite
|
// overwrite
|
||||||
msg['need_override'] = 'true';
|
need_override = true;
|
||||||
}
|
}
|
||||||
FFI.setByName("set_confirm_override_file", jsonEncode(msg));
|
bind.sessionSetConfirmOverrideFile(
|
||||||
|
id: parent.target?.id ?? "",
|
||||||
|
actId: id,
|
||||||
|
fileNum: int.parse(evt['file_num']),
|
||||||
|
needOverride: need_override,
|
||||||
|
remember: fileConfirmCheckboxRemember,
|
||||||
|
isUpload: evt['is_upload'] == "true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,20 +305,24 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onReady() async {
|
onReady() async {
|
||||||
_localOption.home = FFI.getByName("get_home_dir");
|
_localOption.home = await bind.mainGetHomeDir();
|
||||||
_localOption.showHidden =
|
_localOption.showHidden = (await bind.sessionGetPeerOption(
|
||||||
FFI.getByName("peer_option", "local_show_hidden").isNotEmpty;
|
id: parent.target?.id ?? "", name: "local_show_hidden"))
|
||||||
|
.isNotEmpty;
|
||||||
|
|
||||||
_remoteOption.showHidden =
|
_remoteOption.showHidden = (await bind.sessionGetPeerOption(
|
||||||
FFI.getByName("peer_option", "remote_show_hidden").isNotEmpty;
|
id: parent.target?.id ?? "", name: "remote_show_hidden"))
|
||||||
_remoteOption.isWindows = FFI.ffiModel.pi.platform == "Windows";
|
.isNotEmpty;
|
||||||
|
_remoteOption.isWindows = parent.target?.ffiModel.pi.platform == "Windows";
|
||||||
|
|
||||||
debugPrint("remote platform: ${FFI.ffiModel.pi.platform}");
|
debugPrint("remote platform: ${parent.target?.ffiModel.pi.platform}");
|
||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
final local = FFI.getByName("peer_option", "local_dir");
|
final local = (await bind.sessionGetPeerOption(
|
||||||
final remote = FFI.getByName("peer_option", "remote_dir");
|
id: parent.target?.id ?? "", name: "local_dir"));
|
||||||
|
final remote = (await bind.sessionGetPeerOption(
|
||||||
|
id: parent.target?.id ?? "", name: "remote_dir"));
|
||||||
openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true);
|
openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true);
|
||||||
openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false);
|
openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false);
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
@@ -195,38 +332,40 @@ class FileModel extends ChangeNotifier {
|
|||||||
if (_currentRemoteDir.path.isEmpty) {
|
if (_currentRemoteDir.path.isEmpty) {
|
||||||
openDirectory(_remoteOption.home, isLocal: false);
|
openDirectory(_remoteOption.home, isLocal: false);
|
||||||
}
|
}
|
||||||
|
// load last transfer jobs
|
||||||
|
await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}');
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
SmartDialog.dismiss();
|
parent.target?.dialogManager.dismissAll();
|
||||||
jobReset();
|
jobReset();
|
||||||
|
|
||||||
// save config
|
// save config
|
||||||
Map<String, String> msg = Map();
|
Map<String, String> msgMap = Map();
|
||||||
|
|
||||||
msg["name"] = "local_dir";
|
msgMap["local_dir"] = _currentLocalDir.path;
|
||||||
msg["value"] = _currentLocalDir.path;
|
msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : "";
|
||||||
FFI.setByName('peer_option', jsonEncode(msg));
|
msgMap["remote_dir"] = _currentRemoteDir.path;
|
||||||
|
msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : "";
|
||||||
msg["name"] = "local_show_hidden";
|
final id = parent.target?.id ?? "";
|
||||||
msg["value"] = _localOption.showHidden ? "Y" : "";
|
for (final msg in msgMap.entries) {
|
||||||
FFI.setByName('peer_option', jsonEncode(msg));
|
bind.sessionPeerOption(id: id, name: msg.key, value: msg.value);
|
||||||
|
}
|
||||||
msg["name"] = "remote_dir";
|
|
||||||
msg["value"] = _currentRemoteDir.path;
|
|
||||||
FFI.setByName('peer_option', jsonEncode(msg));
|
|
||||||
|
|
||||||
msg["name"] = "remote_show_hidden";
|
|
||||||
msg["value"] = _remoteOption.showHidden ? "Y" : "";
|
|
||||||
FFI.setByName('peer_option', jsonEncode(msg));
|
|
||||||
_currentLocalDir.clear();
|
_currentLocalDir.clear();
|
||||||
_currentRemoteDir.clear();
|
_currentRemoteDir.clear();
|
||||||
_localOption.clear();
|
_localOption.clear();
|
||||||
_remoteOption.clear();
|
_remoteOption.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
Future refresh({bool? isLocal}) async {
|
||||||
openDirectory(currentDir.path);
|
if (isDesktop) {
|
||||||
|
isLocal = isLocal ?? _isLocal;
|
||||||
|
await isLocal
|
||||||
|
? openDirectory(currentLocalDir.path, isLocal: isLocal)
|
||||||
|
: openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
||||||
|
} else {
|
||||||
|
await openDirectory(currentDir.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openDirectory(String path, {bool? isLocal}) async {
|
openDirectory(String path, {bool? isLocal}) async {
|
||||||
@@ -235,6 +374,15 @@ class FileModel extends ChangeNotifier {
|
|||||||
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
final isWindows =
|
final isWindows =
|
||||||
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
// process /C:\ -> C:\ on Windows
|
||||||
|
if (isLocal
|
||||||
|
? _localOption.isWindows
|
||||||
|
: _remoteOption.isWindows && path.length > 1 && path[0] == '/') {
|
||||||
|
path = path.substring(1);
|
||||||
|
if (path[path.length - 1] != '\\') {
|
||||||
|
path = path + "\\";
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
||||||
fd.format(isWindows, sort: _sortStyle);
|
fd.format(isWindows, sort: _sortStyle);
|
||||||
@@ -245,48 +393,87 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to openDirectory :$e");
|
debugPrint("Failed to openDirectory ${path} :$e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
goHome() {
|
goHome({bool? isLocal}) {
|
||||||
openDirectory(currentHome);
|
isLocal = isLocal ?? _isLocal;
|
||||||
|
openDirectory(getCurrentHome(isLocal), isLocal: isLocal);
|
||||||
}
|
}
|
||||||
|
|
||||||
goToParentDirectory() {
|
goToParentDirectory({bool? isLocal}) {
|
||||||
final parent = PathUtil.dirname(currentDir.path, currentIsWindows);
|
isLocal = isLocal ?? _isLocal;
|
||||||
openDirectory(parent);
|
final isWindows =
|
||||||
}
|
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
final currDir = isLocal ? currentLocalDir : currentRemoteDir;
|
||||||
sendFiles(SelectedItems items) {
|
var parent = PathUtil.dirname(currDir.path, isWindows);
|
||||||
if (items.isLocal == null) {
|
// specially for C:\, D:\, goto '/'
|
||||||
debugPrint("Failed to sendFiles ,wrong path state");
|
if (parent == currDir.path && isWindows) {
|
||||||
|
openDirectory('/', isLocal: isLocal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_jobProgress.state = JobState.inProgress;
|
openDirectory(parent, isLocal: isLocal);
|
||||||
final toPath =
|
}
|
||||||
items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
|
|
||||||
final isWindows =
|
/// isRemote only for desktop now, [isRemote == true] means [remote -> local]
|
||||||
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
|
sendFiles(SelectedItems items, {bool isRemote = false}) {
|
||||||
final showHidden =
|
if (isDesktop) {
|
||||||
items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden;
|
// desktop sendFiles
|
||||||
items.items.forEach((from) {
|
final toPath = isRemote ? currentLocalDir.path : currentRemoteDir.path;
|
||||||
_jobId++;
|
final isWindows =
|
||||||
final msg = {
|
isRemote ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
"id": _jobId.toString(),
|
final showHidden =
|
||||||
"path": from.path,
|
isRemote ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
"to": PathUtil.join(toPath, from.name, isWindows),
|
items.items.forEach((from) async {
|
||||||
"file_num": "0",
|
final jobId = ++_jobId;
|
||||||
"show_hidden": showHidden.toString(),
|
_jobTable.add(JobProgress()
|
||||||
"is_remote": (!(items.isLocal!)).toString()
|
..jobName = from.path
|
||||||
};
|
..totalSize = from.size
|
||||||
FFI.setByName("send_files", jsonEncode(msg));
|
..state = JobState.inProgress
|
||||||
});
|
..id = jobId
|
||||||
|
..isRemote = isRemote);
|
||||||
|
bind.sessionSendFiles(
|
||||||
|
id: '${parent.target?.id}',
|
||||||
|
actId: _jobId,
|
||||||
|
path: from.path,
|
||||||
|
to: PathUtil.join(toPath, from.name, isWindows),
|
||||||
|
fileNum: 0,
|
||||||
|
includeHidden: showHidden,
|
||||||
|
isRemote: isRemote);
|
||||||
|
print(
|
||||||
|
"path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (items.isLocal == null) {
|
||||||
|
debugPrint("Failed to sendFiles ,wrong path state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_jobProgress.state = JobState.inProgress;
|
||||||
|
final toPath =
|
||||||
|
items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
|
||||||
|
final isWindows =
|
||||||
|
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
final showHidden =
|
||||||
|
items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
items.items.forEach((from) async {
|
||||||
|
_jobId++;
|
||||||
|
await bind.sessionSendFiles(
|
||||||
|
id: await bind.mainGetLastRemoteId(),
|
||||||
|
actId: _jobId,
|
||||||
|
path: from.path,
|
||||||
|
to: PathUtil.join(toPath, from.name, isWindows),
|
||||||
|
fileNum: 0,
|
||||||
|
includeHidden: showHidden,
|
||||||
|
isRemote: !(items.isLocal!));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool removeCheckboxRemember = false;
|
bool removeCheckboxRemember = false;
|
||||||
|
|
||||||
removeAction(SelectedItems items) async {
|
removeAction(SelectedItems items, {bool? isLocal}) async {
|
||||||
|
isLocal = isLocal ?? _isLocal;
|
||||||
removeCheckboxRemember = false;
|
removeCheckboxRemember = false;
|
||||||
if (items.isLocal == null) {
|
if (items.isLocal == null) {
|
||||||
debugPrint("Failed to removeFile, wrong path state");
|
debugPrint("Failed to removeFile, wrong path state");
|
||||||
@@ -305,14 +492,14 @@ class FileModel extends ChangeNotifier {
|
|||||||
entries = [item];
|
entries = [item];
|
||||||
} else if (item.isDirectory) {
|
} else if (item.isDirectory) {
|
||||||
title = translate("Not an empty directory");
|
title = translate("Not an empty directory");
|
||||||
showLoading(translate("Waiting"));
|
parent.target?.dialogManager.showLoading(translate("Waiting"));
|
||||||
final fd = await _fileFetcher.fetchDirectoryRecursive(
|
final fd = await _fileFetcher.fetchDirectoryRecursive(
|
||||||
_jobId, item.path, items.isLocal!, true);
|
_jobId, item.path, items.isLocal!, true);
|
||||||
if (fd.path.isEmpty) {
|
if (fd.path.isEmpty) {
|
||||||
fd.path = item.path;
|
fd.path = item.path;
|
||||||
}
|
}
|
||||||
fd.format(isWindows);
|
fd.format(isWindows);
|
||||||
SmartDialog.dismiss();
|
parent.target?.dialogManager.dismissAll();
|
||||||
if (fd.entries.isEmpty) {
|
if (fd.entries.isEmpty) {
|
||||||
final confirm = await showRemoveDialog(
|
final confirm = await showRemoveDialog(
|
||||||
translate(
|
translate(
|
||||||
@@ -360,16 +547,18 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
print("remove error: ${e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_selectMode = false;
|
_selectMode = false;
|
||||||
refresh();
|
refresh(isLocal: isLocal);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> showRemoveDialog(
|
Future<bool?> showRemoveDialog(
|
||||||
String title, String content, bool showCheckbox) async {
|
String title, String content, bool showCheckbox) async {
|
||||||
return await DialogManager.show<bool>(
|
return await parent.target?.dialogManager.show<bool>(
|
||||||
(setState, Function(bool v) close) => CustomAlertDialog(
|
(setState, Function(bool v) close) => CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -420,7 +609,7 @@ class FileModel extends ChangeNotifier {
|
|||||||
Future<bool?> showFileConfirmDialog(
|
Future<bool?> showFileConfirmDialog(
|
||||||
String title, String content, bool showCheckbox) async {
|
String title, String content, bool showCheckbox) async {
|
||||||
fileConfirmCheckboxRemember = false;
|
fileConfirmCheckboxRemember = false;
|
||||||
return await DialogManager.show<bool?>(
|
return await parent.target?.dialogManager.show<bool?>(
|
||||||
(setState, Function(bool? v) close) => CustomAlertDialog(
|
(setState, Function(bool? v) close) => CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -473,43 +662,122 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
||||||
final msg = {
|
bind.sessionRemoveFile(
|
||||||
"id": _jobId.toString(),
|
id: '${parent.target?.id}',
|
||||||
"path": path,
|
actId: _jobId,
|
||||||
"file_num": fileNum.toString(),
|
path: path,
|
||||||
"is_remote": (!(isLocal)).toString()
|
isRemote: !isLocal,
|
||||||
};
|
fileNum: fileNum);
|
||||||
FFI.setByName("remove_file", jsonEncode(msg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
|
sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
|
||||||
final msg = {
|
bind.sessionRemoveAllEmptyDirs(
|
||||||
"id": _jobId.toString(),
|
id: '${parent.target?.id}',
|
||||||
"path": path,
|
actId: _jobId,
|
||||||
"is_remote": (!isLocal).toString()
|
path: path,
|
||||||
};
|
isRemote: !isLocal);
|
||||||
FFI.setByName("remove_all_empty_dirs", jsonEncode(msg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createDir(String path) {
|
createDir(String path, {bool? isLocal}) async {
|
||||||
|
isLocal = isLocal ?? this.isLocal;
|
||||||
_jobId++;
|
_jobId++;
|
||||||
final msg = {
|
bind.sessionCreateDir(
|
||||||
"id": _jobId.toString(),
|
id: '${parent.target?.id}',
|
||||||
"path": path,
|
actId: _jobId,
|
||||||
"is_remote": (!isLocal).toString()
|
path: path,
|
||||||
};
|
isRemote: !isLocal);
|
||||||
FFI.setByName("create_dir", jsonEncode(msg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelJob(int id) {
|
cancelJob(int id) async {
|
||||||
FFI.setByName("cancel_job", id.toString());
|
bind.sessionCancelJob(id: '${parent.target?.id}', actId: id);
|
||||||
jobReset();
|
jobReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
changeSortStyle(SortBy sort) {
|
changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) {
|
||||||
_sortStyle = sort;
|
_sortStyle = sort;
|
||||||
_currentLocalDir.changeSortStyle(sort);
|
if (isLocal == null) {
|
||||||
_currentRemoteDir.changeSortStyle(sort);
|
// compatible for mobile logic
|
||||||
|
_currentLocalDir.changeSortStyle(sort, ascending: ascending);
|
||||||
|
_currentRemoteDir.changeSortStyle(sort, ascending: ascending);
|
||||||
|
_localSortStyle = sort;
|
||||||
|
_localSortAscending = ascending;
|
||||||
|
_remoteSortStyle = sort;
|
||||||
|
_remoteSortAscending = ascending;
|
||||||
|
} else if (isLocal) {
|
||||||
|
_currentLocalDir.changeSortStyle(sort, ascending: ascending);
|
||||||
|
_localSortStyle = sort;
|
||||||
|
_localSortAscending = ascending;
|
||||||
|
} else {
|
||||||
|
_currentRemoteDir.changeSortStyle(sort, ascending: ascending);
|
||||||
|
_remoteSortStyle = sort;
|
||||||
|
_remoteSortAscending = ascending;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
initFileFetcher() {
|
||||||
|
_fileFetcher.id = parent.target?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFolderFiles(Map<String, dynamic> evt) {
|
||||||
|
// ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}"
|
||||||
|
Map<String, dynamic> info = json.decode(evt['info']);
|
||||||
|
int id = info['id'];
|
||||||
|
int num_entries = info['num_entries'];
|
||||||
|
double total_size = info['total_size'];
|
||||||
|
final jobIndex = getJob(id);
|
||||||
|
if (jobIndex != -1) {
|
||||||
|
final job = jobTable[jobIndex];
|
||||||
|
job.fileCount = num_entries;
|
||||||
|
job.totalSize = total_size.toInt();
|
||||||
|
}
|
||||||
|
debugPrint("update folder files: ${info}");
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get remoteSortAscending => _remoteSortAscending;
|
||||||
|
|
||||||
|
void loadLastJob(Map<String, dynamic> evt) {
|
||||||
|
debugPrint("load last job: ${evt}");
|
||||||
|
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
||||||
|
// int id = int.parse(jobDetail['id']);
|
||||||
|
String remote = jobDetail['remote'];
|
||||||
|
String to = jobDetail['to'];
|
||||||
|
bool showHidden = jobDetail['show_hidden'];
|
||||||
|
int fileNum = jobDetail['file_num'];
|
||||||
|
bool isRemote = jobDetail['is_remote'];
|
||||||
|
final currJobId = _jobId++;
|
||||||
|
var jobProgress = JobProgress()
|
||||||
|
..jobName = isRemote ? remote : to
|
||||||
|
..id = currJobId
|
||||||
|
..isRemote = isRemote
|
||||||
|
..fileNum = fileNum
|
||||||
|
..remote = remote
|
||||||
|
..to = to
|
||||||
|
..showHidden = showHidden
|
||||||
|
..state = JobState.paused;
|
||||||
|
jobTable.add(jobProgress);
|
||||||
|
bind.sessionAddJob(
|
||||||
|
id: '${parent.target?.id}',
|
||||||
|
isRemote: isRemote,
|
||||||
|
includeHidden: showHidden,
|
||||||
|
actId: currJobId,
|
||||||
|
path: isRemote ? remote : to,
|
||||||
|
to: isRemote ? to : remote,
|
||||||
|
fileNum: fileNum,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeJob(int jobId) {
|
||||||
|
final jobIndex = getJob(jobId);
|
||||||
|
if (jobIndex != -1) {
|
||||||
|
final job = jobTable[jobIndex];
|
||||||
|
bind.sessionResumeJob(
|
||||||
|
id: '${parent.target?.id}', actId: job.id, isRemote: job.isRemote);
|
||||||
|
job.state = JobState.inProgress;
|
||||||
|
} else {
|
||||||
|
debugPrint("jobId ${jobId} is not exists");
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,6 +827,17 @@ class FileFetcher {
|
|||||||
Map<String, Completer<FileDirectory>> remoteTasks = Map();
|
Map<String, Completer<FileDirectory>> remoteTasks = Map();
|
||||||
Map<int, Completer<FileDirectory>> readRecursiveTasks = Map();
|
Map<int, Completer<FileDirectory>> readRecursiveTasks = Map();
|
||||||
|
|
||||||
|
String? _id;
|
||||||
|
|
||||||
|
String? get id => _id;
|
||||||
|
|
||||||
|
set id(String? id) {
|
||||||
|
_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if id == null, means to fetch global FFI
|
||||||
|
FFI get _ffi => ffi(_id ?? "");
|
||||||
|
|
||||||
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
|
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
|
||||||
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
||||||
final tasks = remoteTasks; // bypass now
|
final tasks = remoteTasks; // bypass now
|
||||||
@@ -618,13 +897,14 @@ class FileFetcher {
|
|||||||
Future<FileDirectory> fetchDirectory(
|
Future<FileDirectory> fetchDirectory(
|
||||||
String path, bool isLocal, bool showHidden) async {
|
String path, bool isLocal, bool showHidden) async {
|
||||||
try {
|
try {
|
||||||
final msg = {"path": path, "show_hidden": showHidden.toString()};
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
final res = FFI.getByName("read_local_dir_sync", jsonEncode(msg));
|
final res = await bind.sessionReadLocalDirSync(
|
||||||
|
id: id ?? "", path: path, showHidden: showHidden);
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(res));
|
final fd = FileDirectory.fromJson(jsonDecode(res));
|
||||||
return fd;
|
return fd;
|
||||||
} else {
|
} else {
|
||||||
FFI.setByName("read_remote_dir", jsonEncode(msg));
|
await bind.sessionReadRemoteDir(
|
||||||
|
id: id ?? "", path: path, includeHidden: showHidden);
|
||||||
return registerReadTask(isLocal, path);
|
return registerReadTask(isLocal, path);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -636,13 +916,12 @@ class FileFetcher {
|
|||||||
int id, String path, bool isLocal, bool showHidden) async {
|
int id, String path, bool isLocal, bool showHidden) async {
|
||||||
// TODO test Recursive is show hidden default?
|
// TODO test Recursive is show hidden default?
|
||||||
try {
|
try {
|
||||||
final msg = {
|
await bind.sessionReadDirRecursive(
|
||||||
"id": id.toString(),
|
id: _ffi.id,
|
||||||
"path": path,
|
actId: id,
|
||||||
"show_hidden": showHidden.toString(),
|
path: path,
|
||||||
"is_remote": (!isLocal).toString()
|
isRemote: !isLocal,
|
||||||
};
|
showHidden: showHidden);
|
||||||
FFI.setByName("read_dir_recursive", jsonEncode(msg));
|
|
||||||
return registerReadRecursiveTask(id);
|
return registerReadRecursiveTask(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Future.error(e);
|
return Future.error(e);
|
||||||
@@ -675,8 +954,8 @@ class FileDirectory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeSortStyle(SortBy sort) {
|
changeSortStyle(SortBy sort, {bool ascending = true}) {
|
||||||
entries = _sortList(entries, sort);
|
entries = _sortList(entries, sort, ascending);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@@ -711,7 +990,24 @@ class Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum JobState { none, inProgress, done, error }
|
enum JobState { none, inProgress, done, error, paused }
|
||||||
|
|
||||||
|
extension JobStateDisplay on JobState {
|
||||||
|
String display() {
|
||||||
|
switch (this) {
|
||||||
|
case JobState.none:
|
||||||
|
return translate("Waiting");
|
||||||
|
case JobState.inProgress:
|
||||||
|
return translate("Transfer File");
|
||||||
|
case JobState.done:
|
||||||
|
return translate("Finished");
|
||||||
|
case JobState.error:
|
||||||
|
return translate("Error");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class JobProgress {
|
class JobProgress {
|
||||||
JobState state = JobState.none;
|
JobState state = JobState.none;
|
||||||
@@ -719,6 +1015,13 @@ class JobProgress {
|
|||||||
var fileNum = 0;
|
var fileNum = 0;
|
||||||
var speed = 0.0;
|
var speed = 0.0;
|
||||||
var finishedSize = 0;
|
var finishedSize = 0;
|
||||||
|
var totalSize = 0;
|
||||||
|
var fileCount = 0;
|
||||||
|
var isRemote = false;
|
||||||
|
var jobName = "";
|
||||||
|
var remote = "";
|
||||||
|
var to = "";
|
||||||
|
var showHidden = false;
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
state = JobState.none;
|
state = JobState.none;
|
||||||
@@ -726,6 +1029,10 @@ class JobProgress {
|
|||||||
fileNum = 0;
|
fileNum = 0;
|
||||||
speed = 0;
|
speed = 0;
|
||||||
finishedSize = 0;
|
finishedSize = 0;
|
||||||
|
jobName = "";
|
||||||
|
fileCount = 0;
|
||||||
|
remote = "";
|
||||||
|
to = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,7 +1079,7 @@ class DirectoryOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// code from file_manager pkg after edit
|
// code from file_manager pkg after edit
|
||||||
List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
List<Entry> _sortList(List<Entry> list, SortBy sortType, bool ascending) {
|
||||||
if (sortType == SortBy.Name) {
|
if (sortType == SortBy.Name) {
|
||||||
// making list of only folders.
|
// making list of only folders.
|
||||||
final dirs = list.where((element) => element.isDirectory).toList();
|
final dirs = list.where((element) => element.isDirectory).toList();
|
||||||
@@ -785,7 +1092,9 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
|||||||
files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
// first folders will go to list (if available) then files will go to list.
|
// first folders will go to list (if available) then files will go to list.
|
||||||
return [...dirs, ...files];
|
return ascending
|
||||||
|
? [...dirs, ...files]
|
||||||
|
: [...dirs.reversed.toList(), ...files.reversed.toList()];
|
||||||
} else if (sortType == SortBy.Modified) {
|
} else if (sortType == SortBy.Modified) {
|
||||||
// making the list of Path & DateTime
|
// making the list of Path & DateTime
|
||||||
List<_PathStat> _pathStat = [];
|
List<_PathStat> _pathStat = [];
|
||||||
@@ -800,7 +1109,7 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
|||||||
list.sort((a, b) => _pathStat
|
list.sort((a, b) => _pathStat
|
||||||
.indexWhere((element) => element.path == a.name)
|
.indexWhere((element) => element.path == a.name)
|
||||||
.compareTo(_pathStat.indexWhere((element) => element.path == b.name)));
|
.compareTo(_pathStat.indexWhere((element) => element.path == b.name)));
|
||||||
return list;
|
return ascending ? list : list.reversed.toList();
|
||||||
} else if (sortType == SortBy.Type) {
|
} else if (sortType == SortBy.Type) {
|
||||||
// making list of only folders.
|
// making list of only folders.
|
||||||
final dirs = list.where((element) => element.isDirectory).toList();
|
final dirs = list.where((element) => element.isDirectory).toList();
|
||||||
@@ -817,7 +1126,9 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
|||||||
.split('.')
|
.split('.')
|
||||||
.last
|
.last
|
||||||
.compareTo(b.name.toLowerCase().split('.').last));
|
.compareTo(b.name.toLowerCase().split('.').last));
|
||||||
return [...dirs, ...files];
|
return ascending
|
||||||
|
? [...dirs, ...files]
|
||||||
|
: [...dirs.reversed.toList(), ...files.reversed.toList()];
|
||||||
} else if (sortType == SortBy.Size) {
|
} else if (sortType == SortBy.Size) {
|
||||||
// create list of path and size
|
// create list of path and size
|
||||||
Map<String, int> _sizeMap = {};
|
Map<String, int> _sizeMap = {};
|
||||||
@@ -842,7 +1153,9 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
|||||||
.indexWhere((element) => element.key == a.name)
|
.indexWhere((element) => element.key == a.name)
|
||||||
.compareTo(
|
.compareTo(
|
||||||
_sizeMapList.indexWhere((element) => element.key == b.name)));
|
_sizeMapList.indexWhere((element) => element.key == b.name)));
|
||||||
return [...dirs, ...files];
|
return ascending
|
||||||
|
? [...dirs, ...files]
|
||||||
|
: [...dirs.reversed.toList(), ...files.reversed.toList()];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ffi';
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:device_info/device_info.dart';
|
|
||||||
import 'package:package_info/package_info.dart';
|
|
||||||
import 'package:external_path/external_path.dart';
|
import 'package:external_path/external_path.dart';
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../generated_bridge.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
|
import '../generated_bridge.dart';
|
||||||
|
|
||||||
class RgbaFrame extends Struct {
|
class RgbaFrame extends Struct {
|
||||||
@Uint32()
|
@Uint32()
|
||||||
@@ -19,58 +22,100 @@ class RgbaFrame extends Struct {
|
|||||||
|
|
||||||
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
|
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
|
||||||
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
|
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
|
||||||
|
typedef HandleEvent = void Function(Map<String, dynamic> evt);
|
||||||
|
|
||||||
|
/// FFI wrapper around the native Rust core.
|
||||||
|
/// Hides the platform differences.
|
||||||
class PlatformFFI {
|
class PlatformFFI {
|
||||||
static String _dir = '';
|
String _dir = '';
|
||||||
static String _homeDir = '';
|
String _homeDir = '';
|
||||||
static F2? _getByName;
|
F2? _translate;
|
||||||
static F3? _setByName;
|
var _eventHandlers = Map<String, Map<String, HandleEvent>>();
|
||||||
static void Function(Map<String, dynamic>)? _eventCallback;
|
late RustdeskImpl _ffiBind;
|
||||||
static void Function(Uint8List)? _rgbaCallback;
|
late String _appType;
|
||||||
|
void Function(Map<String, dynamic>)? _eventCallback;
|
||||||
|
|
||||||
|
PlatformFFI._();
|
||||||
|
|
||||||
|
static final PlatformFFI instance = PlatformFFI._();
|
||||||
|
final _toAndroidChannel = MethodChannel("mChannel");
|
||||||
|
|
||||||
|
RustdeskImpl get ffiBind => _ffiBind;
|
||||||
|
|
||||||
|
static get localeName => Platform.localeName;
|
||||||
|
|
||||||
static Future<String> getVersion() async {
|
static Future<String> getVersion() async {
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
return packageInfo.version;
|
return packageInfo.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getByName(String name, [String arg = '']) {
|
bool registerEventHandler(
|
||||||
if (_getByName == null) return '';
|
String event_name, String handler_name, HandleEvent handler) {
|
||||||
|
debugPrint('registerEventHandler $event_name $handler_name');
|
||||||
|
var handlers = _eventHandlers[event_name];
|
||||||
|
if (handlers == null) {
|
||||||
|
_eventHandlers[event_name] = {handler_name: handler};
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (handlers.containsKey(handler_name)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
handlers[handler_name] = handler;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterEventHandler(String event_name, String handler_name) {
|
||||||
|
debugPrint('unregisterEventHandler $event_name $handler_name');
|
||||||
|
var handlers = _eventHandlers[event_name];
|
||||||
|
if (handlers != null) {
|
||||||
|
handlers.remove(handler_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String translate(String name, String locale) {
|
||||||
|
if (_translate == null) return name;
|
||||||
var a = name.toNativeUtf8();
|
var a = name.toNativeUtf8();
|
||||||
var b = arg.toNativeUtf8();
|
var b = locale.toNativeUtf8();
|
||||||
var p = _getByName!(a, b);
|
var p = _translate!(a, b);
|
||||||
assert(p != nullptr);
|
assert(p != nullptr);
|
||||||
var res = p.toDartString();
|
final res = p.toDartString();
|
||||||
calloc.free(p);
|
calloc.free(p);
|
||||||
calloc.free(a);
|
calloc.free(a);
|
||||||
calloc.free(b);
|
calloc.free(b);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setByName(String name, [String value = '']) {
|
/// Init the FFI class, loads the native Rust core library.
|
||||||
if (_setByName == null) return;
|
Future<Null> init(String appType) async {
|
||||||
var a = name.toNativeUtf8();
|
_appType = appType;
|
||||||
var b = value.toNativeUtf8();
|
// if (isDesktop) {
|
||||||
_setByName!(a, b);
|
// // TODO
|
||||||
calloc.free(a);
|
// return;
|
||||||
calloc.free(b);
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
static Future<Null> init() async {
|
|
||||||
isIOS = Platform.isIOS;
|
|
||||||
isAndroid = Platform.isAndroid;
|
|
||||||
final dylib = Platform.isAndroid
|
final dylib = Platform.isAndroid
|
||||||
? DynamicLibrary.open('librustdesk.so')
|
? DynamicLibrary.open('librustdesk.so')
|
||||||
: DynamicLibrary.process();
|
: Platform.isLinux
|
||||||
print('initializing FFI');
|
? DynamicLibrary.open("/usr/lib/rustdesk/librustdesk.so")
|
||||||
|
: Platform.isWindows
|
||||||
|
? DynamicLibrary.open("librustdesk.dll")
|
||||||
|
: Platform.isMacOS
|
||||||
|
? DynamicLibrary.open("librustdesk.dylib")
|
||||||
|
: DynamicLibrary.process();
|
||||||
|
debugPrint('initializing FFI ${_appType}');
|
||||||
try {
|
try {
|
||||||
_getByName = dylib.lookupFunction<F2, F2>('get_by_name');
|
_translate = dylib.lookupFunction<F2, F2>('translate');
|
||||||
_setByName =
|
|
||||||
dylib.lookupFunction<Void Function(Pointer<Utf8>, Pointer<Utf8>), F3>(
|
|
||||||
'set_by_name');
|
|
||||||
_dir = (await getApplicationDocumentsDirectory()).path;
|
_dir = (await getApplicationDocumentsDirectory()).path;
|
||||||
_startListenEvent(RustdeskImpl(dylib));
|
_ffiBind = RustdeskImpl(dylib);
|
||||||
|
_startListenEvent(_ffiBind); // global event
|
||||||
try {
|
try {
|
||||||
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
|
if (isAndroid) {
|
||||||
|
// only support for android
|
||||||
|
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
|
||||||
|
} else {
|
||||||
|
_homeDir = (await getDownloadsDirectory())?.path ?? "";
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print(e);
|
||||||
}
|
}
|
||||||
@@ -81,71 +126,91 @@ class PlatformFFI {
|
|||||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||||
name = '${androidInfo.brand}-${androidInfo.model}';
|
name = '${androidInfo.brand}-${androidInfo.model}';
|
||||||
id = androidInfo.id.hashCode.toString();
|
id = androidInfo.id.hashCode.toString();
|
||||||
androidVersion = androidInfo.version.sdkInt;
|
androidVersion = androidInfo.version.sdkInt ?? 0;
|
||||||
} else {
|
} else if (Platform.isIOS) {
|
||||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||||
name = iosInfo.utsname.machine;
|
name = iosInfo.utsname.machine ?? "";
|
||||||
id = iosInfo.identifierForVendor.hashCode.toString();
|
id = iosInfo.identifierForVendor.hashCode.toString();
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
|
||||||
|
name = linuxInfo.name;
|
||||||
|
id = linuxInfo.machineId ?? linuxInfo.id;
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo;
|
||||||
|
name = winInfo.computerName;
|
||||||
|
id = winInfo.computerName;
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo;
|
||||||
|
name = macOsInfo.computerName;
|
||||||
|
id = macOsInfo.systemGUID ?? "";
|
||||||
}
|
}
|
||||||
print("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir");
|
print(
|
||||||
setByName('info1', id);
|
"_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir");
|
||||||
setByName('info2', name);
|
await _ffiBind.mainDeviceId(id: id);
|
||||||
setByName('home_dir', _homeDir);
|
await _ffiBind.mainDeviceName(name: name);
|
||||||
setByName('init', _dir);
|
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
||||||
|
await _ffiBind.mainInit(appDir: _dir);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print(e);
|
||||||
}
|
}
|
||||||
version = await getVersion();
|
version = await getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _startListenEvent(RustdeskImpl rustdeskImpl) {
|
bool _tryHandle(Map<String, dynamic> evt) {
|
||||||
() async {
|
final name = evt['name'];
|
||||||
await for (final message in rustdeskImpl.startEventStream()) {
|
if (name != null) {
|
||||||
if (_eventCallback != null) {
|
final handlers = _eventHandlers[name];
|
||||||
try {
|
if (handlers != null) {
|
||||||
Map<String, dynamic> event = json.decode(message);
|
if (handlers.isNotEmpty) {
|
||||||
_eventCallback!(event);
|
handlers.values.forEach((handler) {
|
||||||
} catch (e) {
|
handler(evt);
|
||||||
print('json.decode fail(): $e');
|
});
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}();
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start listening to the Rust core's events and frames.
|
||||||
|
void _startListenEvent(RustdeskImpl rustdeskImpl) {
|
||||||
() async {
|
() async {
|
||||||
await for (final rgba in rustdeskImpl.startRgbaStream()) {
|
await for (final message
|
||||||
if (_rgbaCallback != null) {
|
in rustdeskImpl.startGlobalEventStream(appType: _appType)) {
|
||||||
_rgbaCallback!(rgba);
|
try {
|
||||||
} else {
|
Map<String, dynamic> event = json.decode(message);
|
||||||
rgba.clear();
|
// _tryHandle here may be more flexible than _eventCallback
|
||||||
|
if (!_tryHandle(event)) {
|
||||||
|
if (_eventCallback != null) {
|
||||||
|
_eventCallback!(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('json.decode fail(): $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setEventCallback(void Function(Map<String, dynamic>) fun) async {
|
void setEventCallback(void Function(Map<String, dynamic>) fun) async {
|
||||||
_eventCallback = fun;
|
_eventCallback = fun;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setRgbaCallback(void Function(Uint8List) fun) async {
|
void setRgbaCallback(void Function(Uint8List) fun) async {}
|
||||||
_rgbaCallback = fun;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startDesktopWebListener() {}
|
void startDesktopWebListener() {}
|
||||||
|
|
||||||
static void stopDesktopWebListener() {}
|
void stopDesktopWebListener() {}
|
||||||
|
|
||||||
static void setMethodCallHandler(FMethod callback) {
|
void setMethodCallHandler(FMethod callback) {
|
||||||
toAndroidChannel.setMethodCallHandler((call) async {
|
_toAndroidChannel.setMethodCallHandler((call) async {
|
||||||
callback(call.method, call.arguments);
|
callback(call.method, call.arguments);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static invokeMethod(String method, [dynamic arguments]) async {
|
invokeMethod(String method, [dynamic arguments]) async {
|
||||||
if (!isAndroid) return Future<bool>(() => false);
|
if (!isAndroid) return Future<bool>(() => false);
|
||||||
return await toAndroidChannel.invokeMethod(method, arguments);
|
return await _toAndroidChannel.invokeMethod(method, arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final localeName = Platform.localeName;
|
|
||||||
final toAndroidChannel = MethodChannel("mChannel");
|
|
||||||
|
|||||||
128
flutter/lib/models/peer_model.dart
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'platform_model.dart';
|
||||||
|
|
||||||
|
class Peer {
|
||||||
|
final String id;
|
||||||
|
final String username;
|
||||||
|
final String hostname;
|
||||||
|
final String platform;
|
||||||
|
final List<dynamic> tags;
|
||||||
|
bool online = false;
|
||||||
|
|
||||||
|
Peer.fromJson(String id, Map<String, dynamic> json)
|
||||||
|
: id = id,
|
||||||
|
username = json['username'] ?? '',
|
||||||
|
hostname = json['hostname'] ?? '',
|
||||||
|
platform = json['platform'] ?? '',
|
||||||
|
tags = json['tags'] ?? [];
|
||||||
|
|
||||||
|
Peer({
|
||||||
|
required this.id,
|
||||||
|
required this.username,
|
||||||
|
required this.hostname,
|
||||||
|
required this.platform,
|
||||||
|
required this.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
Peer.loading()
|
||||||
|
: this(
|
||||||
|
id: '...',
|
||||||
|
username: '...',
|
||||||
|
hostname: '...',
|
||||||
|
platform: '...',
|
||||||
|
tags: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Peers extends ChangeNotifier {
|
||||||
|
late String _name;
|
||||||
|
late List<Peer> _peers;
|
||||||
|
late final _loadEvent;
|
||||||
|
static const _cbQueryOnlines = 'callback_query_onlines';
|
||||||
|
|
||||||
|
Peers(String name, String loadEvent, List<Peer> _initPeers) {
|
||||||
|
_name = name;
|
||||||
|
_loadEvent = loadEvent;
|
||||||
|
_peers = _initPeers;
|
||||||
|
platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) {
|
||||||
|
_updateOnlineState(evt);
|
||||||
|
});
|
||||||
|
platformFFI.registerEventHandler(_loadEvent, _name, (evt) {
|
||||||
|
_updatePeers(evt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Peer> get peers => _peers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
platformFFI.unregisterEventHandler(_cbQueryOnlines, _name);
|
||||||
|
platformFFI.unregisterEventHandler(_loadEvent, _name);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Peer getByIndex(int index) {
|
||||||
|
if (index < _peers.length) {
|
||||||
|
return _peers[index];
|
||||||
|
} else {
|
||||||
|
return Peer.loading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getPeersCount() {
|
||||||
|
return _peers.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||||
|
evt['onlines'].split(',').forEach((online) {
|
||||||
|
for (var i = 0; i < _peers.length; i++) {
|
||||||
|
if (_peers[i].id == online) {
|
||||||
|
_peers[i].online = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
evt['offlines'].split(',').forEach((offline) {
|
||||||
|
for (var i = 0; i < _peers.length; i++) {
|
||||||
|
if (_peers[i].id == offline) {
|
||||||
|
_peers[i].online = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updatePeers(Map<String, dynamic> evt) {
|
||||||
|
final onlineStates = _getOnlineStates();
|
||||||
|
_peers = _decodePeers(evt['peers']);
|
||||||
|
_peers.forEach((peer) {
|
||||||
|
final state = onlineStates[peer.id];
|
||||||
|
peer.online = state != null && state != false;
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, bool> _getOnlineStates() {
|
||||||
|
var onlineStates = new Map<String, bool>();
|
||||||
|
_peers.forEach((peer) {
|
||||||
|
onlineStates[peer.id] = peer.online;
|
||||||
|
});
|
||||||
|
return onlineStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Peer> _decodePeers(String peersStr) {
|
||||||
|
try {
|
||||||
|
if (peersStr == "") return [];
|
||||||
|
List<dynamic> peers = json.decode(peersStr);
|
||||||
|
return peers
|
||||||
|
.map((s) => s as List<dynamic>)
|
||||||
|
.map((s) =>
|
||||||
|
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
print('peers(): $e');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
7
flutter/lib/models/platform_model.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:flutter_hbb/generated_bridge.dart';
|
||||||
|
import 'native_model.dart' if (dart.library.html) 'web_model.dart';
|
||||||
|
|
||||||
|
final platformFFI = PlatformFFI.instance;
|
||||||
|
final localeName = PlatformFFI.localeName;
|
||||||
|
|
||||||
|
RustdeskImpl get bind => platformFFI.ffiBind;
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../pages/server_page.dart';
|
import '../desktop/pages/server_page.dart' as Desktop;
|
||||||
|
import '../desktop/widgets/tabbar_widget.dart';
|
||||||
|
import '../mobile/pages/server_page.dart';
|
||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
|
|
||||||
const loginDialogTag = "LOGIN";
|
const KLoginDialogTag = "LOGIN";
|
||||||
final _emptyIdShow = translate("Generating ...");
|
|
||||||
|
|
||||||
const kUseTemporaryPassword = "use-temporary-password";
|
const kUseTemporaryPassword = "use-temporary-password";
|
||||||
const kUsePermanentPassword = "use-permanent-password";
|
const kUsePermanentPassword = "use-permanent-password";
|
||||||
@@ -21,11 +26,15 @@ class ServerModel with ChangeNotifier {
|
|||||||
bool _fileOk = false;
|
bool _fileOk = false;
|
||||||
int _connectStatus = 0; // Rendezvous Server status
|
int _connectStatus = 0; // Rendezvous Server status
|
||||||
String _verificationMethod = "";
|
String _verificationMethod = "";
|
||||||
|
String _temporaryPasswordLength = "";
|
||||||
|
|
||||||
final _serverId = TextEditingController(text: _emptyIdShow);
|
late String _emptyIdShow;
|
||||||
|
late final TextEditingController _serverId;
|
||||||
final _serverPasswd = TextEditingController(text: "");
|
final _serverPasswd = TextEditingController(text: "");
|
||||||
|
|
||||||
Map<int, Client> _clients = {};
|
final tabController = DesktopTabController();
|
||||||
|
|
||||||
|
List<Client> _clients = [];
|
||||||
|
|
||||||
bool get isStart => _isStart;
|
bool get isStart => _isStart;
|
||||||
|
|
||||||
@@ -39,57 +48,50 @@ class ServerModel with ChangeNotifier {
|
|||||||
|
|
||||||
int get connectStatus => _connectStatus;
|
int get connectStatus => _connectStatus;
|
||||||
|
|
||||||
String get verificationMethod => _verificationMethod;
|
String get verificationMethod {
|
||||||
|
final index = [
|
||||||
|
kUseTemporaryPassword,
|
||||||
|
kUsePermanentPassword,
|
||||||
|
kUseBothPasswords
|
||||||
|
].indexOf(_verificationMethod);
|
||||||
|
if (index < 0) {
|
||||||
|
return kUseBothPasswords;
|
||||||
|
}
|
||||||
|
return _verificationMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
set verificationMethod(String method) {
|
||||||
|
bind.mainSetOption(key: "verification-method", value: method);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get temporaryPasswordLength {
|
||||||
|
final lengthIndex = ["6", "8", "10"].indexOf(_temporaryPasswordLength);
|
||||||
|
if (lengthIndex < 0) {
|
||||||
|
return "6";
|
||||||
|
}
|
||||||
|
return _temporaryPasswordLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
set temporaryPasswordLength(String length) {
|
||||||
|
bind.mainSetOption(key: "temporary-password-length", value: length);
|
||||||
|
}
|
||||||
|
|
||||||
TextEditingController get serverId => _serverId;
|
TextEditingController get serverId => _serverId;
|
||||||
|
|
||||||
TextEditingController get serverPasswd => _serverPasswd;
|
TextEditingController get serverPasswd => _serverPasswd;
|
||||||
|
|
||||||
Map<int, Client> get clients => _clients;
|
List<Client> get clients => _clients;
|
||||||
|
|
||||||
final controller = ScrollController();
|
final controller = ScrollController();
|
||||||
|
|
||||||
ServerModel() {
|
WeakReference<FFI> parent;
|
||||||
() async {
|
|
||||||
/**
|
|
||||||
* 1. check android permission
|
|
||||||
* 2. check config
|
|
||||||
* audio true by default (if permission on) (false default < Android 10)
|
|
||||||
* file true by default (if permission on)
|
|
||||||
*/
|
|
||||||
await Future.delayed(Duration(seconds: 1));
|
|
||||||
|
|
||||||
// audio
|
ServerModel(this.parent) {
|
||||||
if (androidVersion < 30 || !await PermissionManager.check("audio")) {
|
_emptyIdShow = translate("Generating ...");
|
||||||
_audioOk = false;
|
_serverId = TextEditingController(text: this._emptyIdShow);
|
||||||
FFI.setByName(
|
|
||||||
'option',
|
|
||||||
jsonEncode(Map()
|
|
||||||
..["name"] = "enable-audio"
|
|
||||||
..["value"] = "N"));
|
|
||||||
} else {
|
|
||||||
final audioOption = FFI.getByName('option', 'enable-audio');
|
|
||||||
_audioOk = audioOption.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// file
|
Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||||
if (!await PermissionManager.check("file")) {
|
var status = await bind.mainGetOnlineStatue();
|
||||||
_fileOk = false;
|
|
||||||
FFI.setByName(
|
|
||||||
'option',
|
|
||||||
jsonEncode(Map()
|
|
||||||
..["name"] = "enable-file-transfer"
|
|
||||||
..["value"] = "N"));
|
|
||||||
} else {
|
|
||||||
final fileOption = FFI.getByName('option', 'enable-file-transfer');
|
|
||||||
_fileOk = fileOption.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}();
|
|
||||||
|
|
||||||
Timer.periodic(Duration(seconds: 1), (timer) {
|
|
||||||
var status = int.tryParse(FFI.getByName('connect_statue')) ?? 0;
|
|
||||||
if (status > 0) {
|
if (status > 0) {
|
||||||
status = 1;
|
status = 1;
|
||||||
}
|
}
|
||||||
@@ -97,9 +99,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
_connectStatus = status;
|
_connectStatus = status;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
final res =
|
final res = await bind.mainCheckClientsLength(length: _clients.length);
|
||||||
FFI.getByName('check_clients_length', _clients.length.toString());
|
if (res != null) {
|
||||||
if (res.isNotEmpty) {
|
|
||||||
debugPrint("clients not match!");
|
debugPrint("clients not match!");
|
||||||
updateClientState(res);
|
updateClientState(res);
|
||||||
}
|
}
|
||||||
@@ -108,19 +109,59 @@ class ServerModel with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePasswordModel() {
|
/// 1. check android permission
|
||||||
var update = false;
|
/// 2. check config
|
||||||
final temporaryPassword = FFI.getByName("temporary_password");
|
/// audio true by default (if permission on) (false default < Android 10)
|
||||||
final verificationMethod = FFI.getByName("option", "verification-method");
|
/// file true by default (if permission on)
|
||||||
if (_serverPasswd.text != temporaryPassword) {
|
checkAndroidPermission() async {
|
||||||
_serverPasswd.text = temporaryPassword;
|
// audio
|
||||||
update = true;
|
if (androidVersion < 30 || !await PermissionManager.check("audio")) {
|
||||||
|
_audioOk = false;
|
||||||
|
bind.mainSetOption(key: "enable-audio", value: "N");
|
||||||
|
} else {
|
||||||
|
final audioOption = await bind.mainGetOption(key: 'enable-audio');
|
||||||
|
_audioOk = audioOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// file
|
||||||
|
if (!await PermissionManager.check("file")) {
|
||||||
|
_fileOk = false;
|
||||||
|
bind.mainSetOption(key: "enable-file-transfer", value: "N");
|
||||||
|
} else {
|
||||||
|
final fileOption = await bind.mainGetOption(key: 'enable-file-transfer');
|
||||||
|
_fileOk = fileOption.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// input (mouse control) false by default
|
||||||
|
bind.mainSetOption(key: "enable-keyboard", value: "N");
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePasswordModel() async {
|
||||||
|
var update = false;
|
||||||
|
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
||||||
|
final verificationMethod =
|
||||||
|
await bind.mainGetOption(key: "verification-method");
|
||||||
|
final temporaryPasswordLength =
|
||||||
|
await bind.mainGetOption(key: "temporary-password-length");
|
||||||
|
final oldPwdText = _serverPasswd.text;
|
||||||
|
if (_serverPasswd.text != temporaryPassword) {
|
||||||
|
_serverPasswd.text = temporaryPassword;
|
||||||
|
}
|
||||||
|
if (verificationMethod == kUsePermanentPassword) {
|
||||||
|
_serverPasswd.text = '-';
|
||||||
|
}
|
||||||
|
if (oldPwdText != _serverPasswd.text) {
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
if (_verificationMethod != verificationMethod) {
|
if (_verificationMethod != verificationMethod) {
|
||||||
_verificationMethod = verificationMethod;
|
_verificationMethod = verificationMethod;
|
||||||
update = true;
|
update = true;
|
||||||
}
|
}
|
||||||
|
if (_temporaryPasswordLength != temporaryPasswordLength) {
|
||||||
|
_temporaryPasswordLength = temporaryPasswordLength;
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
if (update) {
|
if (update) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -136,10 +177,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_audioOk = !_audioOk;
|
_audioOk = !_audioOk;
|
||||||
Map<String, String> res = Map()
|
bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N');
|
||||||
..["name"] = "enable-audio"
|
|
||||||
..["value"] = _audioOk ? '' : 'N';
|
|
||||||
FFI.setByName('option', jsonEncode(res));
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,25 +191,25 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fileOk = !_fileOk;
|
_fileOk = !_fileOk;
|
||||||
Map<String, String> res = Map()
|
bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N');
|
||||||
..["name"] = "enable-file-transfer"
|
|
||||||
..["value"] = _fileOk ? '' : 'N';
|
|
||||||
FFI.setByName('option', jsonEncode(res));
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleInput() {
|
toggleInput() {
|
||||||
if (_inputOk) {
|
if (_inputOk) {
|
||||||
FFI.invokeMethod("stop_input");
|
parent.target?.invokeMethod("stop_input");
|
||||||
} else {
|
} else {
|
||||||
showInputWarnAlert();
|
if (parent.target != null) {
|
||||||
|
showInputWarnAlert(parent.target!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle the screen sharing service.
|
||||||
toggleService() async {
|
toggleService() async {
|
||||||
if (_isStart) {
|
if (_isStart) {
|
||||||
final res =
|
final res = await parent.target?.dialogManager
|
||||||
await DialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
Icon(Icons.warning_amber_sharp,
|
Icon(Icons.warning_amber_sharp,
|
||||||
color: Colors.redAccent, size: 28),
|
color: Colors.redAccent, size: 28),
|
||||||
@@ -192,8 +230,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
stopService();
|
stopService();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final res =
|
final res = await parent.target?.dialogManager
|
||||||
await DialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
Icon(Icons.warning_amber_sharp,
|
Icon(Icons.warning_amber_sharp,
|
||||||
color: Colors.redAccent, size: 28),
|
color: Colors.redAccent, size: 28),
|
||||||
@@ -216,34 +254,44 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start the screen sharing service.
|
||||||
Future<Null> startService() async {
|
Future<Null> startService() async {
|
||||||
_isStart = true;
|
_isStart = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
FFI.ffiModel.updateEventListener("");
|
// TODO
|
||||||
await FFI.invokeMethod("init_service");
|
parent.target?.ffiModel.updateEventListener("");
|
||||||
FFI.setByName("start_service");
|
await parent.target?.invokeMethod("init_service");
|
||||||
|
await bind.mainStartService();
|
||||||
_fetchID();
|
_fetchID();
|
||||||
updateClientState();
|
updateClientState();
|
||||||
Wakelock.enable();
|
if (!Platform.isLinux) {
|
||||||
|
// current linux is not supported
|
||||||
|
Wakelock.enable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop the screen sharing service.
|
||||||
Future<Null> stopService() async {
|
Future<Null> stopService() async {
|
||||||
_isStart = false;
|
_isStart = false;
|
||||||
FFI.serverModel.closeAll();
|
// TODO
|
||||||
await FFI.invokeMethod("stop_service");
|
closeAll();
|
||||||
FFI.setByName("stop_service");
|
await parent.target?.invokeMethod("stop_service");
|
||||||
|
await bind.mainStopService();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
Wakelock.disable();
|
if (!Platform.isLinux) {
|
||||||
|
// current linux is not supported
|
||||||
|
Wakelock.disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Null> initInput() async {
|
Future<Null> initInput() async {
|
||||||
await FFI.invokeMethod("init_input");
|
await parent.target?.invokeMethod("init_input");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> setPermanentPassword(String newPW) async {
|
Future<bool> setPermanentPassword(String newPW) async {
|
||||||
FFI.setByName("permanent_password", newPW);
|
await bind.mainSetPermanentPassword(password: newPW);
|
||||||
await Future.delayed(Duration(milliseconds: 500));
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
final pw = FFI.getByName("permanent_password", newPW);
|
final pw = await bind.mainGetPermanentPassword();
|
||||||
if (newPW == pw) {
|
if (newPW == pw) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -257,7 +305,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
const maxCount = 10;
|
const maxCount = 10;
|
||||||
while (count < maxCount) {
|
while (count < maxCount) {
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
final id = FFI.getByName("server_id");
|
final id = await bind.mainGetMyId();
|
||||||
if (id.isEmpty) {
|
if (id.isEmpty) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
@@ -284,10 +332,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case "input":
|
case "input":
|
||||||
if (_inputOk != value) {
|
if (_inputOk != value) {
|
||||||
Map<String, String> res = Map()
|
bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N');
|
||||||
..["name"] = "enable-keyboard"
|
|
||||||
..["value"] = value ? '' : 'N';
|
|
||||||
FFI.setByName('option', jsonEncode(res));
|
|
||||||
}
|
}
|
||||||
_inputOk = value;
|
_inputOk = value;
|
||||||
break;
|
break;
|
||||||
@@ -297,13 +342,25 @@ class ServerModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClientState([String? json]) {
|
// force
|
||||||
var res = json ?? FFI.getByName("clients_state");
|
updateClientState([String? json]) async {
|
||||||
|
var res = await bind.mainGetClientsState();
|
||||||
try {
|
try {
|
||||||
final List clientsJson = jsonDecode(res);
|
final List clientsJson = jsonDecode(res);
|
||||||
|
if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) {
|
||||||
|
// exit cm when >1 peers to no peers
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
_clients.clear();
|
||||||
|
tabController.state.value.tabs.clear();
|
||||||
for (var clientJson in clientsJson) {
|
for (var clientJson in clientsJson) {
|
||||||
final client = Client.fromJson(clientJson);
|
final client = Client.fromJson(clientJson);
|
||||||
_clients[client.id] = client;
|
_clients.add(client);
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: client.id.toString(),
|
||||||
|
label: client.name,
|
||||||
|
closable: false,
|
||||||
|
page: Desktop.buildConnectionCard(client)));
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -314,20 +371,25 @@ class ServerModel with ChangeNotifier {
|
|||||||
void loginRequest(Map<String, dynamic> evt) {
|
void loginRequest(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
final client = Client.fromJson(jsonDecode(evt["client"]));
|
final client = Client.fromJson(jsonDecode(evt["client"]));
|
||||||
if (_clients.containsKey(client.id)) {
|
if (_clients.any((c) => c.id == client.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_clients[client.id] = client;
|
_clients.add(client);
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: client.id.toString(),
|
||||||
|
label: client.name,
|
||||||
|
closable: false,
|
||||||
|
page: Desktop.buildConnectionCard(client)));
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
showLoginDialog(client);
|
if (isAndroid) showLoginDialog(client);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to call loginRequest,error:$e");
|
debugPrint("Failed to call loginRequest,error:$e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLoginDialog(Client client) {
|
void showLoginDialog(Client client) {
|
||||||
DialogManager.show(
|
parent.target?.dialogManager.show(
|
||||||
(setState, close) => CustomAlertDialog(
|
(setState, close) => CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -373,6 +435,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
|
if (isDesktop) return;
|
||||||
Future.delayed(Duration(milliseconds: 200), () {
|
Future.delayed(Duration(milliseconds: 200), () {
|
||||||
controller.animateTo(controller.position.maxScrollExtent,
|
controller.animateTo(controller.position.maxScrollExtent,
|
||||||
duration: Duration(milliseconds: 200),
|
duration: Duration(milliseconds: 200),
|
||||||
@@ -380,30 +443,39 @@ class ServerModel with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendLoginResponse(Client client, bool res) {
|
void sendLoginResponse(Client client, bool res) async {
|
||||||
final Map<String, dynamic> response = Map();
|
|
||||||
response["id"] = client.id;
|
|
||||||
response["res"] = res;
|
|
||||||
if (res) {
|
if (res) {
|
||||||
FFI.setByName("login_res", jsonEncode(response));
|
bind.cmLoginRes(connId: client.id, res: res);
|
||||||
if (!client.isFileTransfer) {
|
if (!client.isFileTransfer) {
|
||||||
FFI.invokeMethod("start_capture");
|
parent.target?.invokeMethod("start_capture");
|
||||||
}
|
}
|
||||||
FFI.invokeMethod("cancel_notification", client.id);
|
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||||
_clients[client.id]?.authorized = true;
|
client.authorized = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
FFI.setByName("login_res", jsonEncode(response));
|
bind.cmLoginRes(connId: client.id, res: res);
|
||||||
FFI.invokeMethod("cancel_notification", client.id);
|
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||||
_clients.remove(client.id);
|
final index = _clients.indexOf(client);
|
||||||
|
tabController.remove(index);
|
||||||
|
_clients.remove(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onClientAuthorized(Map<String, dynamic> evt) {
|
void onClientAuthorized(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
final client = Client.fromJson(jsonDecode(evt['client']));
|
final client = Client.fromJson(jsonDecode(evt['client']));
|
||||||
DialogManager.dismissByTag(getLoginDialogTag(client.id));
|
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id));
|
||||||
_clients[client.id] = client;
|
final index = _clients.indexWhere((c) => c.id == client.id);
|
||||||
|
if (index < 0) {
|
||||||
|
_clients.add(client);
|
||||||
|
} else {
|
||||||
|
_clients[index].authorized = true;
|
||||||
|
}
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: client.id.toString(),
|
||||||
|
label: client.name,
|
||||||
|
closable: false,
|
||||||
|
page: Desktop.buildConnectionCard(client)));
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -412,10 +484,12 @@ class ServerModel with ChangeNotifier {
|
|||||||
void onClientRemove(Map<String, dynamic> evt) {
|
void onClientRemove(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
final id = int.parse(evt['id'] as String);
|
final id = int.parse(evt['id'] as String);
|
||||||
if (_clients.containsKey(id)) {
|
if (_clients.any((c) => c.id == id)) {
|
||||||
_clients.remove(id);
|
final index = _clients.indexWhere((client) => client.id == id);
|
||||||
DialogManager.dismissByTag(getLoginDialogTag(id));
|
_clients.removeAt(index);
|
||||||
FFI.invokeMethod("cancel_notification", id);
|
tabController.remove(index);
|
||||||
|
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
|
||||||
|
parent.target?.invokeMethod("cancel_notification", id);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -424,10 +498,16 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeAll() {
|
closeAll() {
|
||||||
_clients.forEach((id, client) {
|
_clients.forEach((client) {
|
||||||
FFI.setByName("close_conn", id.toString());
|
bind.cmCloseConnection(connId: client.id);
|
||||||
});
|
});
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
|
tabController.state.value.tabs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void jumpTo(int id) {
|
||||||
|
final index = _clients.indexWhere((client) => client.id == id);
|
||||||
|
tabController.jumpTo(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,8 +520,10 @@ class Client {
|
|||||||
bool keyboard = false;
|
bool keyboard = false;
|
||||||
bool clipboard = false;
|
bool clipboard = false;
|
||||||
bool audio = false;
|
bool audio = false;
|
||||||
|
bool file = false;
|
||||||
|
bool restart = false;
|
||||||
|
|
||||||
Client(this.authorized, this.isFileTransfer, this.name, this.peerId,
|
Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId,
|
||||||
this.keyboard, this.clipboard, this.audio);
|
this.keyboard, this.clipboard, this.audio);
|
||||||
|
|
||||||
Client.fromJson(Map<String, dynamic> json) {
|
Client.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -453,6 +535,8 @@ class Client {
|
|||||||
keyboard = json['keyboard'];
|
keyboard = json['keyboard'];
|
||||||
clipboard = json['clipboard'];
|
clipboard = json['clipboard'];
|
||||||
audio = json['audio'];
|
audio = json['audio'];
|
||||||
|
file = json['file'];
|
||||||
|
restart = json['restart'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -470,11 +554,11 @@ class Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getLoginDialogTag(int id) {
|
String getLoginDialogTag(int id) {
|
||||||
return loginDialogTag + id.toString();
|
return KLoginDialogTag + id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
showInputWarnAlert() {
|
showInputWarnAlert(FFI ffi) {
|
||||||
DialogManager.show((setState, close) => CustomAlertDialog(
|
ffi.dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
title: Text(translate("How to get Android input permission?")),
|
title: Text(translate("How to get Android input permission?")),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -489,7 +573,7 @@ showInputWarnAlert() {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Text(translate("Open System Setting")),
|
child: Text(translate("Open System Setting")),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FFI.serverModel.initInput();
|
ffi.serverModel.initInput();
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
75
flutter/lib/models/user_model.dart
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import 'model.dart';
|
||||||
|
import 'platform_model.dart';
|
||||||
|
|
||||||
|
class UserModel extends ChangeNotifier {
|
||||||
|
var userName = "".obs;
|
||||||
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
UserModel(this.parent);
|
||||||
|
|
||||||
|
Future<String> getUserName() async {
|
||||||
|
if (userName.isNotEmpty) {
|
||||||
|
return userName.value;
|
||||||
|
}
|
||||||
|
final userInfo = await bind.mainGetLocalOption(key: 'user_info');
|
||||||
|
if (userInfo.trim().isEmpty) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
final m = jsonDecode(userInfo);
|
||||||
|
userName.value = m['name'] ?? '';
|
||||||
|
return userName.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logOut() async {
|
||||||
|
debugPrint("start logout");
|
||||||
|
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 _getHeaders());
|
||||||
|
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 = "";
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>>? _getHeaders() {
|
||||||
|
return parent.target?.getHttpHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> login(String userName, String pass) async {
|
||||||
|
final url = await bind.mainGetApiServer();
|
||||||
|
try {
|
||||||
|
final resp = await http.post(Uri.parse("$url/api/login"),
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({
|
||||||
|
"username": userName,
|
||||||
|
"password": pass,
|
||||||
|
"id": await bind.mainGetMyId(),
|
||||||
|
"uuid": await bind.mainGetUuid()
|
||||||
|
}));
|
||||||
|
final body = jsonDecode(resp.body);
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: "access_token", value: body['access_token'] ?? "");
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: "user_info", value: jsonEncode(body['user']));
|
||||||
|
this.userName.value = body['user']?['name'] ?? "";
|
||||||
|
return body;
|
||||||
|
} catch (err) {
|
||||||
|
return {"error": "$err"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,14 @@ class PlatformFFI {
|
|||||||
context.callMethod('setByName', [name, value]);
|
context.callMethod('setByName', [name, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Null> init() async {
|
PlatformFFI._();
|
||||||
|
static final PlatformFFI instance = PlatformFFI._();
|
||||||
|
|
||||||
|
static get localeName => window.navigator.language;
|
||||||
|
|
||||||
|
static Future<Null> init(String _appType) async {
|
||||||
isWeb = true;
|
isWeb = true;
|
||||||
isDesktop = !context.callMethod('isMobile');
|
isWebDesktop = !context.callMethod('isMobile');
|
||||||
context.callMethod('init');
|
context.callMethod('init');
|
||||||
version = getByName('version');
|
version = getByName('version');
|
||||||
}
|
}
|
||||||
@@ -68,5 +73,3 @@ class PlatformFFI {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final localeName = window.navigator.language;
|
|
||||||
|
|||||||
145
flutter/lib/utils/multi_window_manager.dart
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// must keep the order
|
||||||
|
enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown }
|
||||||
|
|
||||||
|
extension Index on int {
|
||||||
|
WindowType get windowType {
|
||||||
|
switch (this) {
|
||||||
|
case 0:
|
||||||
|
return WindowType.Main;
|
||||||
|
case 1:
|
||||||
|
return WindowType.RemoteDesktop;
|
||||||
|
case 2:
|
||||||
|
return WindowType.FileTransfer;
|
||||||
|
case 3:
|
||||||
|
return WindowType.PortForward;
|
||||||
|
default:
|
||||||
|
return WindowType.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Window Manager
|
||||||
|
/// mainly use it in `Main Window`
|
||||||
|
/// use it in sub window is not recommended
|
||||||
|
class RustDeskMultiWindowManager {
|
||||||
|
RustDeskMultiWindowManager._();
|
||||||
|
|
||||||
|
static final instance = RustDeskMultiWindowManager._();
|
||||||
|
|
||||||
|
int? _remoteDesktopWindowId;
|
||||||
|
int? _fileTransferWindowId;
|
||||||
|
|
||||||
|
Future<dynamic> new_remote_desktop(String remote_id) async {
|
||||||
|
final msg =
|
||||||
|
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
|
if (!ids.contains(_remoteDesktopWindowId)) {
|
||||||
|
_remoteDesktopWindowId = null;
|
||||||
|
}
|
||||||
|
} on Error {
|
||||||
|
_remoteDesktopWindowId = null;
|
||||||
|
}
|
||||||
|
if (_remoteDesktopWindowId == null) {
|
||||||
|
final remoteDesktopController =
|
||||||
|
await DesktopMultiWindow.createWindow(msg);
|
||||||
|
remoteDesktopController
|
||||||
|
..setFrame(const Offset(0, 0) & const Size(1280, 720))
|
||||||
|
..center()
|
||||||
|
..setTitle("rustdesk - remote desktop")
|
||||||
|
..show();
|
||||||
|
_remoteDesktopWindowId = remoteDesktopController.windowId;
|
||||||
|
} else {
|
||||||
|
return call(WindowType.RemoteDesktop, "new_remote_desktop", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> new_file_transfer(String remote_id) async {
|
||||||
|
final msg =
|
||||||
|
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
|
if (!ids.contains(_fileTransferWindowId)) {
|
||||||
|
_fileTransferWindowId = null;
|
||||||
|
}
|
||||||
|
} on Error {
|
||||||
|
_fileTransferWindowId = null;
|
||||||
|
}
|
||||||
|
if (_fileTransferWindowId == null) {
|
||||||
|
final fileTransferController = await DesktopMultiWindow.createWindow(msg);
|
||||||
|
fileTransferController
|
||||||
|
..setFrame(const Offset(0, 0) & const Size(1280, 720))
|
||||||
|
..center()
|
||||||
|
..setTitle("rustdesk - file transfer")
|
||||||
|
..show();
|
||||||
|
_fileTransferWindowId = fileTransferController.windowId;
|
||||||
|
} else {
|
||||||
|
return call(WindowType.FileTransfer, "new_file_transfer", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
|
||||||
|
int? windowId = findWindowByType(type);
|
||||||
|
if (windowId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? findWindowByType(WindowType type) {
|
||||||
|
switch (type) {
|
||||||
|
case WindowType.Main:
|
||||||
|
return 0;
|
||||||
|
case WindowType.RemoteDesktop:
|
||||||
|
return _remoteDesktopWindowId;
|
||||||
|
case WindowType.FileTransfer:
|
||||||
|
return _fileTransferWindowId;
|
||||||
|
case WindowType.PortForward:
|
||||||
|
break;
|
||||||
|
case WindowType.Unknown:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMethodHandler(
|
||||||
|
Future<dynamic> Function(MethodCall call, int fromWindowId)? handler) {
|
||||||
|
DesktopMultiWindow.setMethodHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeAllSubWindows() async {
|
||||||
|
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeWindows(WindowType type) async {
|
||||||
|
if (type == WindowType.Main) {
|
||||||
|
// skip main window, use window manager instead
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int? wId = findWindowByType(type);
|
||||||
|
if (wId != null) {
|
||||||
|
debugPrint("closing multi window: ${type.toString()}");
|
||||||
|
try {
|
||||||
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
|
if (!ids.contains(wId)) {
|
||||||
|
// no such window already
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await WindowController.fromWindowId(wId).close();
|
||||||
|
} on Error {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final rustDeskWinManager = RustDeskMultiWindowManager.instance;
|
||||||
23
flutter/lib/utils/tray_manager.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
|
|
||||||
|
import '../common.dart';
|
||||||
|
|
||||||
|
Future<void> initTray({List<MenuItem>? extra_item}) async {
|
||||||
|
List<MenuItem> items = [
|
||||||
|
MenuItem(key: "show", label: translate("show rustdesk")),
|
||||||
|
MenuItem.separator(),
|
||||||
|
MenuItem(key: "quit", label: translate("quit rustdesk")),
|
||||||
|
];
|
||||||
|
if (extra_item != null) {
|
||||||
|
items.insertAll(0, extra_item);
|
||||||
|
}
|
||||||
|
await Future.wait([
|
||||||
|
trayManager
|
||||||
|
.setIcon(Platform.isWindows ? "assets/logo.ico" : "assets/logo.png"),
|
||||||
|
trayManager.setContextMenu(Menu(items: items)),
|
||||||
|
trayManager.setToolTip("rustdesk"),
|
||||||
|
trayManager.setTitle("rustdesk")
|
||||||
|
]);
|
||||||
|
}
|
||||||
1
flutter/linux/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||
158
flutter/linux/CMakeLists.txt
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.12)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "flutter_hbb")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.carriez.flutter_hbb")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||||
|
|
||||||
|
# Root filesystem for cross-building.
|
||||||
|
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||||
|
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration options.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||||
|
|
||||||
|
# flutter_rust_bridge
|
||||||
|
find_package(Corrosion REQUIRED)
|
||||||
|
|
||||||
|
corrosion_import_crate(MANIFEST_PATH ../../Cargo.toml
|
||||||
|
# Equivalent to --all-features passed to cargo build
|
||||||
|
# [ALL_FEATURES]
|
||||||
|
# Equivalent to --no-default-features passed to cargo build
|
||||||
|
# [NO_DEFAULT_FEATURES]
|
||||||
|
# Disable linking of standard libraries (required for no_std crates).
|
||||||
|
# [NO_STD]
|
||||||
|
# Specify cargo build profile (e.g. release or a custom profile)
|
||||||
|
# [PROFILE <cargo-profile>]
|
||||||
|
# Only import the specified crates from a workspace
|
||||||
|
# [CRATES <crate1> ... <crateN>]
|
||||||
|
# Enable the specified features
|
||||||
|
# [FEATURES <feature1> ... <featureN>]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME above,
|
||||||
|
# not the value here, or `flutter run` will no longer work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME}
|
||||||
|
"main.cc"
|
||||||
|
"my_application.cc"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add dependency libraries. Add any application-specific dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE librustdesk)
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
|
# directory.
|
||||||
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
|
#if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
#endif()
|
||||||
|
|
||||||
|
# Start with a clean build bundle directory every time.
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
88
flutter/linux/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||||
|
# which isn't available in 3.10.
|
||||||
|
function(list_prepend LIST_NAME PREFIX)
|
||||||
|
set(NEW_LIST "")
|
||||||
|
foreach(element ${${LIST_NAME}})
|
||||||
|
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||||
|
endforeach(element)
|
||||||
|
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
28
flutter/linux/main.cc
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include <dlfcn.h>
|
||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so"
|
||||||
|
typedef bool (*RustDeskCoreMain)();
|
||||||
|
|
||||||
|
bool flutter_rustdesk_core_main() {
|
||||||
|
void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY);
|
||||||
|
if (!librustdesk) {
|
||||||
|
fprintf(stderr,"load librustdesk.so failed\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main");
|
||||||
|
char* error;
|
||||||
|
if ((error = dlerror()) != nullptr) {
|
||||||
|
fprintf(stderr, "error finding rustdesk_core_main: %s", error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return core_main();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (!flutter_rustdesk_core_main()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
g_autoptr(MyApplication) app = my_application_new();
|
||||||
|
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
|
}
|
||||||
106
flutter/linux/my_application.cc
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
struct _MyApplication {
|
||||||
|
GtkApplication parent_instance;
|
||||||
|
char** dart_entrypoint_arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
|
// Implements GApplication::activate.
|
||||||
|
static void my_application_activate(GApplication* application) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
GtkWindow* window =
|
||||||
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// desktop).
|
||||||
|
// If running on X and not using GNOME then just use a traditional title bar
|
||||||
|
// in case the window manager does more exotic layout, e.g. tiling.
|
||||||
|
// If running on Wayland assume the header bar will work (may need changing
|
||||||
|
// if future cases occur).
|
||||||
|
gboolean use_header_bar = TRUE;
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
if (GDK_IS_X11_SCREEN(screen)) {
|
||||||
|
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||||
|
use_header_bar = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (use_header_bar) {
|
||||||
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
|
gtk_header_bar_set_title(header_bar, "rustdesk");
|
||||||
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
|
} else {
|
||||||
|
gtk_window_set_title(window, "rustdesk");
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto bdw = bitsdojo_window_from(window); // <--- add this line
|
||||||
|
// bdw->setCustomFrame(true); // <-- add this line
|
||||||
|
gtk_window_set_default_size(window, 1280, 720); // <-- comment this line
|
||||||
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
|
||||||
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||||
|
|
||||||
|
FlView* view = fl_view_new(project);
|
||||||
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::local_command_line.
|
||||||
|
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
// Strip out the first argument as it is the binary name.
|
||||||
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||||
|
|
||||||
|
g_autoptr(GError) error = nullptr;
|
||||||
|
if (!g_application_register(application, nullptr, &error)) {
|
||||||
|
g_warning("Failed to register: %s", error->message);
|
||||||
|
*exit_status = 1;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_application_activate(application);
|
||||||
|
*exit_status = 0;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GObject::dispose.
|
||||||
|
static void my_application_dispose(GObject* object) {
|
||||||
|
MyApplication* self = MY_APPLICATION(object);
|
||||||
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
|
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_class_init(MyApplicationClass* klass) {
|
||||||
|
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||||
|
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_init(MyApplication* self) {}
|
||||||
|
|
||||||
|
MyApplication* my_application_new() {
|
||||||
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
|
"application-id", APPLICATION_ID,
|
||||||
|
"flags", G_APPLICATION_NON_UNIQUE,
|
||||||
|
nullptr));
|
||||||
|
}
|
||||||
18
flutter/linux/my_application.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||||
|
#define FLUTTER_MY_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||||
|
GtkApplication)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* my_application_new:
|
||||||
|
*
|
||||||
|
* Creates a new Flutter-based application.
|
||||||
|
*
|
||||||
|
* Returns: a new #MyApplication.
|
||||||
|
*/
|
||||||
|
MyApplication* my_application_new();
|
||||||
|
|
||||||
|
#endif // FLUTTER_MY_APPLICATION_H_
|
||||||
7
flutter/macos/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Flutter-related
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
|
||||||
|
# Xcode-related
|
||||||
|
**/dgph
|
||||||
|
**/xcuserdata/
|
||||||
2
flutter/macos/Flutter/Flutter-Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
2
flutter/macos/Flutter/Flutter-Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
40
flutter/macos/Podfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
platform :osx, '10.12'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_macos_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_macos_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
196
flutter/macos/Podfile.lock
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
PODS:
|
||||||
|
- bitsdojo_window_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- desktop_multi_window (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- device_info_plus_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- Firebase/Analytics (8.15.0):
|
||||||
|
- Firebase/Core
|
||||||
|
- Firebase/Core (8.15.0):
|
||||||
|
- Firebase/CoreOnly
|
||||||
|
- FirebaseAnalytics (~> 8.15.0)
|
||||||
|
- Firebase/CoreOnly (8.15.0):
|
||||||
|
- FirebaseCore (= 8.15.0)
|
||||||
|
- firebase_analytics (9.1.9):
|
||||||
|
- Firebase/Analytics (= 8.15.0)
|
||||||
|
- firebase_core
|
||||||
|
- FlutterMacOS
|
||||||
|
- firebase_core (1.17.1):
|
||||||
|
- Firebase/CoreOnly (~> 8.15.0)
|
||||||
|
- FlutterMacOS
|
||||||
|
- FirebaseAnalytics (8.15.0):
|
||||||
|
- FirebaseAnalytics/AdIdSupport (= 8.15.0)
|
||||||
|
- FirebaseCore (~> 8.0)
|
||||||
|
- FirebaseInstallations (~> 8.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- FirebaseAnalytics/AdIdSupport (8.15.0):
|
||||||
|
- FirebaseCore (~> 8.0)
|
||||||
|
- FirebaseInstallations (~> 8.0)
|
||||||
|
- GoogleAppMeasurement (= 8.15.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- FirebaseCore (8.15.0):
|
||||||
|
- FirebaseCoreDiagnostics (~> 8.0)
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- GoogleUtilities/Logger (~> 7.7)
|
||||||
|
- FirebaseCoreDiagnostics (8.15.0):
|
||||||
|
- GoogleDataTransport (~> 9.1)
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- GoogleUtilities/Logger (~> 7.7)
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- FirebaseInstallations (8.15.0):
|
||||||
|
- FirebaseCore (~> 8.0)
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 7.7)
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- FlutterMacOS (1.0.0)
|
||||||
|
- GoogleAppMeasurement (8.15.0):
|
||||||
|
- GoogleAppMeasurement/AdIdSupport (= 8.15.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- GoogleAppMeasurement/AdIdSupport (8.15.0):
|
||||||
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- GoogleAppMeasurement/WithoutAdIdSupport (8.15.0):
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- GoogleDataTransport (9.1.4):
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (7.7.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network
|
||||||
|
- GoogleUtilities/Environment (7.7.0):
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- GoogleUtilities/Logger (7.7.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/MethodSwizzler (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- "GoogleUtilities/NSData+zlib"
|
||||||
|
- GoogleUtilities/Reachability
|
||||||
|
- "GoogleUtilities/NSData+zlib (7.7.0)"
|
||||||
|
- GoogleUtilities/Reachability (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/UserDefaults (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- nanopb (2.30908.0):
|
||||||
|
- nanopb/decode (= 2.30908.0)
|
||||||
|
- nanopb/encode (= 2.30908.0)
|
||||||
|
- nanopb/decode (2.30908.0)
|
||||||
|
- nanopb/encode (2.30908.0)
|
||||||
|
- package_info_plus_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- path_provider_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- PromisesObjC (2.1.0)
|
||||||
|
- shared_preferences_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- url_launcher_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- wakelock_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- window_manager (0.2.0):
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
||||||
|
- desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`)
|
||||||
|
- device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`)
|
||||||
|
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
||||||
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
|
||||||
|
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||||
|
- shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
|
||||||
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
- wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
|
||||||
|
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- Firebase
|
||||||
|
- FirebaseAnalytics
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreDiagnostics
|
||||||
|
- FirebaseInstallations
|
||||||
|
- GoogleAppMeasurement
|
||||||
|
- GoogleDataTransport
|
||||||
|
- GoogleUtilities
|
||||||
|
- nanopb
|
||||||
|
- PromisesObjC
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
bitsdojo_window_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
||||||
|
desktop_multi_window:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos
|
||||||
|
device_info_plus_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos
|
||||||
|
firebase_analytics:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos
|
||||||
|
firebase_core:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
||||||
|
FlutterMacOS:
|
||||||
|
:path: Flutter/ephemeral
|
||||||
|
package_info_plus_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
|
||||||
|
path_provider_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
||||||
|
shared_preferences_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos
|
||||||
|
url_launcher_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
wakelock_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
|
||||||
|
window_manager:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
|
||||||
|
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||||
|
device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7
|
||||||
|
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
|
||||||
|
firebase_analytics: d448483150504ed84f25c5437a34af2591a7929e
|
||||||
|
firebase_core: 7b87364e2d1eae70018a60698e89e7d6f5320bad
|
||||||
|
FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa
|
||||||
|
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
|
||||||
|
FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb
|
||||||
|
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
|
||||||
|
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||||
|
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
|
||||||
|
GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b
|
||||||
|
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
|
||||||
|
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
||||||
|
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
|
||||||
|
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||||
|
PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72
|
||||||
|
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
||||||
|
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||||
|
wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
|
||||||
|
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176
|
||||||
|
|
||||||
|
COCOAPODS: 1.11.3
|
||||||
730
flutter/macos/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 51;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXAggregateTarget section */
|
||||||
|
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||||
|
isa = PBXAggregateTarget;
|
||||||
|
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||||
|
buildPhases = (
|
||||||
|
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "Flutter Assemble";
|
||||||
|
productName = FLX;
|
||||||
|
};
|
||||||
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||||
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||||
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; };
|
||||||
|
CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; };
|
||||||
|
CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||||
|
remoteInfo = FLX;
|
||||||
|
};
|
||||||
|
CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */;
|
||||||
|
proxyType = 2;
|
||||||
|
remoteGlobalIDString = CA6071B5A0F5A7A3EF2297AA;
|
||||||
|
remoteInfo = "librustdesk-cdylib";
|
||||||
|
};
|
||||||
|
CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */;
|
||||||
|
proxyType = 2;
|
||||||
|
remoteGlobalIDString = CA604C7415FB2A3731F5016A;
|
||||||
|
remoteInfo = "librustdesk-staticlib";
|
||||||
|
};
|
||||||
|
CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */;
|
||||||
|
proxyType = 2;
|
||||||
|
remoteGlobalIDString = CA60D3BC5386D3D7DBD96893;
|
||||||
|
remoteInfo = "naming-bin";
|
||||||
|
};
|
||||||
|
CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */;
|
||||||
|
proxyType = 2;
|
||||||
|
remoteGlobalIDString = CA60D3BC5386B357B2AB834F;
|
||||||
|
remoteInfo = "rustdesk-bin";
|
||||||
|
};
|
||||||
|
CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = CA6071B5A0F5D6691E4C3FF1;
|
||||||
|
remoteInfo = "librustdesk-cdylib";
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */,
|
||||||
|
);
|
||||||
|
name = "Bundle Framework";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
26C84465887F29AE938039CB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
|
33CC10ED2044A3C60003C045 /* flutter_hbb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_hbb.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
|
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||||
|
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||||
|
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; 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>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.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>"; };
|
||||||
|
CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = rustdesk.xcodeproj; sourceTree = SOURCE_ROOT; };
|
||||||
|
CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bridge_generated.h; path = Runner/bridge_generated.h; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */,
|
||||||
|
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Configs;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CC10E42044A3C60003C045 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */,
|
||||||
|
33FAB671232836740065AC1E /* Runner */,
|
||||||
|
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||||
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
|
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||||
|
A6C450E1C32EC39A23170131 /* Pods */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33CC10ED2044A3C60003C045 /* flutter_hbb.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CC11242044D66E0003C045 /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||||
|
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||||
|
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||||
|
);
|
||||||
|
name = Resources;
|
||||||
|
path = ..;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||||
|
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||||
|
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||||
|
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33FAB671232836740065AC1E /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */,
|
||||||
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||||
|
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||||
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||||
|
33E51914231749380026EE4D /* Release.entitlements */,
|
||||||
|
33CC11242044D66E0003C045 /* Resources */,
|
||||||
|
33BA886A226E78AF003329D5 /* Configs */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A6C450E1C32EC39A23170131 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */,
|
||||||
|
C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
CC13D42F2847C8C200EF8B54 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CC13D4362847C8C200EF8B54 /* librustdesk.dylib */,
|
||||||
|
CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */,
|
||||||
|
CC13D43A2847C8C200EF8B54 /* naming */,
|
||||||
|
CC13D43C2847C8C200EF8B54 /* rustdesk */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
26C84465887F29AE938039CB /* Pods_Runner.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
610B125EE2B990E4D4B30D05 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
|
4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */,
|
||||||
|
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 33CC10ED2044A3C60003C045 /* flutter_hbb.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 0920;
|
||||||
|
LastUpgradeCheck = 1300;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
33CC10EC2044A3C60003C045 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
SystemCapabilities = {
|
||||||
|
com.apple.Sandbox = {
|
||||||
|
enabled = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
33CC111A2044C6BA0003C045 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Manual;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 33CC10E42044A3C60003C045;
|
||||||
|
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectReferences = (
|
||||||
|
{
|
||||||
|
ProductGroup = CC13D42F2847C8C200EF8B54 /* Products */;
|
||||||
|
ProjectRef = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
33CC10EC2044A3C60003C045 /* Runner */,
|
||||||
|
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXReferenceProxy section */
|
||||||
|
CC13D4362847C8C200EF8B54 /* librustdesk.dylib */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = "compiled.mach-o.dylib";
|
||||||
|
path = librustdesk.dylib;
|
||||||
|
remoteRef = CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
|
CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = archive.ar;
|
||||||
|
path = liblibrustdesk_static.a;
|
||||||
|
remoteRef = CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
|
CC13D43A2847C8C200EF8B54 /* naming */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = "compiled.mach-o.executable";
|
||||||
|
path = naming;
|
||||||
|
remoteRef = CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
|
CC13D43C2847C8C200EF8B54 /* rustdesk */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = "compiled.mach-o.executable";
|
||||||
|
path = rustdesk;
|
||||||
|
remoteRef = CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
|
/* End PBXReferenceProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||||
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
|
||||||
|
};
|
||||||
|
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
Flutter/ephemeral/tripwire,
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
|
};
|
||||||
|
4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
610B125EE2B990E4D4B30D05 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||||
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||||
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||||
|
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
name = "librustdesk-cdylib";
|
||||||
|
targetProxy = CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
33CC10F52044A3C60003C045 /* Base */,
|
||||||
|
);
|
||||||
|
name = MainMenu.xib;
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
33CC10F92044A3C60003C045 /* Debug */,
|
||||||
|
33CC10FA2044A3C60003C045 /* Release */,
|
||||||
|
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
33CC10FC2044A3C60003C045 /* Debug */,
|
||||||
|
33CC10FD2044A3C60003C045 /* Release */,
|
||||||
|
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
33CC111C2044C6BA0003C045 /* Debug */,
|
||||||
|
33CC111D2044C6BA0003C045 /* Release */,
|
||||||
|
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1300"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "flutter_hbb.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "flutter_hbb.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "flutter_hbb.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "flutter_hbb.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
10
flutter/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
10
flutter/macos/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@NSApplicationMain
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
dummy_method_to_enforce_bundling()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "16x16",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_16.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "16x16",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_32.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "32x32",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_32.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "32x32",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_64.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "128x128",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_128.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "128x128",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_256.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "256x256",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_256.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "256x256",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_512.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "512x512",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_512.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "512x512",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_1024.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
343
flutter/macos/Runner/Base.lproj/MainMenu.xib
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||||
|
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
|
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||||
|
<items>
|
||||||
|
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||||
|
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||||
|
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||||
|
<menuItem title="Services" id="NMo-om-nkz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||||
|
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||||
|
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||||
|
<connections>
|
||||||
|
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||||
|
<connections>
|
||||||
|
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||||
|
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||||
|
<connections>
|
||||||
|
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||||
|
<connections>
|
||||||
|
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||||
|
<connections>
|
||||||
|
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||||
|
<connections>
|
||||||
|
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||||
|
<menuItem title="Find" id="4EN-yA-p0u">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||||
|
<connections>
|
||||||
|
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||||
|
<connections>
|
||||||
|
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||||
|
<connections>
|
||||||
|
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||||
|
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||||
|
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="View" id="H8h-7b-M4v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="aUF-d1-5bR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||||
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Help" id="EPT-qC-fAb">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
<point key="canvasLocation" x="142" y="-258"/>
|
||||||
|
</menu>
|
||||||
|
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||||
|
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
</window>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
14
flutter/macos/Runner/Configs/AppInfo.xcconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Application-level settings for the Runner target.
|
||||||
|
//
|
||||||
|
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
|
||||||
|
// future. If not, the values below would default to using the project name when this becomes a
|
||||||
|
// 'flutter create' template.
|
||||||
|
|
||||||
|
// The application's name. By default this is also the title of the Flutter window.
|
||||||
|
PRODUCT_NAME = flutter_hbb
|
||||||
|
|
||||||
|
// The application's bundle identifier
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
|
||||||
|
|
||||||
|
// The copyright displayed in application information
|
||||||
|
PRODUCT_COPYRIGHT = Copyright © 2022 com.carriez. All rights reserved.
|
||||||
2
flutter/macos/Runner/Configs/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||||
|
#include "Warnings.xcconfig"
|
||||||
2
flutter/macos/Runner/Configs/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||||
|
#include "Warnings.xcconfig"
|
||||||
13
flutter/macos/Runner/Configs/Warnings.xcconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES
|
||||||
|
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
|
||||||
|
CLANG_WARN_PRAGMA_PACK = YES
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES
|
||||||
|
CLANG_WARN_COMMA = YES
|
||||||
|
GCC_WARN_STRICT_SELECTOR_MATCH = YES
|
||||||
|
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
|
||||||
|
GCC_WARN_SHADOW = YES
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES
|
||||||
12
flutter/macos/Runner/DebugProfile.entitlements
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
32
flutter/macos/Runner/Info.plist
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
|
<key>NSMainNibFile</key>
|
||||||
|
<string>MainMenu</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
24
flutter/macos/Runner/MainFlutterWindow.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
// import bitsdojo_window_macos
|
||||||
|
|
||||||
|
class MainFlutterWindow: NSWindow {
|
||||||
|
override func awakeFromNib() {
|
||||||
|
if (!rustdesk_core_main()){
|
||||||
|
print("Rustdesk core returns false, exiting without launching Flutter app")
|
||||||
|
NSApplication.shared.terminate(self)
|
||||||
|
}
|
||||||
|
let flutterViewController = FlutterViewController.init()
|
||||||
|
let windowFrame = self.frame
|
||||||
|
self.contentViewController = flutterViewController
|
||||||
|
self.setFrame(windowFrame, display: true)
|
||||||
|
|
||||||
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
|
super.awakeFromNib()
|
||||||
|
}
|
||||||
|
|
||||||
|
// override func bitsdojo_window_configure() -> UInt {
|
||||||
|
// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
|
||||||
|
// }
|
||||||
|
}
|
||||||
8
flutter/macos/Runner/Release.entitlements
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
439
flutter/macos/rustdesk.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 53;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
CA6061C6409F12977AAB839F /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; };
|
||||||
|
CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin naming"; }; };
|
||||||
|
CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin rustdesk"; }; };
|
||||||
|
CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXBuildRule section */
|
||||||
|
CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */ = {
|
||||||
|
isa = PBXBuildRule;
|
||||||
|
compilerSpec = com.apple.compilers.proxy.script;
|
||||||
|
dependencyFile = "$(DERIVED_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME).d";
|
||||||
|
filePatterns = "*/Cargo.toml";
|
||||||
|
fileType = pattern.proxy;
|
||||||
|
inputFiles = (
|
||||||
|
);
|
||||||
|
isEditable = 0;
|
||||||
|
name = "Cargo project build";
|
||||||
|
outputFiles = (
|
||||||
|
"$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)",
|
||||||
|
);
|
||||||
|
script = "# generated with cargo-xcode 1.4.1\n\nset -eu; export PATH=$PATH:~/.cargo/bin:/usr/local/bin;\nif [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-ios-macabi\"\nelse\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-${CARGO_XCODE_TARGET_OS}\"\nfi\nif [ \"$CARGO_XCODE_TARGET_OS\" != \"darwin\" ]; then\n PATH=\"${PATH/\\/Contents\\/Developer\\/Toolchains\\/XcodeDefault.xctoolchain\\/usr\\/bin:/xcode-provided-ld-cant-link-lSystem-for-the-host-build-script:}\"\nfi\nPATH=\"$PATH:/opt/homebrew/bin\" # Rust projects often depend on extra tools like nasm, which Xcode lacks\nif [ \"$CARGO_XCODE_BUILD_MODE\" == release ]; then\n OTHER_INPUT_FILE_FLAGS=\"${OTHER_INPUT_FILE_FLAGS} --release\"\nfi\nif command -v rustup &> /dev/null; then\n if ! rustup target list --installed | egrep -q \"${CARGO_XCODE_TARGET_TRIPLE}\"; then\n echo \"warning: this build requires rustup toolchain for $CARGO_XCODE_TARGET_TRIPLE, but it isn't installed\"\n rustup target add \"${CARGO_XCODE_TARGET_TRIPLE}\" || echo >&2 \"warning: can't install $CARGO_XCODE_TARGET_TRIPLE\"\n fi\nfi\nif [ \"$ACTION\" = clean ]; then\n ( set -x; cargo clean --manifest-path=\"$SCRIPT_INPUT_FILE\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nelse\n ( set -x; cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nfi\n# it's too hard to explain Cargo's actual exe path to Xcode build graph, so hardlink to a known-good path instead\nBUILT_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_FILE_NAME}\"\nln -f -- \"$BUILT_SRC\" \"$SCRIPT_OUTPUT_FILE_0\"\n\n# xcode generates dep file, but for its own path, so append our rename to it\nDEP_FILE_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_DEP_FILE_NAME}\"\nif [ -f \"$DEP_FILE_SRC\" ]; then\n DEP_FILE_DST=\"${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d\"\n cp -f \"$DEP_FILE_SRC\" \"$DEP_FILE_DST\"\n echo >> \"$DEP_FILE_DST\" \"$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC\"\nfi\n\n# lipo script needs to know all the platform-specific files that have been built\n# archs is in the file name, so that paths don't stay around after archs change\n# must match input for LipoScript\nFILE_LIST=\"${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist\"\ntouch \"$FILE_LIST\"\nif ! egrep -q \"$SCRIPT_OUTPUT_FILE_0\" \"$FILE_LIST\" ; then\n echo >> \"$FILE_LIST\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n";
|
||||||
|
};
|
||||||
|
/* End PBXBuildRule section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
|
||||||
|
CA603C4309E13EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = /Users/ruizruiz/Work/Code/Projects/RustDesk/rustdesk/Cargo.toml; sourceTree = "<group>"; };
|
||||||
|
CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibrustdesk_static.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = librustdesk.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
CA60D3BC5386B357B2AB834F /* rustdesk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rustdesk; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
CA60D3BC5386D3D7DBD96893 /* naming */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = naming; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
ADDEDBA66A6E2 /* Required for static linking */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ADDEDBA66A6E1 /* libresolv.tbd */,
|
||||||
|
);
|
||||||
|
name = "Required for static linking";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
CA603C4309E122869D176AE5 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */,
|
||||||
|
CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */,
|
||||||
|
CA60D3BC5386D3D7DBD96893 /* naming */,
|
||||||
|
CA60D3BC5386B357B2AB834F /* rustdesk */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
CA603C4309E198AF0B5890DB /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ADDEDBA66A6E2 /* Required for static linking */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
CA603C4309E1D65BC3C892A8 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CA603C4309E13EF4668187A5 /* Cargo.toml */,
|
||||||
|
CA603C4309E122869D176AE5 /* Products */,
|
||||||
|
CA603C4309E198AF0B5890DB /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
CA604C7415FB12977AAB839F /* librustdesk-staticlib */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */;
|
||||||
|
buildPhases = (
|
||||||
|
CA6033723F8212977AAB839F /* Sources */,
|
||||||
|
CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */,
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "librustdesk-staticlib";
|
||||||
|
productName = liblibrustdesk_static.a;
|
||||||
|
productReference = CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */;
|
||||||
|
productType = "com.apple.product-type.library.static";
|
||||||
|
};
|
||||||
|
CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */;
|
||||||
|
buildPhases = (
|
||||||
|
CA6033723F82D6691E4C3FF1 /* Sources */,
|
||||||
|
CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */,
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "librustdesk-cdylib";
|
||||||
|
productName = librustdesk.dylib;
|
||||||
|
productReference = CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */;
|
||||||
|
productType = "com.apple.product-type.library.dynamic";
|
||||||
|
};
|
||||||
|
CA60D3BC5386C858B7409EE3 /* naming-bin */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */;
|
||||||
|
buildPhases = (
|
||||||
|
CA6033723F82C858B7409EE3 /* Sources */,
|
||||||
|
CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */,
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "naming-bin";
|
||||||
|
productName = naming;
|
||||||
|
productReference = CA60D3BC5386D3D7DBD96893 /* naming */;
|
||||||
|
productType = "com.apple.product-type.tool";
|
||||||
|
};
|
||||||
|
CA60D3BC5386C9FA710A2219 /* rustdesk-bin */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */;
|
||||||
|
buildPhases = (
|
||||||
|
CA6033723F82C9FA710A2219 /* Sources */,
|
||||||
|
CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */,
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "rustdesk-bin";
|
||||||
|
productName = rustdesk;
|
||||||
|
productReference = CA60D3BC5386B357B2AB834F /* rustdesk */;
|
||||||
|
productType = "com.apple.product-type.tool";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
CA603C4309E1E04653AD465F /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastUpgradeCheck = 1300;
|
||||||
|
TargetAttributes = {
|
||||||
|
CA604C7415FB12977AAB839F = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
CA6071B5A0F5D6691E4C3FF1 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
CA60D3BC5386C858B7409EE3 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
CA60D3BC5386C9FA710A2219 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */;
|
||||||
|
compatibilityVersion = "Xcode 11.4";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = CA603C4309E1D65BC3C892A8;
|
||||||
|
productRefGroup = CA603C4309E122869D176AE5 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */,
|
||||||
|
CA604C7415FB12977AAB839F /* librustdesk-staticlib */,
|
||||||
|
CA60D3BC5386C858B7409EE3 /* naming-bin */,
|
||||||
|
CA60D3BC5386C9FA710A2219 /* rustdesk-bin */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist",
|
||||||
|
);
|
||||||
|
name = "Universal Binary lipo";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "# generated with cargo-xcode 1.4.1\nset -eux; cat \"$DERIVED_FILE_DIR/$ARCHS-$EXECUTABLE_NAME.xcfilelist\" | tr '\\n' '\\0' | xargs -0 lipo -create -output \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\"";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
CA6033723F8212977AAB839F /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CA6061C6409F12977AAB839F /* Cargo.toml in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
CA6033723F82C858B7409EE3 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
CA6033723F82C9FA710A2219 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
CA6033723F82D6691E4C3FF1 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
CA604B55B26012977AAB839F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a;
|
||||||
|
INSTALL_GROUP = "";
|
||||||
|
INSTALL_MODE_FLAG = "";
|
||||||
|
INSTALL_OWNER = "";
|
||||||
|
PRODUCT_NAME = librustdesk_static;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CA604B55B260C858B7409EE3 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = naming;
|
||||||
|
PRODUCT_NAME = naming;
|
||||||
|
SUPPORTED_PLATFORMS = macosx;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CA604B55B260C9FA710A2219 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = rustdesk;
|
||||||
|
PRODUCT_NAME = rustdesk;
|
||||||
|
SUPPORTED_PLATFORMS = macosx;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CA604B55B260D6691E4C3FF1 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib;
|
||||||
|
PRODUCT_NAME = librustdesk;
|
||||||
|
SUPPORTED_PLATFORMS = macosx;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CA60583BB9CE12977AAB839F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a;
|
||||||
|
INSTALL_GROUP = "";
|
||||||
|
INSTALL_MODE_FLAG = "";
|
||||||
|
INSTALL_OWNER = "";
|
||||||
|
PRODUCT_NAME = librustdesk_static;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
CA60583BB9CEC858B7409EE3 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = naming;
|
||||||
|
PRODUCT_NAME = naming;
|
||||||
|
SUPPORTED_PLATFORMS = macosx;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
CA60583BB9CEC9FA710A2219 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = rustdesk;
|
||||||
|
PRODUCT_NAME = rustdesk;
|
||||||
|
SUPPORTED_PLATFORMS = macosx;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
CA60583BB9CED6691E4C3FF1 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d;
|
||||||
|
CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib;
|
||||||
|
PRODUCT_NAME = librustdesk;
|
||||||
|
SUPPORTED_PLATFORMS = macosx;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
CA608F3F78EE228BE02872F8 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target";
|
||||||
|
CARGO_XCODE_BUILD_MODE = debug;
|
||||||
|
CARGO_XCODE_FEATURES = "";
|
||||||
|
"CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64;
|
||||||
|
"CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686;
|
||||||
|
"CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim";
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
PRODUCT_NAME = rustdesk;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CA608F3F78EE3CC16B37690B /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target";
|
||||||
|
CARGO_XCODE_BUILD_MODE = release;
|
||||||
|
CARGO_XCODE_FEATURES = "";
|
||||||
|
"CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64;
|
||||||
|
"CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686;
|
||||||
|
"CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios;
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim";
|
||||||
|
"CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin;
|
||||||
|
PRODUCT_NAME = rustdesk;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CA60583BB9CE12977AAB839F /* Release */,
|
||||||
|
CA604B55B26012977AAB839F /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CA60583BB9CEC858B7409EE3 /* Release */,
|
||||||
|
CA604B55B260C858B7409EE3 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CA60583BB9CEC9FA710A2219 /* Release */,
|
||||||
|
CA604B55B260C9FA710A2219 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CA60583BB9CED6691E4C3FF1 /* Release */,
|
||||||
|
CA604B55B260D6691E4C3FF1 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CA608F3F78EE3CC16B37690B /* Release */,
|
||||||
|
CA608F3F78EE228BE02872F8 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = CA603C4309E1E04653AD465F /* Project object */;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ description: Your Remote Desktop Software
|
|||||||
|
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
# The following defines the version and build number for your application.
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
@@ -19,93 +19,127 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
version: 1.1.10-1+28
|
version: 1.1.10-1+28
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.16.1"
|
sdk: ">=2.16.1"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
cupertino_icons: ^1.0.3
|
||||||
cupertino_icons: ^1.0.3
|
ffi: ^2.0.1
|
||||||
ffi: ^1.1.2
|
path_provider: ^2.0.2
|
||||||
path_provider: ^2.0.2
|
external_path: ^1.0.1
|
||||||
external_path: ^1.0.1
|
provider: ^6.0.3
|
||||||
provider: ^6.0.3
|
tuple: ^2.0.0
|
||||||
tuple: ^2.0.0
|
wakelock: ^0.5.2
|
||||||
wakelock: ^0.5.2
|
device_info_plus: ^4.0.2
|
||||||
device_info: ^2.0.2
|
firebase_analytics: ^9.1.5
|
||||||
firebase_analytics: ^9.1.5
|
package_info_plus: ^1.4.2
|
||||||
package_info: ^2.0.2
|
url_launcher: ^6.0.9
|
||||||
url_launcher: ^6.0.9
|
shared_preferences: ^2.0.6
|
||||||
shared_preferences: ^2.0.6
|
toggle_switch: ^1.4.0
|
||||||
toggle_switch: ^1.4.0
|
dash_chat_2:
|
||||||
dash_chat_2: ^0.0.12
|
git:
|
||||||
draggable_float_widget: ^0.0.2
|
url: https://github.com/fufesou/Dash-Chat-2
|
||||||
settings_ui: ^2.0.2
|
ref: feat_maxWidth
|
||||||
flutter_breadcrumb: ^1.0.1
|
draggable_float_widget: ^0.0.2
|
||||||
http: ^0.13.4
|
settings_ui: ^2.0.2
|
||||||
qr_code_scanner: ^1.0.0
|
flutter_breadcrumb: ^1.0.1
|
||||||
zxing2: ^0.1.0
|
http: ^0.13.4
|
||||||
image_picker: ^0.8.5
|
qr_code_scanner: ^1.0.0
|
||||||
image: ^3.1.3
|
zxing2: ^0.1.0
|
||||||
flutter_smart_dialog: ^4.3.1
|
image_picker: ^0.8.5
|
||||||
flutter_rust_bridge: ^1.30.0
|
image: ^3.1.3
|
||||||
|
back_button_interceptor: ^6.0.1
|
||||||
|
flutter_rust_bridge:
|
||||||
|
git:
|
||||||
|
url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge
|
||||||
|
ref: master
|
||||||
|
path: frb_dart
|
||||||
|
window_manager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Kingtous/rustdesk_window_manager
|
||||||
|
ref: 799ef079e87938c3f4340591b4330c2598f38bb9
|
||||||
|
desktop_multi_window:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||||
|
ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
||||||
|
freezed_annotation: ^2.0.3
|
||||||
|
tray_manager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Kingtous/rustdesk_tray_manager
|
||||||
|
ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a
|
||||||
|
get: ^4.6.5
|
||||||
|
visibility_detector: ^0.3.3
|
||||||
|
contextmenu: ^3.0.0
|
||||||
|
desktop_drop: ^0.3.3
|
||||||
|
scroll_pos: ^0.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_launcher_icons: ^0.9.1
|
flutter_launcher_icons: ^0.9.1
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
build_runner: ^2.1.11
|
||||||
|
freezed: ^2.0.3
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
# rerun: flutter pub run flutter_launcher_icons:main
|
# rerun: flutter pub run flutter_launcher_icons:main
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: "ic_launcher"
|
android: "ic_launcher"
|
||||||
ios: true
|
ios: true
|
||||||
image_path: "../1024-rec.png"
|
image_path: "../1024-rec.png"
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
# The following section is specific to Flutter.
|
# The following section is specific to Flutter.
|
||||||
flutter:
|
flutter:
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
# To add assets to your application, add an assets section, like this:
|
||||||
# included with your application, so that you can use the icons in
|
assets:
|
||||||
# the material Icons class.
|
- assets/
|
||||||
uses-material-design: true
|
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
fonts:
|
||||||
assets:
|
- family: GestureIcons
|
||||||
- assets/
|
fonts:
|
||||||
|
- asset: assets/gestures.ttf
|
||||||
|
- family: Tabbar
|
||||||
|
fonts:
|
||||||
|
- asset: assets/tabbar.ttf
|
||||||
|
- family: PeerSearchbar
|
||||||
|
fonts:
|
||||||
|
- asset: assets/peer_searchbar.ttf
|
||||||
|
|
||||||
fonts:
|
|
||||||
- family: GestureIcons
|
|
||||||
fonts:
|
|
||||||
- asset: assets/gestures.ttf
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
# For details regarding adding assets from package dependencies, see
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
# example:
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
# fonts:
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
# - family: Schyler
|
# list giving the asset and other descriptors for the font. For
|
||||||
# fonts:
|
# example:
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
# fonts:
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
# - family: Schyler
|
||||||
# style: italic
|
# fonts:
|
||||||
# - family: Trajan Pro
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
# fonts:
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
# - asset: fonts/TrajanPro.ttf
|
# style: italic
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
# - family: Trajan Pro
|
||||||
# weight: 700
|
# fonts:
|
||||||
#
|
# - asset: fonts/TrajanPro.ttf
|
||||||
# For details regarding fonts from package dependencies,
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|||||||
19
flutter/rustdesk.desktop
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.1.10
|
||||||
|
Name=RustDesk
|
||||||
|
GenericName=Remote Desktop
|
||||||
|
Comment=Remote Desktop
|
||||||
|
Exec=/usr/lib/rustdesk/flutter_hbb %u
|
||||||
|
Icon=/usr/share/rustdesk/files/rustdesk.png
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=Network;RemoteAccess;GTK;
|
||||||
|
Keywords=internet;
|
||||||
|
Actions=new-window;
|
||||||
|
|
||||||
|
X-Desktop-File-Install-Version=0.23
|
||||||
|
|
||||||
|
[Desktop Action new-window]
|
||||||
|
Name=Open a New Window
|
||||||
|
|
||||||
16
flutter/rustdesk.service
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RustDesk
|
||||||
|
Requires=network.target
|
||||||
|
After=systemd-user-sessions.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/lib/rustdesk/flutter_hbb --service
|
||||||
|
PIDFile=/run/rustdesk.pid
|
||||||
|
KillMode=mixed
|
||||||
|
TimeoutStopSec=30
|
||||||
|
User=root
|
||||||
|
LimitNOFILE=100000
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
17
flutter/windows/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
101
flutter/windows/CMakeLists.txt
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(flutter_hbb LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "flutter_hbb")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Define build configuration option.
|
||||||
|
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
if(IS_MULTICONFIG)
|
||||||
|
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
# Define settings for the Profile build mode.
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
|
||||||
|
# Use Unicode for all projects.
|
||||||
|
add_definitions(-DUNICODE -D_UNICODE)
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||||
|
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# Support files are copied into place next to the executable, so that it can
|
||||||
|
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||||
|
# so that building and running from within Visual Studio will work.
|
||||||
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
|
# Make the "install" step default, as it's required to run.
|
||||||
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
CONFIGURATIONS Profile;Release
|
||||||
|
COMPONENT Runtime)
|
||||||
104
flutter/windows/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"flutter_export.h"
|
||||||
|
"flutter_windows.h"
|
||||||
|
"flutter_messenger.h"
|
||||||
|
"flutter_plugin_registrar.h"
|
||||||
|
"flutter_texture_registrar.h"
|
||||||
|
)
|
||||||
|
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Wrapper ===
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||||
|
"core_implementations.cc"
|
||||||
|
"standard_codec.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||||
|
"plugin_registrar.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||||
|
"flutter_engine.cc"
|
||||||
|
"flutter_view_controller.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
|
||||||
|
# Wrapper sources needed for a plugin.
|
||||||
|
add_library(flutter_wrapper_plugin STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_plugin)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||||
|
|
||||||
|
# Wrapper sources needed for the runner.
|
||||||
|
add_library(flutter_wrapper_app STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_app)
|
||||||
|
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_app PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||||
|
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
${PHONY_OUTPUT}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
|
windows-x64 $<CONFIG>
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
51
flutter/windows/runner/CMakeLists.txt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME} WIN32
|
||||||
|
"flutter_window.cpp"
|
||||||
|
"main.cpp"
|
||||||
|
"utils.cpp"
|
||||||
|
"win32_window.cpp"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
"Runner.rc"
|
||||||
|
"runner.exe.manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
# flutter_rust_bridge with Corrosion
|
||||||
|
find_package(Corrosion REQUIRED)
|
||||||
|
|
||||||
|
corrosion_import_crate(MANIFEST_PATH ../../../Cargo.toml
|
||||||
|
# Equivalent to --all-features passed to cargo build
|
||||||
|
# [ALL_FEATURES]
|
||||||
|
# Equivalent to --no-default-features passed to cargo build
|
||||||
|
# [NO_DEFAULT_FEATURES]
|
||||||
|
# Disable linking of standard libraries (required for no_std crates).
|
||||||
|
# [NO_STD]
|
||||||
|
# Specify cargo build profile (e.g. release or a custom profile)
|
||||||
|
# [PROFILE <cargo-profile>]
|
||||||
|
# Only import the specified crates from a workspace
|
||||||
|
# [CRATES <crate1> ... <crateN>]
|
||||||
|
# Enable the specified features
|
||||||
|
# [FEATURES <feature1> ... <featureN>]
|
||||||
|
)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE librustdesk)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
# Add dependency libraries and include directories. Add any application-specific
|
||||||
|
# dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
121
flutter/windows/runner/Runner.rc
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#pragma code_page(65001)
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#include "winres.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"resource.h\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#include ""winres.h""\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Icon
|
||||||
|
//
|
||||||
|
|
||||||
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
|
// remains consistent on all systems.
|
||||||
|
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Version
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifdef FLUTTER_BUILD_NUMBER
|
||||||
|
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_NUMBER 1,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef FLUTTER_BUILD_NAME
|
||||||
|
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_AS_NUMBER
|
||||||
|
PRODUCTVERSION VERSION_AS_NUMBER
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904e4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "com.carriez" "\0"
|
||||||
|
VALUE "FileDescription", "flutter_hbb" "\0"
|
||||||
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
|
VALUE "InternalName", "flutter_hbb" "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2022 com.carriez. All rights reserved." "\0"
|
||||||
|
VALUE "OriginalFilename", "flutter_hbb.exe" "\0"
|
||||||
|
VALUE "ProductName", "flutter_hbb" "\0"
|
||||||
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
||||||
61
flutter/windows/runner/flutter_window.cpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
|
: project_(project) {}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() {}
|
||||||
|
|
||||||
|
bool FlutterWindow::OnCreate() {
|
||||||
|
if (!Win32Window::OnCreate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
// The size here must match the window dimensions to avoid unnecessary surface
|
||||||
|
// creation / destruction in the startup path.
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||||
|
// Ensure that basic setup of the controller was successful.
|
||||||
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::OnDestroy() {
|
||||||
|
if (flutter_controller_) {
|
||||||
|
flutter_controller_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||||
|
if (flutter_controller_) {
|
||||||
|
std::optional<LRESULT> result =
|
||||||
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||||
|
lparam);
|
||||||
|
if (result) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||