Merge remote-tracking branch 'github/master' into sigma

This commit is contained in:
sjpark 2023-02-28 16:00:31 +09:00
commit 19d1214a05
87 changed files with 3236 additions and 584 deletions

View File

@ -18,7 +18,7 @@ on:
env: env:
LLVM_VERSION: "15.0.6" LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.7.0" FLUTTER_VERSION: "3.7.5"
# vcpkg version: 2022.05.10 # vcpkg version: 2022.05.10
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
@ -260,7 +260,7 @@ jobs:
job: job:
- { - {
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
os: ubuntu-18.04, os: ubuntu-20.04,
extra-build-args: "", extra-build-args: "",
} }
steps: steps:
@ -330,13 +330,13 @@ jobs:
- { - {
arch: x86_64, arch: x86_64,
target: aarch64-linux-android, target: aarch64-linux-android,
os: ubuntu-18.04, os: ubuntu-20.04,
extra-build-features: "", extra-build-features: "",
} }
# - { # - {
# arch: x86_64, # arch: x86_64,
# target: armv7-linux-androideabi, # target: armv7-linux-androideabi,
# os: ubuntu-18.04, # os: ubuntu-20.04,
# extra-build-features: "", # extra-build-features: "",
# } # }
steps: steps:
@ -907,19 +907,19 @@ jobs:
- { - {
arch: x86_64, arch: x86_64,
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
os: ubuntu-18.04, os: ubuntu-20.04,
extra-build-features: "", extra-build-features: "",
} }
- { - {
arch: x86_64, arch: x86_64,
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
os: ubuntu-18.04, os: ubuntu-20.04,
extra-build-features: "flatpak", extra-build-features: "flatpak",
} }
- { - {
arch: x86_64, arch: x86_64,
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
os: ubuntu-18.04, os: ubuntu-20.04,
extra-build-features: "appimage", extra-build-features: "appimage",
} }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }

View File

@ -732,7 +732,7 @@ jobs:
x86_64) x86_64)
# no need mock on x86_64 # no need mock on x86_64
export VCPKG_ROOT=/opt/artifacts/vcpkg export VCPKG_ROOT=/opt/artifacts/vcpkg
cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release
;; ;;
esac esac
@ -900,7 +900,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm64-linux/include ln -s /usr/include /vcpkg/installed/arm64-linux/include
export VCPKG_ROOT=/vcpkg export VCPKG_ROOT=/vcpkg
# disable hwcodec for compilation # disable hwcodec for compilation
cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release
;; ;;
armv7) armv7)
cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/
@ -910,7 +910,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm-linux/include ln -s /usr/include /vcpkg/installed/arm-linux/include
export VCPKG_ROOT=/vcpkg export VCPKG_ROOT=/vcpkg
# disable hwcodec for compilation # disable hwcodec for compilation
cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release
;; ;;
esac esac

View File

@ -132,6 +132,7 @@ flutter_rust_bridge = "1.61.1"
[workspace] [workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"]
exclude = ["vdi/host"]
[package.metadata.winres] [package.metadata.winres]
LegalCopyright = "Copyright © 2022 Purslane, Inc." LegalCopyright = "Copyright © 2022 Purslane, Inc."

184
build.py
View File

@ -18,14 +18,11 @@ exe_path = 'target/release/' + hbb_name
flutter_win_target_dir = 'flutter/build/windows/runner/Release/' flutter_win_target_dir = 'flutter/build/windows/runner/Release/'
skip_cargo = False skip_cargo = False
def custom_os_system(cmd): def system2(cmd):
err = os._system(cmd) err = os.system(cmd)
if err != 0: if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.") print(f"Error occurred when executing: {cmd}. Exiting.")
sys.exit(-1) sys.exit(-1)
# replace prebuilt os.system
os._system = os.system
os.system = custom_os_system
def get_version(): def get_version():
with open("Cargo.toml", encoding="utf-8") as fh: with open("Cargo.toml", encoding="utf-8") as fh:
@ -144,8 +141,8 @@ def generate_build_script_for_docker():
# build rustdesk # build rustdesk
./build.py --flutter --hwcodec ./build.py --flutter --hwcodec
''') ''')
os.system("chmod +x /tmp/build.sh") system2("chmod +x /tmp/build.sh")
os.system("bash /tmp/build.sh") system2("bash /tmp/build.sh")
def download_extract_features(features, res_dir): def download_extract_features(features, res_dir):
@ -250,7 +247,7 @@ def get_features(args):
def generate_control_file(version): def generate_control_file(version):
control_file_path = "../res/DEBIAN/control" control_file_path = "../res/DEBIAN/control"
os.system('/bin/rm -rf %s' % control_file_path) system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk content = """Package: rustdesk
Version: %s Version: %s
@ -268,45 +265,45 @@ Description: A remote control software.
def ffi_bindgen_function_refactor(): def ffi_bindgen_function_refactor():
# workaround ffigen # workaround ffigen
os.system( system2(
'sed -i "s/ffi.NativeFunction<ffi.Bool Function(DartPort/ffi.NativeFunction<ffi.Uint8 Function(DartPort/g" flutter/lib/generated_bridge.dart') 'sed -i "s/ffi.NativeFunction<ffi.Bool Function(DartPort/ffi.NativeFunction<ffi.Uint8 Function(DartPort/g" flutter/lib/generated_bridge.dart')
def build_flutter_deb(version, features): def build_flutter_deb(version, features):
if not skip_cargo: if not skip_cargo:
os.system(f'cargo build --features {features} --lib --release') system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor() ffi_bindgen_function_refactor()
os.chdir('flutter') os.chdir('flutter')
os.system('flutter build linux --release') system2('flutter build linux --release')
os.system('mkdir -p tmpdeb/usr/bin/') system2('mkdir -p tmpdeb/usr/bin/')
os.system('mkdir -p tmpdeb/usr/lib/rustdesk') system2('mkdir -p tmpdeb/usr/lib/rustdesk')
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system('mkdir -p tmpdeb/usr/share/applications/') system2('mkdir -p tmpdeb/usr/share/applications/')
os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
os.system('rm tmpdeb/usr/bin/rustdesk || true') system2('rm tmpdeb/usr/bin/rustdesk || true')
os.system( system2(
'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/')
os.system( system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
os.system( system2(
'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') 'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
os.system( system2(
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
os.system( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
os.system( system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
os.system( system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
os.system('mkdir -p tmpdeb/DEBIAN') system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version) generate_control_file(version)
os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
os.system('dpkg-deb -b tmpdeb rustdesk.deb;') system2('dpkg-deb -b tmpdeb rustdesk.deb;')
os.system('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf tmpdeb/')
os.system('/bin/rm -rf ../res/DEBIAN/control') system2('/bin/rm -rf ../res/DEBIAN/control')
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..") os.chdir("..")
@ -314,16 +311,16 @@ def build_flutter_deb(version, features):
def build_flutter_dmg(version, features): def build_flutter_dmg(version, features):
if not skip_cargo: if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
# copy dylib # copy dylib
os.system( system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
# ffi_bindgen_function_refactor() # ffi_bindgen_function_refactor()
# limitations from flutter rust bridge # limitations from flutter rust bridge
os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') system2('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h')
os.chdir('flutter') os.chdir('flutter')
os.system('flutter build macos --release') system2('flutter build macos --release')
os.system( system2(
"create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
os.chdir("..") os.chdir("..")
@ -331,29 +328,29 @@ def build_flutter_dmg(version, features):
def build_flutter_arch_manjaro(version, features): def build_flutter_arch_manjaro(version, features):
if not skip_cargo: if not skip_cargo:
os.system(f'cargo build --features {features} --lib --release') system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor() ffi_bindgen_function_refactor()
os.chdir('flutter') os.chdir('flutter')
os.system('flutter build linux --release') system2('flutter build linux --release')
os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so') system2('strip build/linux/x64/release/bundle/lib/librustdesk.so')
os.chdir('../res') os.chdir('../res')
os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f') system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
def build_flutter_windows(version, features): def build_flutter_windows(version, features):
if not skip_cargo: if not skip_cargo:
os.system(f'cargo build --features {features} --lib --release') system2(f'cargo build --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"): if not os.path.exists("target/release/librustdesk.dll"):
print("cargo build failed, please check rust source code.") print("cargo build failed, please check rust source code.")
exit(-1) exit(-1)
os.chdir('flutter') os.chdir('flutter')
os.system('flutter build windows --release') system2('flutter build windows --release')
os.chdir('..') os.chdir('..')
shutil.copy2('target/release/deps/dylib_virtual_display.dll', shutil.copy2('target/release/deps/dylib_virtual_display.dll',
flutter_win_target_dir) flutter_win_target_dir)
os.chdir('libs/portable') os.chdir('libs/portable')
os.system('pip3 install -r requirements.txt') system2('pip3 install -r requirements.txt')
os.system( system2(
f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe')
os.chdir('../..') os.chdir('../..')
if os.path.exists('./rustdesk_portable.exe'): if os.path.exists('./rustdesk_portable.exe'):
@ -374,22 +371,15 @@ def main():
parser = make_parser() parser = make_parser()
args = parser.parse_args() args = parser.parse_args()
shutil.copy2('Cargo.toml', 'Cargo.toml.bk')
shutil.copy2('src/main.rs', 'src/main.rs.bk')
if windows:
txt = open('src/main.rs', encoding='utf8').read()
with open('src/main.rs', 'wt', encoding='utf8') as fh:
fh.write(txt.replace(
'//#![windows_subsystem', '#![windows_subsystem'))
if os.path.exists(exe_path): if os.path.exists(exe_path):
os.unlink(exe_path) os.unlink(exe_path)
if os.path.isfile('/usr/bin/pacman'): if os.path.isfile('/usr/bin/pacman'):
os.system('git checkout src/ui/common.tis') system2('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 flutter = args.flutter
if not flutter: if not flutter:
os.system('python3 res/inline-sciter.py') system2('python3 res/inline-sciter.py')
print(args.skip_cargo) print(args.skip_cargo)
if args.skip_cargo: if args.skip_cargo:
skip_cargo = True skip_cargo = True
@ -397,55 +387,55 @@ def main():
if windows: if windows:
# build virtual display dynamic library # build virtual display dynamic library
os.chdir('libs/virtual_display/dylib') os.chdir('libs/virtual_display/dylib')
os.system('cargo build --release') system2('cargo build --release')
os.chdir('../../..') os.chdir('../../..')
if flutter: if flutter:
build_flutter_windows(version, features) build_flutter_windows(version, features)
return return
os.system('cargo build --release --features ' + features) system2('cargo build --release --features ' + features)
# os.system('upx.exe target/release/rustdesk.exe') # system2('upx.exe target/release/rustdesk.exe')
os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P') pa = os.environ.get('P')
if pa: if pa:
os.system( system2(
f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com '
'target\\release\\rustdesk.exe') 'target\\release\\rustdesk.exe')
else: else:
print('Not signed') print('Not signed')
os.system( system2(
f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe')
elif os.path.isfile('/usr/bin/pacman'): elif os.path.isfile('/usr/bin/pacman'):
# pacman -S -needed base-devel # pacman -S -needed base-devel
os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
if flutter: if flutter:
build_flutter_arch_manjaro(version, features) build_flutter_arch_manjaro(version, features)
else: else:
os.system('cargo build --release --features ' + features) system2('cargo build --release --features ' + features)
os.system('git checkout src/ui/common.tis') system2('git checkout src/ui/common.tis')
os.system('strip target/release/rustdesk') system2('strip target/release/rustdesk')
os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
os.system('HBB=`pwd` makepkg -f') system2('HBB=`pwd` makepkg -f')
os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (
version, version)) version, version))
# pacman -U ./rustdesk.pkg.tar.zst # 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) system2('cargo build --release --features ' + features)
os.system('strip target/release/rustdesk') system2('strip target/release/rustdesk')
os.system( system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') system2('HBB=`pwd` rpmbuild -ba res/rpm.spec')
os.system( system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % (
version, version)) version, version))
# yum localinstall rustdesk.rpm # yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'): elif os.path.isfile('/usr/bin/zypper'):
os.system('cargo build --release --features ' + features) system2('cargo build --release --features ' + features)
os.system('strip target/release/rustdesk') system2('strip target/release/rustdesk')
os.system( system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec')
os.system( system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (
version, version)) version, version))
# yum localinstall rustdesk.rpm # yum localinstall rustdesk.rpm
@ -455,18 +445,18 @@ def main():
build_flutter_dmg(version, features) build_flutter_dmg(version, features)
pass pass
else: else:
# os.system( # system2(
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
build_flutter_deb(version, features) build_flutter_deb(version, features)
else: else:
os.system('cargo bundle --release --features ' + features) system2('cargo bundle --release --features ' + features)
if osx: if osx:
os.system( system2(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
os.system( system2(
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# https://github.com/sindresorhus/create-dmg # https://github.com/sindresorhus/create-dmg
os.system('/bin/rm -rf *.dmg') system2('/bin/rm -rf *.dmg')
plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist"
txt = open(plist).read() txt = open(plist).read()
with open(plist, "wt") as fh: with open(plist, "wt") as fh:
@ -476,7 +466,7 @@ def main():
</dict>""")) </dict>"""))
pa = os.environ.get('P') pa = os.environ.get('P')
if pa: if pa:
os.system(''' system2('''
# buggy: rcodesign sign ... path/*, have to sign one by one # buggy: rcodesign sign ... path/*, have to sign one by one
# install rcodesign via cargo install apple-codesign # install rcodesign via cargo install apple-codesign
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk
@ -486,11 +476,11 @@ def main():
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/Contents/MacOS/*
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
'''.format(pa)) '''.format(pa))
os.system('create-dmg target/release/bundle/osx/RustDesk.app') system2('create-dmg target/release/bundle/osx/RustDesk.app')
os.rename('RustDesk %s.dmg' % os.rename('RustDesk %s.dmg' %
version, 'rustdesk-%s.dmg' % version) version, 'rustdesk-%s.dmg' % version)
if pa: if pa:
os.system(''' system2('''
# https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html
# https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html
# https://developer.apple.com/developer-id/ # https://developer.apple.com/developer-id/
@ -507,34 +497,32 @@ def main():
print('Not signed') print('Not signed')
else: else:
# buid deb package # buid deb package
os.system( system2(
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
os.system('dpkg-deb -R rustdesk.deb tmpdeb') system2('dpkg-deb -R rustdesk.deb tmpdeb')
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system( system2(
'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
os.system( system2(
'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
os.system( system2(
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
os.system( system2(
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
os.system('strip tmpdeb/usr/bin/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk')
os.system('mkdir -p tmpdeb/usr/lib/rustdesk') system2('mkdir -p tmpdeb/usr/lib/rustdesk')
os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') system2('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')
md5_file('usr/lib/rustdesk/libsciter-gtk.so') md5_file('usr/lib/rustdesk/libsciter-gtk.so')
os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
os.system("mv Cargo.toml.bk Cargo.toml")
os.system("mv src/main.rs.bk src/main.rs")
def md5_file(fn): def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
os.system('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
if __name__ == "__main__": if __name__ == "__main__":

50
docs/CONTRIBUTING-DE.md Normal file
View File

@ -0,0 +1,50 @@
# Beiträge zu RustDesk
RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns
helfen möchten:
## Beiträge
Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull
Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur
(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den
Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle
Beiträge sollten diesem Format folgen, auch die von Hauptakteuren.
Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem
Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert
werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden.
## Checkliste für Pull Requests
- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum
aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das
Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie
möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten.
- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass
jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte
sich übersetzen lassen und Tests bestehen).
- Commits sollten von einem "Herkunftszertifikat für Entwickler"
(https://developercertificate.org) begleitet werden, das besagt, dass Sie (und
ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE)
einverstanden sind. In Git ist dies die Option `-s` für `git commit`.
- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur
Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine
Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch
per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten.
- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue
Funktion beziehen.
Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow).
## Verhalten
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## Kommunikation
RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV).

14
docs/DEVCONTAINER-DE.md Normal file
View File

@ -0,0 +1,14 @@
Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binärprogramm im Debug-Modus erstellt.
Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an.
Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgeführt werden müssen, um bestimmte Builds zu erstellen.
Kommando|Build-Typ|Modus
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@ -17,9 +17,9 @@ RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the b
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn du Unterstützung beim Start brauchst.
[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) [**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases)
@ -41,6 +41,14 @@ Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann se
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM | | Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM |
## Dev-Container
[![In Dev-Containern öffnen](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Wenn du VS Code und Docker bereits installiert hast, kannst du auf das Abzeichen oben klicken, um loszulegen. Wenn du darauf klickst, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen.
Weitere Informationen findest du in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md).
## Abhängigkeiten ## Abhängigkeiten
Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.

View File

@ -26,6 +26,7 @@
android:exported="false"> android:exported="false">
<intent-filter android:priority="1000"> <intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
</receiver> </receiver>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="298.171 98.799 32 32" width="32pt" height="32pt"><g><path d=" M 298.171 98.799 L 330.171 98.799 L 330.171 130.799 L 298.171 130.799 L 298.171 98.799 Z " fill="none"/><path d=" M 310.278 119.949 L 321.825 119.949 C 322.772 119.949 323.542 119.18 323.542 118.233 L 323.542 118.233 C 323.542 117.285 322.772 116.516 321.825 116.516 L 307.25 116.516 C 304.69 116.516 304.031 118.035 305.779 119.905 L 311.537 126.064 C 312.207 126.734 313.295 126.734 313.965 126.064 L 313.965 126.064 C 314.635 125.394 314.635 124.306 313.965 123.636 L 310.278 119.949 Z M 318.065 109.649 L 306.518 109.649 C 305.57 109.649 304.801 110.419 304.801 111.366 L 304.801 111.366 C 304.801 112.313 305.57 113.083 306.518 113.083 L 321.092 113.083 C 323.653 113.083 324.312 111.564 322.563 109.693 L 316.806 103.535 C 316.136 102.865 315.048 102.865 314.378 103.535 L 314.378 103.535 C 313.708 104.205 313.708 105.293 314.378 105.963 L 318.065 109.649 Z " fill-rule="evenodd" fill="rgb(0,0,0)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -189,7 +189,9 @@ class MyTheme {
style: ButtonStyle(splashFactory: NoSplash.splashFactory), style: ButtonStyle(splashFactory: NoSplash.splashFactory),
) )
: null, : null,
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith( colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.blue,
).copyWith(
brightness: Brightness.light, brightness: Brightness.light,
background: Color(0xFFEEEEEE), background: Color(0xFFEEEEEE),
), ),
@ -229,9 +231,11 @@ class MyTheme {
checkboxTheme: checkboxTheme:
const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)),
colorScheme: ColorScheme.fromSwatch( colorScheme: ColorScheme.fromSwatch(
brightness: Brightness.dark,
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
).copyWith(background: Color(0xFF24252B)), ).copyWith(
brightness: Brightness.dark,
background: Color(0xFF24252B),
),
).copyWith( ).copyWith(
extensions: <ThemeExtension<dynamic>>[ extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.dark, ColorThemeExtension.dark,
@ -1453,10 +1457,12 @@ connectMainDesktop(String id,
connect(BuildContext context, String id, connect(BuildContext context, String id,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false, bool isRDP = false}) async {
bool forceRelay = false}) async {
if (id == '') return; if (id == '') return;
id = id.replaceAll(' ', ''); id = id.replaceAll(' ', '');
final oldId = id;
id = await bind.mainHandleRelayId(id: id);
final forceRelay = id != oldId;
assert(!(isFileTransfer && isTcpTunneling && isRDP), assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type"); "more than one connect type");

View File

@ -534,7 +534,7 @@ abstract class BasePeerCard extends StatelessWidget {
proc: () { proc: () {
() async { () async {
if (isLan) { if (isLan) {
// TODO bind.mainRemoveDiscovered(id: id);
} else { } else {
final favs = (await bind.mainGetFav()).toList(); final favs = (await bind.mainGetFav()).toList();
if (favs.remove(id)) { if (favs.remove(id)) {
@ -745,12 +745,9 @@ class RecentPeerCard extends BasePeerCard {
} }
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
// if (!gFFI.abModel.idContainBy(peer.id)) { if (!gFFI.abModel.idContainBy(peer.id)) {
// menuItems.add(_addToAb(peer)); menuItems.add(_addToAb(peer));
// } else { }
// menuItems.add(_removeFromAb(peer));
// }
menuItems.add(_addToAb(peer));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
@ -797,12 +794,9 @@ class FavoritePeerCard extends BasePeerCard {
})); }));
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
// if (!gFFI.abModel.idContainBy(peer.id)) { if (!gFFI.abModel.idContainBy(peer.id)) {
// menuItems.add(_addToAb(peer)); menuItems.add(_addToAb(peer));
// } else { }
// menuItems.add(_removeFromAb(peer));
// }
menuItems.add(_addToAb(peer));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
@ -843,23 +837,27 @@ class DiscoveredPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
if (!favs.contains(peer.id)) { final inRecent = await bind.mainIsInRecentPeers(id: peer.id);
menuItems.add(_addFavAction(peer.id)); if (inRecent) {
} else { if (!favs.contains(peer.id)) {
menuItems.add(_rmFavAction(peer.id, () async {})); menuItems.add(_addFavAction(peer.id));
} else {
menuItems.add(_rmFavAction(peer.id, () async {}));
}
} }
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
// if (!gFFI.abModel.idContainBy(peer.id)) { if (!gFFI.abModel.idContainBy(peer.id)) {
// menuItems.add(_addToAb(peer)); menuItems.add(_addToAb(peer));
// } else { }
// menuItems.add(_removeFromAb(peer));
// }
menuItems.add(_addToAb(peer));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
menuItems.add(_removeAction(peer.id, () async {})); menuItems.add(
_removeAction(peer.id, () async {
await bind.mainLoadLanPeers();
}, isLan: true),
);
return menuItems; return menuItems;
} }

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0; const double kDesktopRemoteTabBarHeight = 28.0;
const int kMainWindowId = 0; const int kMainWindowId = 0;
@ -58,6 +59,11 @@ const double kDesktopFileTransferMaximumWidth = 300;
const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferRowHeight = 30.0;
const double kDesktopFileTransferHeaderHeight = 25.0; const double kDesktopFileTransferHeaderHeight = 25.0;
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && Platform.isLinux
? stateGlobal.fullscreen || stateGlobal.maximize
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space // https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0; const int $nbsp = 0x00A0;
@ -79,6 +85,7 @@ const kDefaultScrollAmountMultiplier = 5.0;
const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0; const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0;
const kWindowBorderWidth = 1.0; const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);

View File

@ -151,10 +151,7 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Connects to the selected peer. /// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) { void onConnect({bool isFileTransfer = false}) {
var id = _idController.id; var id = _idController.id;
var forceRelay = id.endsWith(r'/r'); connect(context, id, isFileTransfer: isFileTransfer);
if (forceRelay) id = id.substring(0, id.length - 2);
connect(context, id,
isFileTransfer: isFileTransfer, forceRelay: forceRelay);
} }
/// UI for the remote ID TextField. /// UI for the remote ID TextField.

View File

@ -75,7 +75,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
isClose: false, isClose: false,
), ),
))); )));
return Platform.isMacOS return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: Obx( : Obx(
() => DragToResizeArea( () => DragToResizeArea(

View File

@ -567,161 +567,187 @@ class _FileManagerPageState extends State<FileManagerPage>
return false; return false;
} }
Widget generateCard(Widget child) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(
Radius.circular(15.0),
),
),
child: child,
);
}
/// transfer status list /// transfer status list
/// watch transfer status /// watch transfer status
Widget statusList() { Widget statusList() {
return PreferredSize( return PreferredSize(
preferredSize: const Size(200, double.infinity), preferredSize: const Size(200, double.infinity),
child: Container(
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
padding: const EdgeInsets.all(8.0),
child: model.jobTable.isEmpty child: model.jobTable.isEmpty
? Center(child: Text(translate("Empty"))) ? generateCard(
: Container( Center(
margin: child: Column(
const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), mainAxisAlignment: MainAxisAlignment.center,
padding: const EdgeInsets.all(8.0), children: [
child: Obx( SvgPicture.asset(
() => ListView.builder( "assets/transfer.svg",
controller: ScrollController(), color: Theme.of(context).tabBarTheme.labelColor,
itemBuilder: (BuildContext context, int index) { height: 40,
final item = model.jobTable[index]; ).paddingOnly(bottom: 10),
return Padding( Text(
padding: const EdgeInsets.only(bottom: 5), translate("No transfers in progress"),
child: Container( textAlign: TextAlign.center,
decoration: BoxDecoration( textScaleFactor: 1.20,
color: Theme.of(context).cardColor, style: TextStyle(
borderRadius: BorderRadius.all( color: Theme.of(context).tabBarTheme.labelColor),
Radius.circular(15.0), ),
), ],
), ),
child: Column( ),
mainAxisSize: MainAxisSize.min, )
children: [ : Obx(
Row( () => ListView.builder(
crossAxisAlignment: CrossAxisAlignment.center, controller: ScrollController(),
children: [ itemBuilder: (BuildContext context, int index) {
Transform.rotate( final item = model.jobTable[index];
angle: item.isRemote ? pi : 0, return Padding(
child: SvgPicture.asset( padding: const EdgeInsets.only(bottom: 5),
"assets/arrow.svg", child: generateCard(
color: Theme.of(context) Column(
.tabBarTheme mainAxisSize: MainAxisSize.min,
.labelColor, children: [
), Row(
).paddingOnly(left: 15), crossAxisAlignment: CrossAxisAlignment.center,
const SizedBox( children: [
width: 16.0, Transform.rotate(
angle: item.isRemote ? pi : 0,
child: SvgPicture.asset(
"assets/arrow.svg",
color: Theme.of(context)
.tabBarTheme
.labelColor,
), ),
Expanded( ).paddingOnly(left: 15),
child: Column( const SizedBox(
mainAxisSize: MainAxisSize.min, width: 16.0,
crossAxisAlignment: ),
CrossAxisAlignment.start, Expanded(
children: [ child: Column(
Tooltip( mainAxisSize: MainAxisSize.min,
waitDuration: crossAxisAlignment:
Duration(milliseconds: 500), CrossAxisAlignment.start,
message: item.jobName, children: [
child: Text( Tooltip(
item.jobName, waitDuration:
maxLines: 1, Duration(milliseconds: 500),
overflow: TextOverflow.ellipsis, message: item.jobName,
).paddingSymmetric(vertical: 10), child: Text(
item.jobName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).paddingSymmetric(vertical: 10),
),
Text(
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
), ),
Text( ),
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', Offstage(
offstage:
item.state != JobState.inProgress,
child: Text(
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: MyTheme.darkGray,
), ),
), ),
Offstage( ),
offstage:
item.state != JobState.inProgress,
child: Text(
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage:
item.state == JobState.inProgress,
child: Text(
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage:
item.state != JobState.inProgress,
child: LinearPercentIndicator(
padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true,
center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
),
barRadius: Radius.circular(15),
percent: item.finishedSize /
item.totalSize,
progressColor: MyTheme.accent,
backgroundColor:
Theme.of(context).hoverColor,
lineHeight:
kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Offstage( Offstage(
offstage: item.state != JobState.paused, offstage:
child: MenuButton( item.state == JobState.inProgress,
onPressed: () { child: Text(
model.resumeJob(item.id); translate(
}, item.display(),
child: SvgPicture.asset( ),
"assets/refresh.svg", style: TextStyle(
color: Colors.white, fontSize: 12,
color: MyTheme.darkGray,
), ),
color: MyTheme.accent,
hoverColor: MyTheme.accent80,
), ),
), ),
MenuButton( Offstage(
padding: EdgeInsets.only(right: 15), offstage:
child: SvgPicture.asset( item.state != JobState.inProgress,
"assets/close.svg", child: LinearPercentIndicator(
color: Colors.white, padding: EdgeInsets.only(right: 15),
), animateFromLastPercent: true,
onPressed: () { center: Text(
model.jobTable.removeAt(index); '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
model.cancelJob(item.id); ),
}, barRadius: Radius.circular(15),
color: MyTheme.accent, percent: item.finishedSize /
hoverColor: MyTheme.accent80, item.totalSize,
progressColor: MyTheme.accent,
backgroundColor:
Theme.of(context).hoverColor,
lineHeight:
kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15),
), ),
], ],
), ),
], ),
), Row(
], mainAxisAlignment: MainAxisAlignment.end,
).paddingSymmetric(vertical: 10), children: [
), Offstage(
); offstage: item.state != JobState.paused,
}, child: MenuButton(
itemCount: model.jobTable.length, onPressed: () {
), model.resumeJob(item.id);
},
child: SvgPicture.asset(
"assets/refresh.svg",
color: Colors.white,
),
color: MyTheme.accent,
hoverColor: MyTheme.accent80,
),
),
MenuButton(
padding: EdgeInsets.only(right: 15),
child: SvgPicture.asset(
"assets/close.svg",
color: Colors.white,
),
onPressed: () {
model.jobTable.removeAt(index);
model.cancelJob(item.id);
},
color: MyTheme.accent,
hoverColor: MyTheme.accent80,
),
],
),
],
),
],
).paddingSymmetric(vertical: 10),
),
);
},
itemCount: model.jobTable.length,
), ),
)); ),
),
);
} }
Widget headTools(bool isLocal) { Widget headTools(bool isLocal) {
@ -1028,7 +1054,9 @@ class _FileManagerPageState extends State<FileManagerPage>
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle( style: TextStyle(
color: selectedItems.length == 0 color: selectedItems.length == 0
? MyTheme.darkGray ? Theme.of(context).brightness == Brightness.light
? MyTheme.grayBg
: MyTheme.darkGray
: Colors.white, : Colors.white,
), ),
) )
@ -1037,7 +1065,9 @@ class _FileManagerPageState extends State<FileManagerPage>
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/arrow.svg", "assets/arrow.svg",
color: selectedItems.length == 0 color: selectedItems.length == 0
? MyTheme.darkGray ? Theme.of(context).brightness == Brightness.light
? MyTheme.grayBg
: MyTheme.darkGray
: Colors.white, : Colors.white,
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
), ),
@ -1046,14 +1076,18 @@ class _FileManagerPageState extends State<FileManagerPage>
? SvgPicture.asset( ? SvgPicture.asset(
"assets/arrow.svg", "assets/arrow.svg",
color: selectedItems.length == 0 color: selectedItems.length == 0
? MyTheme.darkGray ? Theme.of(context).brightness == Brightness.light
? MyTheme.grayBg
: MyTheme.darkGray
: Colors.white, : Colors.white,
) )
: Text( : Text(
translate('Receive'), translate('Receive'),
style: TextStyle( style: TextStyle(
color: selectedItems.length == 0 color: selectedItems.length == 0
? MyTheme.darkGray ? Theme.of(context).brightness == Brightness.light
? MyTheme.grayBg
: MyTheme.darkGray
: Colors.white, : Colors.white,
), ),
), ),

View File

@ -98,7 +98,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
labelGetter: DesktopTab.labelGetterAlias, labelGetter: DesktopTab.labelGetterAlias,
)), )),
); );
return Platform.isMacOS return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: SubWindowDragToResizeArea( : SubWindowDragToResizeArea(
child: tabWidget, child: tabWidget,

View File

@ -46,15 +46,19 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
final double em = 13; final double em = 13;
final btnFontSize = 0.9 * em; final btnFontSize = 0.9 * em;
final double button_radius = 6; final double button_radius = 6;
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
final buttonStyle = OutlinedButton.styleFrom( final buttonStyle = OutlinedButton.styleFrom(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(button_radius)), borderRadius: BorderRadius.all(Radius.circular(button_radius)),
)); ));
final inputBorder = OutlineInputBorder( final inputBorder = OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: Colors.black12)); borderSide:
BorderSide(color: isDarkTheme ? Colors.white70 : Colors.black12));
final textColor = isDarkTheme ? null : Colors.black87;
final dividerColor = isDarkTheme ? Colors.white70 : Colors.black87;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: null,
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -91,8 +95,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
style: buttonStyle, style: buttonStyle,
child: Text(translate('Change Path'), child: Text(translate('Change Path'),
style: TextStyle( style: TextStyle(
color: Colors.black87, color: textColor, fontSize: btnFontSize)))
fontSize: btnFontSize)))
.marginOnly(left: em)) .marginOnly(left: em))
], ],
).marginSymmetric(vertical: 2 * em), ).marginSymmetric(vertical: 2 * em),
@ -127,8 +130,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
)).marginOnly(top: 2 * em), )).marginOnly(top: 2 * em),
Row(children: [Text(translate('agreement_tip'))]) Row(children: [Text(translate('agreement_tip'))])
.marginOnly(top: em), .marginOnly(top: em),
Divider(color: Colors.black87) Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em),
.marginSymmetric(vertical: 0.5 * em),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -143,8 +145,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
style: buttonStyle, style: buttonStyle,
child: Text(translate('Cancel'), child: Text(translate('Cancel'),
style: TextStyle( style: TextStyle(
color: Colors.black87, color: textColor, fontSize: btnFontSize)))
fontSize: btnFontSize)))
.marginOnly(right: 2 * em)), .marginOnly(right: 2 * em)),
Obx(() => ElevatedButton( Obx(() => ElevatedButton(
onPressed: btnEnabled.value ? install : null, onPressed: btnEnabled.value ? install : null,
@ -167,8 +168,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
style: buttonStyle, style: buttonStyle,
child: Text(translate('Run without install'), child: Text(translate('Run without install'),
style: TextStyle( style: TextStyle(
color: Colors.black87, color: textColor, fontSize: btnFontSize)))
fontSize: btnFontSize)))
.marginOnly(left: 2 * em)), .marginOnly(left: 2 * em)),
), ),
], ],

View File

@ -107,13 +107,15 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
labelGetter: DesktopTab.labelGetterAlias, labelGetter: DesktopTab.labelGetterAlias,
)), )),
); );
return Platform.isMacOS return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: SubWindowDragToResizeArea( : Obx(
child: tabWidget, () => SubWindowDragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, child: tabWidget,
windowId: stateGlobal.windowId, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
); windowId: stateGlobal.windowId,
),
);
} }
void onRemoveId(String id) { void onRemoveId(String id) {

View File

@ -205,11 +205,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
), ),
), ),
); );
return Platform.isMacOS return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: Obx(() => SubWindowDragToResizeArea( : Obx(() => SubWindowDragToResizeArea(
key: contentKey, key: contentKey,
child: tabWidget, child: tabWidget,
// Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
)); ));

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
@ -26,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.canvasModel), ChangeNotifierProvider.value(value: gFFI.canvasModel),
], ],
child: Scaffold( child: Scaffold(
// Set transparent background for padding the resize area out of the flutter view.
// This allows the wallpaper goes through our resize area. (Linux only now).
backgroundColor: Platform.isLinux ? Colors.transparent : null,
body: ConnectionTabPage( body: ConnectionTabPage(
params: params, params: params,
), ),

View File

@ -37,7 +37,7 @@ class _MenuButtonState extends State<MenuButton> {
message: widget.tooltip, message: widget.tooltip,
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Ink( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_borderRadius), borderRadius: BorderRadius.circular(_borderRadius),
color: _isHover ? widget.hoverColor : widget.color, color: _isHover ? widget.hoverColor : widget.color,

View File

@ -23,6 +23,10 @@ import '../../common/shared_state.dart';
import './popup_menu.dart'; import './popup_menu.dart';
import './kb_layout_type_chooser.dart'; import './kb_layout_type_chooser.dart';
const _kKeyLegacyMode = 'legacy';
const _kKeyMapMode = 'map';
const _kKeyTranslateMode = 'translate';
class MenubarState { class MenubarState {
final kStoreKey = 'remoteMenubarState'; final kStoreKey = 'remoteMenubarState';
late RxBool show; late RxBool show;
@ -598,6 +602,7 @@ class _ControlMenu extends StatelessWidget {
hoverColor: _MenubarTheme.hoverBlueColor, hoverColor: _MenubarTheme.hoverBlueColor,
ffi: ffi, ffi: ffi,
menuChildren: [ menuChildren: [
requestElevation(),
osPassword(), osPassword(),
transferFile(context), transferFile(context),
tcpTunneling(context), tcpTunneling(context),
@ -605,12 +610,22 @@ class _ControlMenu extends StatelessWidget {
Divider(), Divider(),
ctrlAltDel(), ctrlAltDel(),
restart(), restart(),
insertLock(),
blockUserInput(), blockUserInput(),
switchSides(), switchSides(),
refresh(), refresh(),
]); ]);
} }
requestElevation() {
final visible = ffi.elevationModel.showRequestMenu;
if (!visible) return Offstage();
return _MenuItemButton(
child: Text(translate('Request Elevation')),
ffi: ffi,
onPressed: () => showRequestElevationDialog(id, ffi.dialogManager));
}
osPassword() { osPassword() {
return _MenuItemButton( return _MenuItemButton(
child: Text(translate('OS Password')), child: Text(translate('OS Password')),
@ -779,6 +794,16 @@ class _ControlMenu extends StatelessWidget {
onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager));
} }
insertLock() {
final perms = ffi.ffiModel.permissions;
final visible = perms['keyboard'] != false;
if (!visible) return Offstage();
return _MenuItemButton(
child: Text(translate('Insert Lock')),
ffi: ffi,
onPressed: () => bind.sessionLockScreen(id: id));
}
blockUserInput() { blockUserInput() {
final perms = ffi.ffiModel.permissions; final perms = ffi.ffiModel.permissions;
final pi = ffi.ffiModel.pi; final pi = ffi.ffiModel.pi;
@ -1092,7 +1117,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
await bind.sessionSetImageQuality(id: widget.id, value: value); await bind.sessionSetImageQuality(id: widget.id, value: value);
} }
return SubmenuButton( return _SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Image Quality')), child: Text(translate('Image Quality')),
menuChildren: [ menuChildren: [
_RadioMenuButton<String>( _RadioMenuButton<String>(
@ -1126,7 +1152,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
}, },
ffi: widget.ffi, ffi: widget.ffi,
), ),
].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), ],
); );
}); });
} }
@ -1301,7 +1327,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
bind.sessionChangePreferCodec(id: widget.id); bind.sessionChangePreferCodec(id: widget.id);
} }
return SubmenuButton( return _SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Codec')), child: Text(translate('Codec')),
menuChildren: [ menuChildren: [
_RadioMenuButton<String>( _RadioMenuButton<String>(
@ -1332,7 +1359,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
onChanged: onChanged, onChanged: onChanged,
ffi: widget.ffi, ffi: widget.ffi,
), ),
].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); ]);
}); });
} }
@ -1364,7 +1391,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
} }
} }
return SubmenuButton( return _SubmenuButton(
ffi: widget.ffi,
menuChildren: resolutions menuChildren: resolutions
.map((e) => _RadioMenuButton( .map((e) => _RadioMenuButton(
value: '${e.width}x${e.height}', value: '${e.width}x${e.height}',
@ -1372,8 +1400,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
onChanged: onChanged, onChanged: onChanged,
ffi: widget.ffi, ffi: widget.ffi,
child: Text('${e.width}x${e.height}'))) child: Text('${e.width}x${e.height}')))
.toList()
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
.toList(), .toList(),
child: Text(translate("Resolution"))); child: Text(translate("Resolution")));
} }
@ -1534,11 +1560,16 @@ class _KeyboardMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var ffiModel = Provider.of<FfiModel>(context); // Do not check permission here?
if (ffiModel.permissions['keyboard'] == false) return Offstage(); // var ffiModel = Provider.of<FfiModel>(context);
// Do not support peer 1.1.9. // if (ffiModel.permissions['keyboard'] == false) return Offstage();
if (stateGlobal.grabKeyboard) { if (stateGlobal.grabKeyboard) {
bind.sessionSetKeyboardMode(id: id, value: 'map'); if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) {
bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode);
} else if (bind.sessionIsKeyboardModeSupported(
id: id, mode: _kKeyLegacyMode)) {
bind.sessionSetKeyboardMode(id: id, value: _kKeyLegacyMode);
}
return Offstage(); return Offstage();
} }
return _IconSubmenuButton( return _IconSubmenuButton(
@ -1551,13 +1582,13 @@ class _KeyboardMenu extends StatelessWidget {
mode() { mode() {
return futureBuilder(future: () async { return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy'; return await bind.sessionGetKeyboardMode(id: id) ?? _kKeyLegacyMode;
}(), hasData: (data) { }(), hasData: (data) {
final groupValue = data as String; final groupValue = data as String;
List<KeyboardModeMenu> modes = [ List<KeyboardModeMenu> modes = [
KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
KeyboardModeMenu(key: 'map', menu: 'Map mode'), KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
]; ];
List<_RadioMenuButton> list = []; List<_RadioMenuButton> list = [];
onChanged(String? value) async { onChanged(String? value) async {
@ -1567,13 +1598,13 @@ class _KeyboardMenu extends StatelessWidget {
for (KeyboardModeMenu mode in modes) { for (KeyboardModeMenu mode in modes) {
if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) {
if (mode.key == 'translate') { if (mode.key == _kKeyTranslateMode) {
if (Platform.isLinux || pi.platform == kPeerPlatformLinux) { if (Platform.isLinux || pi.platform == kPeerPlatformLinux) {
continue; continue;
} }
} }
var text = translate(mode.menu); var text = translate(mode.menu);
if (mode.key == 'translate') { if (mode.key == _kKeyTranslateMode) {
text = '$text beta'; text = '$text beta';
} }
list.add(_RadioMenuButton<String>( list.add(_RadioMenuButton<String>(
@ -1877,6 +1908,28 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
} }
} }
class _SubmenuButton extends StatelessWidget {
final List<Widget> menuChildren;
final Widget? child;
final FFI ffi;
const _SubmenuButton({
Key? key,
required this.menuChildren,
required this.child,
required this.ffi,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SubmenuButton(
key: key,
child: child,
menuChildren:
menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(),
);
}
}
class _MenuItemButton extends StatelessWidget { class _MenuItemButton extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
final Widget? trailingIcon; final Widget? trailingIcon;

View File

@ -523,12 +523,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
super.dispose(); super.dispose();
} }
void _setMaximize(bool maximize) {
stateGlobal.setMaximize(maximize);
setState(() {});
}
@override @override
void onWindowMaximize() { void onWindowMaximize() {
// catch maximize from system // catch maximize from system
if (!widget.isMaximized.value) { if (!widget.isMaximized.value) {
widget.isMaximized.value = true; widget.isMaximized.value = true;
} }
_setMaximize(true);
super.onWindowMaximize(); super.onWindowMaximize();
} }
@ -538,6 +544,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
if (widget.isMaximized.value) { if (widget.isMaximized.value) {
widget.isMaximized.value = false; widget.isMaximized.value = false;
} }
_setMaximize(false);
super.onWindowUnmaximize(); super.onWindowUnmaximize();
} }

View File

@ -291,7 +291,7 @@ void _runApp(
void runInstallPage() async { void runInstallPage() async {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
_runApp('', const InstallPage(), ThemeMode.light); _runApp('', const InstallPage(), MyTheme.currentThemeMode());
windowManager.waitUntilReadyToShow( windowManager.waitUntilReadyToShow(
WindowOptions(size: Size(800, 600), center: true), () async { WindowOptions(size: Size(800, 600), center: true), () async {
windowManager.show(); windowManager.show();

View File

@ -374,8 +374,7 @@ void showWaitUacDialog(
)); ));
} }
void _showRequestElevationDialog( void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) {
String id, OverlayDialogManager dialogManager) {
RxString groupValue = ''.obs; RxString groupValue = ''.obs;
RxString errUser = ''.obs; RxString errUser = ''.obs;
RxString errPwd = ''.obs; RxString errPwd = ''.obs;
@ -531,7 +530,7 @@ void showOnBlockDialog(
dialogManager.show(tag: '$id-$type', (setState, close) { dialogManager.show(tag: '$id-$type', (setState, close) {
void submit() { void submit() {
close(); close();
_showRequestElevationDialog(id, dialogManager); showRequestElevationDialog(id, dialogManager);
} }
return CustomAlertDialog( return CustomAlertDialog(
@ -553,7 +552,7 @@ void showElevationError(String id, String type, String title, String text,
dialogManager.show(tag: '$id-$type', (setState, close) { dialogManager.show(tag: '$id-$type', (setState, close) {
void submit() { void submit() {
close(); close();
_showRequestElevationDialog(id, dialogManager); showRequestElevationDialog(id, dialogManager);
} }
return CustomAlertDialog( return CustomAlertDialog(

View File

@ -459,17 +459,22 @@ class InputModel {
} }
evt['type'] = type; evt['type'] = type;
if (isDesktop) { if (isDesktop) {
y = y - stateGlobal.tabBarHeight; y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value;
x -= stateGlobal.windowBorderWidth.value;
} }
final canvasModel = parent.target!.canvasModel; final canvasModel = parent.target!.canvasModel;
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final ffiModel = parent.target!.ffiModel; final ffiModel = parent.target!.ffiModel;
if (isMove) { if (isMove) {
canvasModel.moveDesktopMouse(x, y); canvasModel.moveDesktopMouse(x, y);
} }
final d = ffiModel.display; final d = ffiModel.display;
final imageWidth = d.width * canvasModel.scale;
final imageHeight = d.height * canvasModel.scale;
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = d.width * canvasModel.scale;
final imageHeight = d.height * canvasModel.scale;
x += imageWidth * canvasModel.scrollX; x += imageWidth * canvasModel.scrollX;
y += imageHeight * canvasModel.scrollY; y += imageHeight * canvasModel.scrollY;
@ -487,10 +492,32 @@ class InputModel {
x /= canvasModel.scale; x /= canvasModel.scale;
y /= canvasModel.scale; y /= canvasModel.scale;
if (canvasModel.scale > 0 && canvasModel.scale < 1) {
final step = 1.0 / canvasModel.scale - 1;
if (nearRight) {
x += step;
}
if (nearBottom) {
y += step;
}
}
x += d.x; x += d.x;
y += d.y; y += d.y;
var evtX = 0;
var evtY = 0;
try {
evtX = x.round();
evtY = y.round();
} catch (e) {
debugPrintStack(
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
return;
}
if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) { if (evtX < d.x ||
evtY < d.y ||
evtX > (d.x + d.width) ||
evtY > (d.y + d.height)) {
// If left mouse up, no early return. // If left mouse up, no early return.
if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
return; return;
@ -498,12 +525,12 @@ class InputModel {
} }
if (type != '') { if (type != '') {
x = 0; evtX = 0;
y = 0; evtY = 0;
} }
evt['x'] = '${x.round()}'; evt['x'] = '$evtX';
evt['y'] = '${y.round()}'; evt['y'] = '$evtY';
var buttons = ''; var buttons = '';
switch (evt['buttons']) { switch (evt['buttons']) {
case kPrimaryMouseButton: case kPrimaryMouseButton:

View File

@ -156,7 +156,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'clipboard') { } else if (name == 'clipboard') {
Clipboard.setData(ClipboardData(text: evt['content'])); Clipboard.setData(ClipboardData(text: evt['content']));
} else if (name == 'permission') { } else if (name == 'permission') {
parent.target?.ffiModel.updatePermission(evt, peerId); updatePermission(evt, peerId);
} else if (name == 'chat_client_mode') { } else if (name == 'chat_client_mode') {
parent.target?.chatModel parent.target?.chatModel
.receive(ChatModel.clientModeID, evt['text'] ?? ''); .receive(ChatModel.clientModeID, evt['text'] ?? '');
@ -203,6 +203,8 @@ class FfiModel with ChangeNotifier {
final peer_id = evt['peer_id'].toString(); final peer_id = evt['peer_id'].toString();
await bind.sessionSwitchSides(id: peer_id); await bind.sessionSwitchSides(id: peer_id);
closeConnection(id: peer_id); closeConnection(id: peer_id);
} else if (name == 'portable_service_running') {
parent.target?.elevationModel.onPortableServiceRunning(evt);
} else if (name == "on_url_scheme_received") { } else if (name == "on_url_scheme_received") {
final url = evt['url'].toString(); final url = evt['url'].toString();
parseRustdeskUri(url); parseRustdeskUri(url);
@ -239,36 +241,33 @@ class FfiModel with ChangeNotifier {
} }
} }
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) { _updateCurDisplay(String peerId, Display newDisplay) {
final oldOrientation = _display.width > _display.height; if (newDisplay != _display) {
var old = _pi.currentDisplay; if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
_pi.currentDisplay = int.parse(evt['display']); parent.target?.cursorModel
_display.x = double.parse(evt['x']); .updateDisplayOrigin(newDisplay.x, newDisplay.y);
_display.y = double.parse(evt['y']); }
_display.width = int.parse(evt['width']); _display = newDisplay;
_display.height = int.parse(evt['height']); _updateSessionWidthHeight(peerId);
_display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
if (old != _pi.currentDisplay) {
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
} }
}
_updateSessionWidthHeight(peerId, display.width, display.height); handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
_pi.currentDisplay = int.parse(evt['display']);
var newDisplay = Display();
newDisplay.x = double.parse(evt['x']);
newDisplay.y = double.parse(evt['y']);
newDisplay.width = int.parse(evt['width']);
newDisplay.height = int.parse(evt['height']);
newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
_updateCurDisplay(peerId, newDisplay);
try { try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay; CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) { } catch (e) {
// //
} }
// remote is mobile, and orientation changed
if ((_display.width > _display.height) != oldOrientation) {
gFFI.canvasModel.updateViewStyle();
}
if (_pi.platform == kPeerPlatformLinux ||
_pi.platform == kPeerPlatformWindows ||
_pi.platform == kPeerPlatformMacOS) {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay(); parent.target?.recordingModel.onSwitchDisplay();
handleResolutions(peerId, evt["resolutions"]); handleResolutions(peerId, evt["resolutions"]);
notifyListeners(); notifyListeners();
@ -370,7 +369,8 @@ class FfiModel with ChangeNotifier {
}); });
} }
_updateSessionWidthHeight(String id, int width, int height) { _updateSessionWidthHeight(String id) {
parent.target?.canvasModel.updateViewStyle();
bind.sessionSetSize(id: id, width: display.width, height: display.height); bind.sessionSetSize(id: id, width: display.width, height: display.height);
} }
@ -427,7 +427,7 @@ class FfiModel with ChangeNotifier {
stateGlobal.displaysCount.value = _pi.displays.length; stateGlobal.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay < _pi.displays.length) { if (_pi.currentDisplay < _pi.displays.length) {
_display = _pi.displays[_pi.currentDisplay]; _display = _pi.displays[_pi.currentDisplay];
_updateSessionWidthHeight(peerId, display.width, display.height); _updateSessionWidthHeight(peerId);
} }
if (displays.isNotEmpty) { if (displays.isNotEmpty) {
parent.target?.dialogManager.showLoading( parent.target?.dialogManager.showLoading(
@ -439,6 +439,7 @@ class FfiModel with ChangeNotifier {
Map<String, dynamic> features = json.decode(evt['features']); Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1; _pi.features.privacyMode = features['privacy_mode'] == 1;
handleResolutions(peerId, evt["resolutions"]); handleResolutions(peerId, evt["resolutions"]);
parent.target?.elevationModel.onPeerInfo(_pi);
} }
notifyListeners(); notifyListeners();
} }
@ -485,7 +486,7 @@ class FfiModel with ChangeNotifier {
_pi.displays = newDisplays; _pi.displays = newDisplays;
stateGlobal.displaysCount.value = _pi.displays.length; stateGlobal.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
_display = _pi.displays[_pi.currentDisplay]; _updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]);
} }
} }
notifyListeners(); notifyListeners();
@ -616,13 +617,28 @@ class ViewStyle {
final int displayWidth; final int displayWidth;
final int displayHeight; final int displayHeight;
ViewStyle({ ViewStyle({
this.style = '', required this.style,
this.width = 0.0, required this.width,
this.height = 0.0, required this.height,
this.displayWidth = 0, required this.displayWidth,
this.displayHeight = 0, required this.displayHeight,
}); });
static defaultViewStyle() {
final desktop = (isDesktop || isWebDesktop);
final w =
desktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth;
final h =
desktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight;
return ViewStyle(
style: '',
width: w.toDouble(),
height: h.toDouble(),
displayWidth: w,
displayHeight: h,
);
}
static int _double2Int(double v) => (v * 100).round().toInt(); static int _double2Int(double v) => (v * 100).round().toInt();
@override @override
@ -651,9 +667,14 @@ class ViewStyle {
double get scale { double get scale {
double s = 1.0; double s = 1.0;
if (style == kRemoteViewStyleAdaptive) { if (style == kRemoteViewStyleAdaptive) {
final s1 = width / displayWidth; if (width != 0 &&
final s2 = height / displayHeight; height != 0 &&
s = s1 < s2 ? s1 : s2; displayWidth != 0 &&
displayHeight != 0) {
final s1 = width / displayWidth;
final s2 = height / displayHeight;
s = s1 < s2 ? s1 : s2;
}
} }
return s; return s;
} }
@ -679,7 +700,7 @@ class CanvasModel with ChangeNotifier {
// scroll offset y percent // scroll offset y percent
double _scrollY = 0.0; double _scrollY = 0.0;
ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
ViewStyle _lastViewStyle = ViewStyle(); ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
final _imageOverflow = false.obs; final _imageOverflow = false.obs;
@ -710,8 +731,15 @@ class CanvasModel with ChangeNotifier {
Size getSize() { Size getSize() {
final size = MediaQueryData.fromWindow(ui.window).size; final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here. // If minimized, w or h may be negative here.
double w = size.width - windowBorderWidth * 2; double w = size.width -
double h = size.height - tabBarHeight - windowBorderWidth * 2; windowBorderWidth * 2 -
kDragToResizeAreaPadding.left -
kDragToResizeAreaPadding.right;
double h = size.height -
tabBarHeight -
windowBorderWidth * 2 -
kDragToResizeAreaPadding.top -
kDragToResizeAreaPadding.bottom;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
} }
@ -789,17 +817,29 @@ class CanvasModel with ChangeNotifier {
double get tabBarHeight => stateGlobal.tabBarHeight; double get tabBarHeight => stateGlobal.tabBarHeight;
moveDesktopMouse(double x, double y) { moveDesktopMouse(double x, double y) {
if (size.width == 0 || size.height == 0) {
return;
}
// On mobile platforms, move the canvas with the cursor. // On mobile platforms, move the canvas with the cursor.
final dw = getDisplayWidth() * _scale; final dw = getDisplayWidth() * _scale;
final dh = getDisplayHeight() * _scale; final dh = getDisplayHeight() * _scale;
var dxOffset = 0; var dxOffset = 0;
var dyOffset = 0; var dyOffset = 0;
if (dw > size.width) { try {
dxOffset = (x - dw * (x / size.width) - _x).toInt(); if (dw > size.width) {
} dxOffset = (x - dw * (x / size.width) - _x).toInt();
if (dh > size.height) { }
dyOffset = (y - dh * (y / size.height) - _y).toInt(); if (dh > size.height) {
dyOffset = (y - dh * (y / size.height) - _y).toInt();
}
} catch (e) {
debugPrintStack(
label:
'(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e');
return;
} }
_x += dxOffset; _x += dxOffset;
_y += dyOffset; _y += dyOffset;
if (dxOffset != 0 || dyOffset != 0) { if (dxOffset != 0 || dyOffset != 0) {
@ -1395,6 +1435,21 @@ class RecordingModel with ChangeNotifier {
} }
} }
class ElevationModel with ChangeNotifier {
WeakReference<FFI> parent;
ElevationModel(this.parent);
bool _running = false;
bool _canElevate = false;
bool get showRequestMenu => _canElevate && !_running;
onPeerInfo(PeerInfo pi) {
_canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false;
}
onPortableServiceRunning(Map<String, dynamic> evt) {
_running = evt['running'] == 'true';
}
}
enum ConnType { defaultConn, fileTransfer, portForward, rdp } enum ConnType { defaultConn, fileTransfer, portForward, rdp }
/// Flutter state manager and data communication with the Rust core. /// Flutter state manager and data communication with the Rust core.
@ -1420,6 +1475,7 @@ class FFI {
late final QualityMonitorModel qualityMonitorModel; // session late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // session late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session late final InputModel inputModel; // session
late final ElevationModel elevationModel; // session
FFI() { FFI() {
imageModel = ImageModel(WeakReference(this)); imageModel = ImageModel(WeakReference(this));
@ -1436,6 +1492,7 @@ class FFI {
qualityMonitorModel = QualityMonitorModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this));
recordingModel = RecordingModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this)); inputModel = InputModel(WeakReference(this));
elevationModel = ElevationModel(WeakReference(this));
} }
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
@ -1559,6 +1616,19 @@ class Display {
? kDesktopDefaultDisplayHeight ? kDesktopDefaultDisplayHeight
: kMobileDefaultDisplayHeight; : kMobileDefaultDisplayHeight;
} }
@override
bool operator ==(Object other) =>
other is Display &&
other.runtimeType == runtimeType &&
_innerEqual(other);
bool _innerEqual(Display other) =>
other.x == x &&
other.y == y &&
other.width == width &&
other.height == height &&
other.cursorEmbedded == cursorEmbedded;
} }
class Resolution { class Resolution {

View File

@ -9,8 +9,10 @@ import '../consts.dart';
class StateGlobal { class StateGlobal {
int _windowId = -1; int _windowId = -1;
bool _fullscreen = false; bool _fullscreen = false;
bool _maximize = false;
bool grabKeyboard = false; bool grabKeyboard = false;
final RxBool _showTabBar = true.obs; final RxBool _showTabBar = true.obs;
final RxBool _showResizeEdge = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteMenuBar = false.obs; final RxBool showRemoteMenuBar = false.obs;
@ -18,12 +20,20 @@ class StateGlobal {
int get windowId => _windowId; int get windowId => _windowId;
bool get fullscreen => _fullscreen; bool get fullscreen => _fullscreen;
bool get maximize => _maximize;
double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight; double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight;
RxBool get showTabBar => _showTabBar; RxBool get showTabBar => _showTabBar;
RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get resizeEdgeSize => _resizeEdgeSize;
RxDouble get windowBorderWidth => _windowBorderWidth; RxDouble get windowBorderWidth => _windowBorderWidth;
setWindowId(int id) => _windowId = id; setWindowId(int id) => _windowId = id;
setMaximize(bool v) {
if (_maximize != v) {
_maximize = v;
_resizeEdgeSize.value =
_maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
}
}
setFullscreen(bool v) { setFullscreen(bool v) {
if (_fullscreen != v) { if (_fullscreen != v) {
_fullscreen = v; _fullscreen = v;

View File

@ -59,7 +59,7 @@ dependencies:
desktop_multi_window: desktop_multi_window:
git: git:
url: https://github.com/Kingtous/rustdesk_desktop_multi_window url: https://github.com/Kingtous/rustdesk_desktop_multi_window
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a ref: e383fffb5c4529c9e0a710f1025a0c590b99ee08
freezed_annotation: ^2.0.3 freezed_annotation: ^2.0.3
flutter_custom_cursor: ^0.0.4 flutter_custom_cursor: ^0.0.4
window_size: window_size:
@ -76,7 +76,7 @@ dependencies:
file_picker: ^5.1.0 file_picker: ^5.1.0
flutter_svg: ^1.1.5 flutter_svg: ^1.1.5
flutter_improved_scrolling: flutter_improved_scrolling:
# currently, we use flutter 3.0.5 for windows build, latest for other builds. # currently, we use flutter 3.7.0+.
# #
# for flutter 3.0.5, please use official version(just comment code below). # for flutter 3.0.5, please use official version(just comment code below).
# if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below).

View File

@ -56,6 +56,7 @@ pub struct Remote<T: InvokeUiSession> {
data_count: Arc<AtomicUsize>, data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>, frame_count: Arc<AtomicUsize>,
video_format: CodecFormat, video_format: CodecFormat,
elevation_requested: bool,
} }
impl<T: InvokeUiSession> Remote<T> { impl<T: InvokeUiSession> Remote<T> {
@ -87,6 +88,7 @@ impl<T: InvokeUiSession> Remote<T> {
video_format: CodecFormat::Unknown, video_format: CodecFormat::Unknown,
stop_voice_call_sender: None, stop_voice_call_sender: None,
voice_call_request_timestamp: None, voice_call_request_timestamp: None,
elevation_requested: false,
} }
} }
@ -686,6 +688,7 @@ impl<T: InvokeUiSession> Remote<T> {
let mut msg = Message::new(); let mut msg = Message::new();
msg.set_misc(misc); msg.set_misc(misc);
allow_err!(peer.send(&msg).await); allow_err!(peer.send(&msg).await);
self.elevation_requested = true;
} }
Data::ElevateWithLogon(username, password) => { Data::ElevateWithLogon(username, password) => {
let mut request = ElevationRequest::new(); let mut request = ElevationRequest::new();
@ -699,6 +702,7 @@ impl<T: InvokeUiSession> Remote<T> {
let mut msg = Message::new(); let mut msg = Message::new();
msg.set_misc(misc); msg.set_misc(misc);
allow_err!(peer.send(&msg).await); allow_err!(peer.send(&msg).await);
self.elevation_requested = true;
} }
Data::NewVoiceCall => { Data::NewVoiceCall => {
let msg = new_voice_call_request(true); let msg = new_voice_call_request(true);
@ -1181,7 +1185,8 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
Some(misc::Union::PortableServiceRunning(b)) => { Some(misc::Union::PortableServiceRunning(b)) => {
if b { self.handler.portable_service_running(b);
if self.elevation_requested && b {
self.handler.msgbox( self.handler.msgbox(
"custom-nocancel-success", "custom-nocancel-success",
"Successful", "Successful",
@ -1253,14 +1258,12 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
} }
Some(message::Union::PeerInfo(pi)) => { Some(message::Union::PeerInfo(pi)) => match pi.conn_id {
match pi.conn_id { crate::SYNC_PEER_INFO_DISPLAYS => {
crate::SYNC_PEER_INFO_DISPLAYS => { self.handler.set_displays(&pi.displays);
self.handler.set_displays(&pi.displays);
}
_ => {}
} }
} _ => {}
},
_ => {} _ => {}
} }
} }

View File

@ -572,6 +572,13 @@ impl InvokeUiSession for FlutterHandler {
self.push_event("switch_back", [("peer_id", peer_id)].into()); self.push_event("switch_back", [("peer_id", peer_id)].into());
} }
fn portable_service_running(&self, running: bool) {
self.push_event(
"portable_service_running",
[("running", running.to_string().as_str())].into(),
);
}
fn on_voice_call_started(&self) { fn on_voice_call_started(&self) {
self.push_event("on_voice_call_started", [].into()); self.push_event("on_voice_call_started", [].into());
} }

View File

@ -726,6 +726,10 @@ pub fn main_peer_has_password(id: String) -> bool {
peer_has_password(id) peer_has_password(id)
} }
pub fn main_is_in_recent_peers(id: String) -> bool {
PeerConfig::peers().iter().any(|e| e.0 == id)
}
pub fn main_load_recent_peers() { pub fn main_load_recent_peers() {
if !config::APP_DIR.read().unwrap().is_empty() { if !config::APP_DIR.read().unwrap().is_empty() {
let peers: Vec<HashMap<&str, String>> = PeerConfig::peers() let peers: Vec<HashMap<&str, String>> = PeerConfig::peers()
@ -796,6 +800,10 @@ pub fn main_load_lan_peers() {
}; };
} }
pub fn main_remove_discovered(id: String) {
remove_discovered(id);
}
fn main_broadcast_message(data: &HashMap<&str, &str>) { fn main_broadcast_message(data: &HashMap<&str, &str>) {
let apps = vec![ let apps = vec![
flutter::APP_TYPE_DESKTOP_REMOTE, flutter::APP_TYPE_DESKTOP_REMOTE,
@ -832,6 +840,10 @@ pub fn main_get_user_default_option(key: String) -> SyncReturn<String> {
SyncReturn(get_user_default_option(key)) SyncReturn(get_user_default_option(key))
} }
pub fn main_handle_relay_id(id: String) -> String {
handle_relay_id(id)
}
pub fn session_add_port_forward( pub fn session_add_port_forward(
id: String, id: String,
local_port: i32, local_port: i32,

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -38,7 +38,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop service", "停止服务"), ("Stop service", "停止服务"),
("Change ID", "更改 ID"), ("Change ID", "更改 ID"),
("Your new ID", "你的新 ID"), ("Your new ID", "你的新 ID"),
("length %min% to %max%", "长度在 %min 与 %max 之间"), ("length %min% to %max%", "长度在 %min% 与 %max% 之间"),
("starts with a letter", "以字母开头"), ("starts with a letter", "以字母开头"),
("allowed characters", "使用允许的字符"), ("allowed characters", "使用允许的字符"),
("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"),
@ -137,7 +137,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Failed to connect to rendezvous server", "连接注册服务器失败"),
("Please try later", "请稍后再试"), ("Please try later", "请稍后再试"),
("Remote desktop is offline", "远程电脑处于离线状态"), ("Remote desktop is offline", "远程电脑处于离线状态"),
("Key mismatch", "密钥不匹配"), ("Key mismatch", "Key 不匹配"),
("Timeout", "连接超时"), ("Timeout", "连接超时"),
("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect to relay server", "无法连接到中继服务器"),
("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"),
@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", "重连"), ("Reconnect", "重连"),
("Codec", "编解码"), ("Codec", "编解码"),
("Resolution", "分辨率"), ("Resolution", "分辨率"),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,6 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""), ("Stop voice call", ""),
("relay_hint_tip", ""), ("relay_hint_tip", ""),
("Reconnect", ""), ("Reconnect", ""),
("No transfers in progress", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
].iter().cloned().collect(); ].iter().cloned().collect();

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,7 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Sprachanruf beenden"), ("Stop voice call", "Sprachanruf beenden"),
("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."),
("Reconnect", "Erneut verbinden"), ("Reconnect", "Erneut verbinden"),
("Codec", ""), ("Codec", "Codec"),
("Resolution", ""), ("Resolution", "Auflösung"),
].iter().cloned().collect(); ("No transfers in progress", "Keine Übertragungen im Gange"),
].iter().cloned().collect();
} }

View File

@ -39,10 +39,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."),
("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."),
("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."),
("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("request_elevation_tip", "You can also request elevation if there is someone on the remote side."),
("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), ("wait_accept_uac_tip", "Please wait for the remote user to accept the UAC dialog."),
("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."),
("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."), ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."),
("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."), ("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,7 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Detener llamada de voz"), ("Stop voice call", "Detener llamada de voz"),
("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."),
("Reconnect", "Reconectar"), ("Reconnect", "Reconectar"),
("Codec", ""), ("Codec", "Códec"),
("Resolution", ""), ("Resolution", "Resolución"),
("No transfers in progress", "No hay transferencias en curso"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,6 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "توقف تماس صوتی"), ("Stop voice call", "توقف تماس صوتی"),
("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"),
("Reconnect", "اتصال مجدد"), ("Reconnect", "اتصال مجدد"),
("No transfers in progress", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
].iter().cloned().collect(); ].iter().cloned().collect();

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,7 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Interrompi la chiamata vocale"), ("Stop voice call", "Interrompi la chiamata vocale"),
("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."),
("Reconnect", "Riconnetti"), ("Reconnect", "Riconnetti"),
("Codec", ""), ("No transfers in progress", "Nessun trasferimento in corso"),
("Resolution", ""), ("Codec", "Codec"),
("Resolution", "Risoluzione"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -444,7 +444,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Default View Style", "Standaard Weergave Stijl"), ("Default View Style", "Standaard Weergave Stijl"),
("Default Scroll Style", "Standaard Scroll Stijl"), ("Default Scroll Style", "Standaard Scroll Stijl"),
("Default Image Quality", "Standaard Beeldkwaliteit"), ("Default Image Quality", "Standaard Beeldkwaliteit"),
("Default Codec", "tandaard Codec"), ("Default Codec", "Standaard Codec"),
("Bitrate", "Bitrate"), ("Bitrate", "Bitrate"),
("FPS", "FPS"), ("FPS", "FPS"),
("Auto", "Auto"), ("Auto", "Auto"),
@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -284,13 +284,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Akceptujesz?"), ("Do you accept?", "Akceptujesz?"),
("Open System Setting", "Otwórz ustawienia systemowe"), ("Open System Setting", "Otwórz ustawienia systemowe"),
("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"),
("android_input_permission_tip1", "android_input_permission_tip1"), ("android_input_permission_tip1", "Aby można było sterować Twoim urządzeniem za pomocą myszy lub dotyku, musisz zezwolić RustDesk na korzystanie z usługi \"Ułatwienia dostępu\"."),
("android_input_permission_tip2", "android_input_permission_tip2"), ("android_input_permission_tip2", "Przejdź do następnej strony ustawień systemowych, znajdź i wejdź w [Zainstalowane usługi], włącz usługę [RustDesk Input]."),
("android_new_connection_tip", "android_new_connection_tip"), ("android_new_connection_tip", "Otrzymano nowe żądanie zdalnego dostępu, które chce przejąć kontrolę nad Twoim urządzeniem."),
("android_service_will_start_tip", "android_service_will_start_tip"), ("android_service_will_start_tip", "Włączenie opcji „Przechwytywanie ekranu” spowoduje automatyczne uruchomienie usługi, umożliwiając innym urządzeniom żądanie połączenia z Twoim urządzeniem."),
("android_stop_service_tip", "android_stop_service_tip"), ("android_stop_service_tip", "Zamknięcie usługi spowoduje automatyczne zamknięcie wszystkich nawiązanych połączeń."),
("android_version_audio_tip", "android_version_audio_tip"), ("android_version_audio_tip", "Bieżąca wersja systemu Android nie obsługuje przechwytywania dźwięku, zaktualizuj system do wersji Android 10 lub nowszej."),
("android_start_service_tip", "android_start_service_tip"), ("android_start_service_tip", "Kliknij [Uruchom usługę] lub Otwórz [Przechwytywanie ekranu], aby uruchomić usługę udostępniania ekranu."),
("Account", "Konto"), ("Account", "Konto"),
("Overwrite", "Nadpisz"), ("Overwrite", "Nadpisz"),
("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"), ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"),
@ -311,7 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Language", "Język"), ("Language", "Język"),
("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"),
("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"),
("android_open_battery_optimizations_tip", "android_open_battery_optimizations_tip"), ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"),
("Connection not allowed", "Połączenie niedozwolone"), ("Connection not allowed", "Połączenie niedozwolone"),
("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"),
("Map mode", "Tryb mapowania"), ("Map mode", "Tryb mapowania"),
@ -449,12 +449,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("FPS", "FPS"), ("FPS", "FPS"),
("Auto", "Auto"), ("Auto", "Auto"),
("Other Default Options", "Inne opcje domyślne"), ("Other Default Options", "Inne opcje domyślne"),
("Voice call", ""), ("Voice call", "Rozmowa głosowa"),
("Text chat", ""), ("Text chat", "Chat tekstowy"),
("Stop voice call", ""), ("Stop voice call", "Rozłącz"),
("relay_hint_tip", ""), ("relay_hint_tip", "Bezpośrednie połączenie może nie być możliwe, możesz spróbować połączyć się przez serwer przekazujący. \nDodatkowo, jeśli chcesz użyć serwera przekazującego przy pierwszej próbie, możesz dodać sufiks \"/r\" do identyfikatora lub wybrać opcję \"Zawsze łącz przez serwer przekazujący\" na karcie peer-ów."),
("Reconnect", ""), ("Reconnect", "Połącz ponownie"),
("Codec", ""), ("Codec", "Kodek"),
("Resolution", ""), ("Resolution", "Rozdzielczość"),
("Use temporary password", "Użyj hasła tymczasowego"),
("Set temporary password length", "Ustaw długość hasła tymczasowego"),
("Key", "Klucz"),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,6 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""), ("Stop voice call", ""),
("relay_hint_tip", ""), ("relay_hint_tip", ""),
("Reconnect", ""), ("Reconnect", ""),
("No transfers in progress", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
].iter().cloned().collect(); ].iter().cloned().collect();

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -454,7 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Завершить голосовой вызов"), ("Stop voice call", "Завершить голосовой вызов"),
("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."),
("Reconnect", "Переподключить"), ("Reconnect", "Переподключить"),
("Codec", ""), ("Codec", "Кодек"),
("Resolution", ""), ("Resolution", "Разрешение"),
("No transfers in progress", "Передача не осуществляется"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -37,19 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Clipboard is empty", "剪貼簿是空的"), ("Clipboard is empty", "剪貼簿是空的"),
("Stop service", "停止服務"), ("Stop service", "停止服務"),
("Change ID", "更改 ID"), ("Change ID", "更改 ID"),
("Your new ID", ""), ("Your new ID", "你的新 ID"),
("length %min% to %max%", ""), ("length %min% to %max%", "長度在 %min% 與 %max% 之間"),
("starts with a letter", ""), ("starts with a letter", "以字母開頭"),
("allowed characters", ""), ("allowed characters", "使用允許的字元"),
("id_change_tip", "僅能使用以下字元a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("id_change_tip", "僅能使用以下字元a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"),
("Website", "網站"), ("Website", "網站"),
("About", "關於"), ("About", "關於"),
("Slogan_tip", ""), ("Slogan_tip", ""),
("Privacy Statement", ""), ("Privacy Statement", "隱私聲明"),
("Mute", "靜音"), ("Mute", "靜音"),
("Build Date", ""), ("Build Date", "建構日期"),
("Version", ""), ("Version", "版本"),
("Home", ""), ("Home", "主頁"),
("Audio Input", "音訊輸入"), ("Audio Input", "音訊輸入"),
("Enhancements", "增強功能"), ("Enhancements", "增強功能"),
("Hardware Codec", "硬件編解碼"), ("Hardware Codec", "硬件編解碼"),
@ -213,15 +213,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by the peer", "由對方手動關閉"), ("Closed manually by the peer", "由對方手動關閉"),
("Enable remote configuration modification", "啟用遠端更改設定"), ("Enable remote configuration modification", "啟用遠端更改設定"),
("Run without install", "跳過安裝直接執行"), ("Run without install", "跳過安裝直接執行"),
("Connect via relay", ""), ("Connect via relay", "中繼連線"),
("Always connect via relay", "一律透過轉送連線"), ("Always connect via relay", "一律透過轉送連線"),
("whitelist_tip", "只有白名單中的 IP 可以存取"), ("whitelist_tip", "只有白名單中的 IP 可以存取"),
("Login", "登入"), ("Login", "登入"),
("Verify", ""), ("Verify", "驗證"),
("Remember me", ""), ("Remember me", "記住我"),
("Trust this device", ""), ("Trust this device", "信任此設備"),
("Verification code", ""), ("Verification code", "驗證碼"),
("verification_tip", ""), ("verification_tip", "檢測到新設備登錄,已向註冊郵箱發送了登入驗證碼,請輸入驗證碼繼續登錄"),
("Logout", "登出"), ("Logout", "登出"),
("Tags", "標籤"), ("Tags", "標籤"),
("Search ID", "搜尋 ID"), ("Search ID", "搜尋 ID"),
@ -391,12 +391,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"),
("JumpLink", "查看"), ("JumpLink", "查看"),
("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"),
("Show RustDesk", ""), ("Show RustDesk", "顯示 RustDesk"),
("This PC", ""), ("This PC", "此電腦"),
("or", ""), ("or", ""),
("Continue with", ""), ("Continue with", "使用"),
("Elevate", "提權"), ("Elevate", "提權"),
("Zoom cursor", ""), ("Zoom cursor", "縮放游標"),
("Accept sessions via password", "只允許密碼訪問"), ("Accept sessions via password", "只允許密碼訪問"),
("Accept sessions via click", "只允許點擊訪問"), ("Accept sessions via click", "只允許點擊訪問"),
("Accept sessions via both", "允許密碼或點擊訪問"), ("Accept sessions via both", "允許密碼或點擊訪問"),
@ -407,9 +407,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Request access to your device", "請求訪問你的設備"), ("Request access to your device", "請求訪問你的設備"),
("Hide connection management window", "隱藏連接管理窗口"), ("Hide connection management window", "隱藏連接管理窗口"),
("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"),
("wayland_experiment_tip", ""), ("wayland_experiment_tip", "Wayland 支持處於實驗階段,如果你需要使用無人值守訪問,請使用 X11。"),
("Right click to select tabs", "右鍵選擇選項卡"), ("Right click to select tabs", "右鍵選擇選項卡"),
("Skipped", ""), ("Skipped", "已略過"),
("Add to Address Book", "添加到地址簿"), ("Add to Address Book", "添加到地址簿"),
("Group", "小組"), ("Group", "小組"),
("Search", "搜索"), ("Search", "搜索"),
@ -418,8 +418,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Select local keyboard type", "請選擇本地鍵盤類型"), ("Select local keyboard type", "請選擇本地鍵盤類型"),
("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"),
("Always use software rendering", "使用軟件渲染"), ("Always use software rendering", "使用軟件渲染"),
("config_input", ""), ("config_input", "為了能夠通過鍵盤控制遠程桌面, 請給予 RustDesk \"輸入監控\" 權限。"),
("config_microphone", ""), ("config_microphone", "為了支持通過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"),
("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"),
("Wait", "等待"), ("Wait", "等待"),
("Elevation Error", "提權失敗"), ("Elevation Error", "提權失敗"),
@ -438,8 +438,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Weak", ""), ("Weak", ""),
("Medium", ""), ("Medium", ""),
("Strong", ""), ("Strong", ""),
("Switch Sides", ""), ("Switch Sides", "反轉訪問方向"),
("Please confirm if you want to share your desktop?", ""), ("Please confirm if you want to share your desktop?", "請確認是否要讓對方訪問你的桌面?"),
("Display", "顯示"), ("Display", "顯示"),
("Default View Style", "默認顯示方式"), ("Default View Style", "默認顯示方式"),
("Default Scroll Style", "默認滾動方式"), ("Default Scroll Style", "默認滾動方式"),
@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", "重連"), ("Reconnect", "重連"),
("Codec", "編解碼"), ("Codec", "編解碼"),
("Resolution", "分辨率"), ("Resolution", "分辨率"),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -456,5 +456,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", ""), ("Reconnect", ""),
("Codec", ""), ("Codec", ""),
("Resolution", ""), ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -1,6 +1,7 @@
// Specify the Windows subsystem to eliminate console window. #![cfg_attr(
// Requires Rust 1.18. all(not(debug_assertions), target_os = "windows"),
//#![windows_subsystem = "windows"] windows_subsystem = "windows"
)]
use librustdesk::*; use librustdesk::*;

View File

@ -1,14 +1,22 @@
use super::{CursorData, ResultType}; use super::{CursorData, ResultType};
use hbb_common::libc::{c_char, c_int, c_long, c_void};
pub use hbb_common::platform::linux::*; pub use hbb_common::platform::linux::*;
use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use hbb_common::{
allow_err,
anyhow::anyhow,
bail,
libc::{c_char, c_int, c_long, c_void},
log,
message_proto::Resolution,
};
use std::{ use std::{
cell::RefCell, cell::RefCell,
path::PathBuf, path::{Path, PathBuf},
process::{Child, Command},
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Arc,
}, },
time::{Duration, Instant},
}; };
use xrandr_parser::Parser; use xrandr_parser::Parser;
@ -162,10 +170,29 @@ fn start_uinput_service() {
}); });
} }
fn stop_server(server: &mut Option<std::process::Child>) { #[inline]
fn try_start_server_(user: Option<(String, String)>) -> ResultType<Option<Child>> {
if user.is_some() {
run_as_user(vec!["--server"], user)
} else {
Ok(Some(crate::run_me(vec!["--server"])?))
}
}
#[inline]
fn start_server(user: Option<(String, String)>, server: &mut Option<Child>) {
match try_start_server_(user) {
Ok(ps) => *server = ps,
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
}
fn stop_server(server: &mut Option<Child>) {
if let Some(mut ps) = server.take() { if let Some(mut ps) = server.take() {
allow_err!(ps.kill()); allow_err!(ps.kill());
std::thread::sleep(std::time::Duration::from_millis(30)); std::thread::sleep(Duration::from_millis(30));
match ps.try_wait() { match ps.try_wait() {
Ok(Some(_status)) => {} Ok(Some(_status)) => {}
Ok(None) => { Ok(None) => {
@ -182,7 +209,7 @@ fn set_x11_env(uid: &str) {
let mut auth = get_env_tries("XAUTHORITY", uid, 10); let mut auth = get_env_tries("XAUTHORITY", uid, 10);
// auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468
if auth.is_empty() || uid == "0" { if auth.is_empty() || uid == "0" {
auth = if std::path::Path::new(&gdm).exists() { auth = if Path::new(&gdm).exists() {
gdm gdm
} else { } else {
let username = get_active_username(); let username = get_active_username();
@ -190,7 +217,7 @@ fn set_x11_env(uid: &str) {
format!("/{}/.Xauthority", username) format!("/{}/.Xauthority", username)
} else { } else {
let tmp = format!("/home/{}/.Xauthority", username); let tmp = format!("/home/{}/.Xauthority", username);
if std::path::Path::new(&tmp).exists() { if Path::new(&tmp).exists() {
tmp tmp
} else { } else {
format!("/var/lib/{}/.Xauthority", username) format!("/var/lib/{}/.Xauthority", username)
@ -223,8 +250,8 @@ fn should_start_server(
uid: &mut String, uid: &mut String,
cur_uid: String, cur_uid: String,
cm0: &mut bool, cm0: &mut bool,
last_restart: &mut std::time::Instant, last_restart: &mut Instant,
server: &mut Option<std::process::Child>, server: &mut Option<Child>,
) -> bool { ) -> bool {
let cm = get_cm(); let cm = get_cm();
let mut start_new = false; let mut start_new = false;
@ -235,8 +262,8 @@ fn should_start_server(
} }
if let Some(ps) = server.as_mut() { if let Some(ps) = server.as_mut() {
allow_err!(ps.kill()); allow_err!(ps.kill());
std::thread::sleep(std::time::Duration::from_millis(30)); std::thread::sleep(Duration::from_millis(30));
*last_restart = std::time::Instant::now(); *last_restart = Instant::now();
} }
} else if !cm } else if !cm
&& ((*cm0 && last_restart.elapsed().as_secs() > 60) && ((*cm0 && last_restart.elapsed().as_secs() > 60)
@ -247,8 +274,8 @@ fn should_start_server(
// and x server get displays failure issue // and x server get displays failure issue
if let Some(ps) = server.as_mut() { if let Some(ps) = server.as_mut() {
allow_err!(ps.kill()); allow_err!(ps.kill());
std::thread::sleep(std::time::Duration::from_millis(30)); std::thread::sleep(Duration::from_millis(30));
*last_restart = std::time::Instant::now(); *last_restart = Instant::now();
log::info!("restart server"); log::info!("restart server");
} }
} }
@ -267,6 +294,13 @@ fn should_start_server(
start_new start_new
} }
// to-do: stop_server(&mut user_server); may not stop child correctly
// stop_rustdesk_servers() is just a temp solution here.
fn force_stop_server() {
stop_rustdesk_servers();
std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL));
}
pub fn start_os_service() { pub fn start_os_service() {
stop_rustdesk_servers(); stop_rustdesk_servers();
start_uinput_service(); start_uinput_service();
@ -274,8 +308,8 @@ pub fn start_os_service() {
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let r = running.clone(); let r = running.clone();
let mut uid = "".to_owned(); let mut uid = "".to_owned();
let mut server: Option<std::process::Child> = None; let mut server: Option<Child> = None;
let mut user_server: Option<std::process::Child> = None; let mut user_server: Option<Child> = None;
if let Err(err) = ctrlc::set_handler(move || { if let Err(err) = ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst); r.store(false, Ordering::SeqCst);
}) { }) {
@ -283,12 +317,13 @@ pub fn start_os_service() {
} }
let mut cm0 = false; let mut cm0 = false;
let mut last_restart = std::time::Instant::now(); let mut last_restart = Instant::now();
while running.load(Ordering::SeqCst) { while running.load(Ordering::SeqCst) {
let (cur_uid, cur_user) = get_active_user_id_name(); let (cur_uid, cur_user) = get_active_user_id_name();
let is_wayland = current_is_wayland(); let is_wayland = current_is_wayland();
if cur_user == "root" || !is_wayland { if cur_user == "root" || !is_wayland {
// try kill subprocess "--server"
stop_server(&mut user_server); stop_server(&mut user_server);
// try start subprocess "--server" // try start subprocess "--server"
if should_start_server( if should_start_server(
@ -299,16 +334,8 @@ pub fn start_os_service() {
&mut last_restart, &mut last_restart,
&mut server, &mut server,
) { ) {
// to-do: stop_server(&mut user_server); may not stop child correctly force_stop_server();
// stop_rustdesk_servers() is just a temp solution here. start_server(None, &mut server);
stop_rustdesk_servers();
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
match crate::run_me(vec!["--server"]) {
Ok(ps) => server = Some(ps),
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
} }
} else if cur_user != "" { } else if cur_user != "" {
if cur_user != "gdm" { if cur_user != "gdm" {
@ -324,23 +351,16 @@ pub fn start_os_service() {
&mut last_restart, &mut last_restart,
&mut user_server, &mut user_server,
) { ) {
stop_rustdesk_servers(); force_stop_server();
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); start_server(Some((cur_uid, cur_user)), &mut user_server);
match run_as_user(vec!["--server"], Some((cur_uid, cur_user))) {
Ok(ps) => user_server = ps,
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
} }
} }
} else { } else {
stop_rustdesk_servers(); force_stop_server();
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
stop_server(&mut user_server); stop_server(&mut user_server);
stop_server(&mut server); stop_server(&mut server);
} }
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL));
} }
if let Some(ps) = user_server.take().as_mut() { if let Some(ps) = user_server.take().as_mut() {
@ -362,7 +382,7 @@ pub fn get_active_userid() -> String {
} }
fn get_cm() -> bool { fn get_cm() -> bool {
if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() { if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() { for line in String::from_utf8_lossy(&output.stdout).lines() {
if line.contains(&format!( if line.contains(&format!(
"{} --cm", "{} --cm",
@ -380,7 +400,7 @@ fn get_cm() -> bool {
fn get_display() -> String { fn get_display() -> String {
let user = get_active_username(); let user = get_active_username();
log::debug!("w {}", &user); log::debug!("w {}", &user);
if let Ok(output) = std::process::Command::new("w").arg(&user).output() { if let Ok(output) = Command::new("w").arg(&user).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() { for line in String::from_utf8_lossy(&output.stdout).lines() {
log::debug!(" {}", line); log::debug!(" {}", line);
let mut iter = line.split_whitespace(); let mut iter = line.split_whitespace();
@ -395,7 +415,7 @@ fn get_display() -> String {
// above not work for gdm user // above not work for gdm user
log::debug!("ls -l /tmp/.X11-unix/"); log::debug!("ls -l /tmp/.X11-unix/");
let mut last = "".to_owned(); let mut last = "".to_owned();
if let Ok(output) = std::process::Command::new("ls") if let Ok(output) = Command::new("ls")
.args(vec!["-l", "/tmp/.X11-unix/"]) .args(vec!["-l", "/tmp/.X11-unix/"])
.output() .output()
{ {
@ -474,10 +494,7 @@ fn is_opensuse() -> bool {
false false
} }
pub fn run_as_user( pub fn run_as_user(arg: Vec<&str>, user: Option<(String, String)>) -> ResultType<Option<Child>> {
arg: Vec<&str>,
user: Option<(String, String)>,
) -> ResultType<Option<std::process::Child>> {
let (uid, username) = match user { let (uid, username) = match user {
Some(id_name) => id_name, Some(id_name) => id_name,
None => get_active_user_id_name(), None => get_active_user_id_name(),
@ -491,7 +508,7 @@ pub fn run_as_user(
args.insert(0, "-E"); args.insert(0, "-E");
} }
let task = std::process::Command::new("sudo").args(args).spawn()?; let task = Command::new("sudo").args(args).spawn()?;
Ok(Some(task)) Ok(Some(task))
} }
@ -553,10 +570,7 @@ pub fn get_default_pa_source() -> Option<(String, String)> {
} }
pub fn lock_screen() { pub fn lock_screen() {
std::process::Command::new("xdg-screensaver") Command::new("xdg-screensaver").arg("lock").spawn().ok();
.arg("lock")
.spawn()
.ok();
} }
pub fn toggle_blank_screen(_v: bool) { pub fn toggle_blank_screen(_v: bool) {
@ -577,7 +591,7 @@ fn get_env_tries(name: &str, uid: &str, n: usize) -> String {
if !x.is_empty() { if !x.is_empty() {
return x; return x;
} }
std::thread::sleep(std::time::Duration::from_millis(300)); std::thread::sleep(Duration::from_millis(300));
} }
"".to_owned() "".to_owned()
} }
@ -604,12 +618,12 @@ pub fn quit_gui() {
pub fn check_super_user_permission() -> ResultType<bool> { pub fn check_super_user_permission() -> ResultType<bool> {
let file = "/usr/share/rustdesk/files/polkit"; let file = "/usr/share/rustdesk/files/polkit";
let arg; let arg;
if std::path::Path::new(file).is_file() { if Path::new(file).is_file() {
arg = file; arg = file;
} else { } else {
arg = "echo"; arg = "echo";
} }
let status = std::process::Command::new("pkexec").arg(arg).status()?; let status = Command::new("pkexec").arg(arg).status()?;
Ok(status.success() && status.code() == Some(0)) Ok(status.success() && status.code() == Some(0))
} }
@ -684,7 +698,7 @@ pub fn current_resolution(name: &str) -> ResultType<Resolution> {
} }
pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> {
std::process::Command::new("xrandr") Command::new("xrandr")
.args(vec![ .args(vec![
"--output", "--output",
name, name,

View File

@ -1,6 +1,9 @@
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <AppKit/AppKit.h> #import <AppKit/AppKit.h>
#import <IOKit/hidsystem/IOHIDLib.h> #import <IOKit/hidsystem/IOHIDLib.h>
#include <Security/Authorization.h>
#include <Security/AuthorizationTags.h>
// https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm // https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm
@ -35,6 +38,33 @@ extern "C" bool InputMonitoringAuthStatus(bool prompt) {
return false; return false;
} }
extern "C" bool MacCheckAdminAuthorization() {
AuthorizationRef authRef;
OSStatus status;
status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
kAuthorizationFlagDefaults, &authRef);
if (status != errAuthorizationSuccess) {
printf("Failed to create AuthorizationRef\n");
return false;
}
AuthorizationItem authItem = {kAuthorizationRightExecute, 0, NULL, 0};
AuthorizationRights authRights = {1, &authItem};
AuthorizationFlags flags = kAuthorizationFlagDefaults |
kAuthorizationFlagInteractionAllowed |
kAuthorizationFlagPreAuthorize |
kAuthorizationFlagExtendRights;
status = AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL);
if (status != errAuthorizationSuccess) {
printf("Failed to authorize\n");
return false;
}
AuthorizationFree(authRef, kAuthorizationFlagDefaults);
return true;
}
extern "C" float BackingScaleFactor() { extern "C" float BackingScaleFactor() {
NSScreen* s = [NSScreen mainScreen]; NSScreen* s = [NSScreen mainScreen];
if (s) return [s backingScaleFactor]; if (s) return [s backingScaleFactor];
@ -44,6 +74,33 @@ extern "C" float BackingScaleFactor() {
// https://github.com/jhford/screenresolution/blob/master/cg_utils.c // https://github.com/jhford/screenresolution/blob/master/cg_utils.c
// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m // https://github.com/jdoupe/screenres/blob/master/setgetscreen.m
size_t bitDepth(CGDisplayModeRef mode) {
size_t depth = 0;
// Deprecated, same display same bpp?
// https://stackoverflow.com/questions/8210824/how-to-avoid-cgdisplaymodecopypixelencoding-to-get-bpp
// https://github.com/libsdl-org/SDL/pull/6628
CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode);
// my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels
// are made up and possibly non-sensical
if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) {
depth = 96;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 64;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) {
depth = 48;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 32;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 30;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 16;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) {
depth = 8;
}
CFRelease(pixelEncoding);
return depth;
}
extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) {
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
if (allModes == NULL) { if (allModes == NULL) {
@ -55,16 +112,28 @@ extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) {
} }
extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) { extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) {
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display);
if (allModes == NULL) { if (currentMode == NULL) {
return false; return false;
} }
*numModes = CFArrayGetCount(allModes); CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
for (int i = 0; i < *numModes && i < max; i++) { if (allModes == NULL) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); CGDisplayModeRelease(currentMode);
widths[i] = (uint32_t)CGDisplayModeGetWidth(mode); return false;
heights[i] = (uint32_t)CGDisplayModeGetHeight(mode);
} }
uint32_t allModeCount = CFArrayGetCount(allModes);
uint32_t realNum = 0;
for (uint32_t i = 0; i < allModeCount && realNum < max; i++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
if (CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) &&
bitDepth(currentMode) == bitDepth(mode)) {
widths[realNum] = (uint32_t)CGDisplayModeGetWidth(mode);
heights[realNum] = (uint32_t)CGDisplayModeGetHeight(mode);
realNum++;
}
}
*numModes = realNum;
CGDisplayModeRelease(currentMode);
CFRelease(allModes); CFRelease(allModes);
return true; return true;
} }
@ -80,31 +149,8 @@ extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t
return true; return true;
} }
size_t bitDepth(CGDisplayModeRef mode) {
size_t depth = 0;
CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode);
// my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels
// are made up and possibly non-sensical
if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) {
depth = 96;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 64;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) {
depth = 48;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 32;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 30;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 16;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) {
depth = 8;
}
CFRelease(pixelEncoding);
return depth;
}
bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) {
CGError rc; CGError rc;
CGDisplayConfigRef config; CGDisplayConfigRef config;
rc = CGBeginDisplayConfiguration(&config); rc = CGBeginDisplayConfiguration(&config);
@ -122,7 +168,6 @@ bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) {
return true; return true;
} }
extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height)
{ {
bool ret = false; bool ret = false;
@ -136,13 +181,12 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
return ret; return ret;
} }
int numModes = CFArrayGetCount(allModes); int numModes = CFArrayGetCount(allModes);
CGDisplayModeRef bestMode = NULL;
for (int i = 0; i < numModes; i++) { for (int i = 0; i < numModes; i++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
if (width == CGDisplayModeGetWidth(mode) && if (width == CGDisplayModeGetWidth(mode) &&
height == CGDisplayModeGetHeight(mode) && height == CGDisplayModeGetHeight(mode) &&
bitDepth(currentMode) == bitDepth(mode) && CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) &&
CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) { bitDepth(currentMode) == bitDepth(mode)) {
ret = setDisplayToMode(display, mode); ret = setDisplayToMode(display, mode);
break; break;
} }

View File

@ -34,6 +34,7 @@ extern "C" {
static kAXTrustedCheckOptionPrompt: CFStringRef; static kAXTrustedCheckOptionPrompt: CFStringRef;
fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL;
fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; fn InputMonitoringAuthStatus(_: BOOL) -> BOOL;
fn MacCheckAdminAuthorization() -> BOOL;
fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL;
fn MacGetModes( fn MacGetModes(
display: u32, display: u32,
@ -612,18 +613,18 @@ pub fn resolutions(name: &str) -> Vec<Resolution> {
unsafe { unsafe {
if YES == MacGetModeNum(display, &mut num) { if YES == MacGetModeNum(display, &mut num) {
let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]);
let mut realNum = 0; let mut real_num = 0;
if YES if YES
== MacGetModes( == MacGetModes(
display, display,
widths.as_mut_ptr(), widths.as_mut_ptr(),
heights.as_mut_ptr(), heights.as_mut_ptr(),
num, num,
&mut realNum, &mut real_num,
) )
{ {
if realNum <= num { if real_num <= num {
for i in 0..realNum { for i in 0..real_num {
let resolution = Resolution { let resolution = Resolution {
width: widths[i as usize] as _, width: widths[i as usize] as _,
height: heights[i as usize] as _, height: heights[i as usize] as _,
@ -665,3 +666,10 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<
} }
Ok(()) Ok(())
} }
pub fn check_super_user_permission() -> ResultType<bool> {
unsafe {
Ok(MacCheckAdminAuthorization() == YES)
}
}

View File

@ -1236,6 +1236,15 @@ pub fn uninstall_me() -> ResultType<()> {
fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathBuf> { fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathBuf> {
let mut tmp = std::env::temp_dir(); let mut tmp = std::env::temp_dir();
// When dir contains these characters, the bat file will not execute in elevated mode.
if vec!["&", "@", "^"]
.drain(..)
.any(|s| tmp.to_string_lossy().to_string().contains(s))
{
if let Ok(dir) = user_accessible_folder() {
tmp = dir;
}
}
tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext)); tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext));
let mut file = std::fs::File::create(&tmp)?; let mut file = std::fs::File::create(&tmp)?;
// in case cmds mixed with \r\n and \n, make sure all ending with \r\n // in case cmds mixed with \r\n and \n, make sure all ending with \r\n
@ -1872,3 +1881,19 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<
Ok(()) Ok(())
} }
} }
pub fn user_accessible_folder() -> ResultType<PathBuf> {
let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string());
let dir1 = PathBuf::from(format!("{}\\ProgramData", disk));
// NOTICE: "C:\Windows\Temp" requires permanent authorization.
let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk));
let dir;
if dir1.exists() {
dir = dir1;
} else if dir2.exists() {
dir = dir2;
} else {
bail!("no vaild user accessible folder");
}
Ok(dir)
}

View File

@ -126,6 +126,7 @@ pub struct Connection {
origin_resolution: HashMap<String, Resolution>, origin_resolution: HashMap<String, Resolution>,
voice_call_request_timestamp: Option<NonZeroI64>, voice_call_request_timestamp: Option<NonZeroI64>,
audio_input_device_before_voice_call: Option<String>, audio_input_device_before_voice_call: Option<String>,
options_in_login: Option<OptionMessage>,
} }
impl ConnInner { impl ConnInner {
@ -233,6 +234,7 @@ impl Connection {
audio_sender: None, audio_sender: None,
voice_call_request_timestamp: None, voice_call_request_timestamp: None,
audio_input_device_before_voice_call: None, audio_input_device_before_voice_call: None,
options_in_login: None,
}; };
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
tokio::spawn(async move { tokio::spawn(async move {
@ -921,6 +923,9 @@ impl Connection {
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_login_response(res); msg_out.set_login_response(res);
self.send(msg_out).await; self.send(msg_out).await;
if let Some(o) = self.options_in_login.take() {
self.update_options(&o).await;
}
if let Some((dir, show_hidden)) = self.file_transfer.clone() { if let Some((dir, show_hidden)) = self.file_transfer.clone() {
let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() {
&dir &dir
@ -1106,8 +1111,7 @@ impl Connection {
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
self.lr = lr.clone(); self.lr = lr.clone();
if let Some(o) = lr.option.as_ref() { if let Some(o) = lr.option.as_ref() {
// It may not be a good practice to update all options here. self.options_in_login = Some(o.clone());
self.update_options(o).await;
if let Some(q) = o.video_codec_state.clone().take() { if let Some(q) = o.video_codec_state.clone().take() {
scrap::codec::Encoder::update_video_encoder( scrap::codec::Encoder::update_video_encoder(
self.inner.id(), self.inner.id(),
@ -1552,7 +1556,6 @@ impl Connection {
.err() .err()
.map_or("".to_string(), |e| e.to_string()); .map_or("".to_string(), |e| e.to_string());
} }
self.portable.elevation_requested = err.is_empty();
let mut misc = Misc::new(); let mut misc = Misc::new();
misc.set_elevation_response(err); misc.set_elevation_response(err);
let mut msg = Message::new(); let mut msg = Message::new();
@ -1571,7 +1574,6 @@ impl Connection {
.err() .err()
.map_or("".to_string(), |e| e.to_string()); .map_or("".to_string(), |e| e.to_string());
} }
self.portable.elevation_requested = err.is_empty();
let mut misc = Misc::new(); let mut misc = Misc::new();
misc.set_elevation_response(err); misc.set_elevation_response(err);
let mut msg = Message::new(); let mut msg = Message::new();
@ -1699,7 +1701,8 @@ impl Connection {
self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
} }
async fn update_options_without_auth(&mut self, o: &OptionMessage) { async fn update_options(&mut self, o: &OptionMessage) {
log::info!("Option update: {:?}", o);
if let Ok(q) = o.image_quality.enum_value() { if let Ok(q) = o.image_quality.enum_value() {
let image_quality; let image_quality;
if let ImageQuality::NotSet = q { if let ImageQuality::NotSet = q {
@ -1730,12 +1733,6 @@ impl Connection {
scrap::codec::EncoderUpdate::State(q), scrap::codec::EncoderUpdate::State(q),
); );
} }
}
async fn update_options_with_auth(&mut self, o: &OptionMessage) {
if !self.authorized {
return;
}
if let Ok(q) = o.lock_after_session_end.enum_value() { if let Ok(q) = o.lock_after_session_end.enum_value() {
if q != BoolOption::NotSet { if q != BoolOption::NotSet {
self.lock_after_session_end = q == BoolOption::Yes; self.lock_after_session_end = q == BoolOption::Yes;
@ -1864,12 +1861,6 @@ impl Connection {
} }
} }
async fn update_options(&mut self, o: &OptionMessage) {
log::info!("Option update: {:?}", o);
self.update_options_without_auth(o).await;
self.update_options_with_auth(o).await;
}
async fn on_close(&mut self, reason: &str, lock: bool) { async fn on_close(&mut self, reason: &str, lock: bool) {
log::info!("#{} Connection closed: {}", self.inner.id(), reason); log::info!("#{} Connection closed: {}", self.inner.id(), reason);
if lock && self.lock_after_session_end && self.keyboard { if lock && self.lock_after_session_end && self.keyboard {
@ -1936,13 +1927,11 @@ impl Connection {
let p = &mut self.portable; let p = &mut self.portable;
if running != p.last_running { if running != p.last_running {
p.last_running = running; p.last_running = running;
if running && p.elevation_requested { let mut misc = Misc::new();
let mut misc = Misc::new(); misc.set_portable_service_running(running);
misc.set_portable_service_running(running); let mut msg = Message::new();
let mut msg = Message::new(); msg.set_misc(misc);
msg.set_misc(misc); self.inner.send(msg.into());
self.inner.send(msg.into());
}
} }
let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone();
if p.last_uac != uac { if p.last_uac != uac {
@ -2166,7 +2155,6 @@ pub struct PortableState {
pub last_foreground_window_elevated: bool, pub last_foreground_window_elevated: bool,
pub last_running: bool, pub last_running: bool,
pub is_installed: bool, pub is_installed: bool,
pub elevation_requested: bool,
} }
#[cfg(windows)] #[cfg(windows)]
@ -2177,7 +2165,6 @@ impl Default for PortableState {
last_uac: Default::default(), last_uac: Default::default(),
last_foreground_window_elevated: Default::default(), last_foreground_window_elevated: Default::default(),
last_running: Default::default(), last_running: Default::default(),
elevation_requested: Default::default(),
} }
} }
} }

View File

@ -117,17 +117,7 @@ impl SharedMemory {
} }
fn flink(name: String) -> ResultType<String> { fn flink(name: String) -> ResultType<String> {
let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); let mut dir = crate::platform::user_accessible_folder()?;
let dir1 = PathBuf::from(format!("{}\\ProgramData", disk));
let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk));
let mut dir;
if dir1.exists() {
dir = dir1;
} else if dir2.exists() {
dir = dir2;
} else {
bail!("no vaild flink directory");
}
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
if !dir.exists() { if !dir.exists() {
std::fs::create_dir(&dir)?; std::fs::create_dir(&dir)?;

View File

@ -577,6 +577,14 @@ fn run(sp: GenericService) -> ResultType<()> {
if last_check_displays.elapsed().as_millis() > 1000 { if last_check_displays.elapsed().as_millis() > 1000 {
last_check_displays = now; last_check_displays = now;
// Capturer on macos does not return Err event the solution is changed.
#[cfg(target_os = "macos")]
if check_display_changed(c.ndisplay, c.current, c.width, c.height) {
log::info!("Displays changed");
*SWITCH.lock().unwrap() = true;
bail!("SWITCH");
}
if let Some(msg_out) = check_displays_changed() { if let Some(msg_out) = check_displays_changed() {
sp.send(msg_out); sp.send(msg_out);
} }

View File

@ -9,7 +9,7 @@ use sciter::Value;
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
config::{self, LocalConfig, PeerConfig}, config::{LocalConfig, PeerConfig},
log, log,
}; };
@ -413,17 +413,15 @@ impl UI {
} }
fn remove_discovered(&mut self, id: String) { fn remove_discovered(&mut self, id: String) {
let mut peers = config::LanPeers::load().peers; remove_discovered(id);
peers.retain(|x| x.id != id);
config::LanPeers::store(&peers);
} }
fn send_wol(&mut self, id: String) { fn send_wol(&mut self, id: String) {
crate::lan::send_wol(id) crate::lan::send_wol(id)
} }
fn new_remote(&mut self, id: String, remote_type: String) { fn new_remote(&mut self, id: String, remote_type: String, force_relay: bool) {
new_remote(id, remote_type) new_remote(id, remote_type, force_relay)
} }
fn is_process_trusted(&mut self, _prompt: bool) -> bool { fn is_process_trusted(&mut self, _prompt: bool) -> bool {
@ -573,6 +571,10 @@ impl UI {
fn default_video_save_directory(&self) -> String { fn default_video_save_directory(&self) -> String {
default_video_save_directory() default_video_save_directory()
} }
fn handle_relay_id(&self, id: String) -> String {
handle_relay_id(id)
}
} }
impl sciter::EventHandler for UI { impl sciter::EventHandler for UI {
@ -590,7 +592,7 @@ impl sciter::EventHandler for UI {
fn set_remote_id(String); fn set_remote_id(String);
fn closing(i32, i32, i32, i32); fn closing(i32, i32, i32, i32);
fn get_size(); fn get_size();
fn new_remote(String, bool); fn new_remote(String, String, bool);
fn send_wol(String); fn send_wol(String);
fn remove_peer(String); fn remove_peer(String);
fn remove_discovered(String); fn remove_discovered(String);
@ -655,6 +657,7 @@ impl sciter::EventHandler for UI {
fn has_hwcodec(); fn has_hwcodec();
fn get_langs(); fn get_langs();
fn default_video_save_directory(); fn default_video_save_directory();
fn handle_relay_id(String);
} }
} }
@ -720,9 +723,13 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc<Vec<Value>> {
} }
#[inline] #[inline]
pub fn new_remote(id: String, remote_type: String) { pub fn new_remote(id: String, remote_type: String, force_relay: bool) {
let mut lock = CHILDREN.lock().unwrap(); let mut lock = CHILDREN.lock().unwrap();
let args = vec![format!("--{}", remote_type), id.clone()]; let mut args = vec![format!("--{}", remote_type), id.clone()];
if force_relay {
args.push("".to_string()); // password
args.push("--relay".to_string());
}
let key = (id.clone(), remote_type.clone()); let key = (id.clone(), remote_type.clone());
if let Some(c) = lock.1.get_mut(&key) { if let Some(c) = lock.1.get_mut(&key) {
if let Ok(Some(_)) = c.try_wait() { if let Ok(Some(_)) = c.try_wait() {

View File

@ -62,12 +62,15 @@ function createNewConnect(id, type) {
id = id.replace(/\s/g, ""); id = id.replace(/\s/g, "");
app.remote_id.value = formatId(id); app.remote_id.value = formatId(id);
if (!id) return; if (!id) return;
var old_id = id;
id = handler.handle_relay_id(id);
var force_relay = old_id != id;
if (id == my_id) { if (id == my_id) {
msgbox("custom-error", "Error", "You cannot connect to your own computer"); msgbox("custom-error", "Error", "You cannot connect to your own computer");
return; return;
} }
handler.set_remote_id(id); handler.set_remote_id(id);
handler.new_remote(id, type); handler.new_remote(id, type, force_relay);
} }
class ShareRdp: Reactor.Component { class ShareRdp: Reactor.Component {

View File

@ -277,6 +277,8 @@ impl InvokeUiSession for SciterHandler {
fn switch_back(&self, _id: &str) {} fn switch_back(&self, _id: &str) {}
fn portable_service_running(&self, _running: bool) {}
fn on_voice_call_started(&self) { fn on_voice_call_started(&self) {
self.call("onVoiceCallStart", &make_args!()); self.call("onVoiceCallStart", &make_args!());
} }
@ -460,6 +462,7 @@ impl sciter::EventHandler for SciterSession {
impl SciterSession { impl SciterSession {
pub fn new(cmd: String, id: String, password: String, args: Vec<String>) -> Self { pub fn new(cmd: String, id: String, password: String, args: Vec<String>) -> Self {
let force_relay = args.contains(&"--relay".to_string());
let session: Session<SciterHandler> = Session { let session: Session<SciterHandler> = Session {
id: id.clone(), id: id.clone(),
password: password.clone(), password: password.clone(),
@ -484,7 +487,7 @@ impl SciterSession {
.lc .lc
.write() .write()
.unwrap() .unwrap()
.initialize(id, conn_type, None, false); .initialize(id, conn_type, None, force_relay);
Self(session) Self(session)
} }

View File

@ -596,6 +596,13 @@ pub fn get_lan_peers() -> Vec<HashMap<&'static str, String>> {
.collect() .collect()
} }
#[inline]
pub fn remove_discovered(id: String) {
let mut peers = config::LanPeers::load().peers;
peers.retain(|x| x.id != id);
config::LanPeers::store(&peers);
}
#[inline] #[inline]
pub fn get_uuid() -> String { pub fn get_uuid() -> String {
base64::encode(hbb_common::get_uuid()) base64::encode(hbb_common::get_uuid())
@ -700,10 +707,10 @@ pub fn is_root() -> bool {
pub fn check_super_user_permission() -> bool { pub fn check_super_user_permission() -> bool {
#[cfg(feature = "flatpak")] #[cfg(feature = "flatpak")]
return true; return true;
#[cfg(any(windows, target_os = "linux"))] #[cfg(any(windows, target_os = "linux", target_os = "macos"))]
return crate::platform::check_super_user_permission().unwrap_or(false); return crate::platform::check_super_user_permission().unwrap_or(false);
#[cfg(not(any(windows, target_os = "linux")))] #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
true return true;
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -963,3 +970,12 @@ async fn check_id(
} }
"" ""
} }
// if it's relay id, return id processed, otherwise return original id
pub fn handle_relay_id(id: String) -> String {
if id.ends_with(r"\r") || id.ends_with(r"/r") {
id[0..id.len() - 2].to_string()
} else {
id
}
}

View File

@ -882,6 +882,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
fn clipboard(&self, content: String); fn clipboard(&self, content: String);
fn cancel_msgbox(&self, tag: &str); fn cancel_msgbox(&self, tag: &str);
fn switch_back(&self, id: &str); fn switch_back(&self, id: &str);
fn portable_service_running(&self, running: bool);
fn on_voice_call_started(&self); fn on_voice_call_started(&self);
fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_closed(&self, reason: &str);
fn on_voice_call_waiting(&self); fn on_voice_call_waiting(&self);

1
vdi/README.md Normal file
View File

@ -0,0 +1 @@
# WIP

View File

@ -0,0 +1,16 @@
FROM rockylinux:9.1
ENV HOME=/home/vscode
ENV WORKDIR=$HOME/rustdesk/vdi/host
# https://ciq.co/blog/top-10-things-to-do-after-rocky-linux-9-install/ also gpu driver install
WORKDIR $HOME
RUN dnf -y install epel-release
RUN dnf config-manager --set-enabled crb
RUN dnf -y install cargo libvpx-devel opus-devel usbredir-devel git cmake gcc-c++ pkg-config nasm yasm ninja-build automake libtool libva-devel libvdpau-devel llvm-devel
WORKDIR /
RUN git clone https://chromium.googlesource.com/libyuv/libyuv
WORKDIR /libyuv
RUN cmake . -DCMAKE_INSTALL_PREFIX=/user
RUN make -j4 && make install
WORKDIR /

View File

@ -0,0 +1,28 @@
{
"name": "rustdesk",
"build": {
"dockerfile": "./Dockerfile",
"context": "."
},
"workspaceMount": "source=${localWorkspaceFolder}/../..,target=/home/vscode/rustdesk,type=bind,consistency=cache",
"workspaceFolder": "/home/vscode/rustdesk/vdi/host",
"customizations": {
"vscode": {
"extensions": [
"vadimcn.vscode-lldb",
"mutantdino.resourcemonitor",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"serayuzgur.crates",
"mhutchie.git-graph",
"formulahendry.terminal",
"eamodio.gitlens"
],
"settings": {
"files.watcherExclude": {
"**/target/**": true
}
}
}
}
}

1
vdi/host/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2147
vdi/host/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
vdi/host/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "qemu-rustdesk"
version = "0.1.0"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
[dependencies]
qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" }
hbb_common = { path = "../../libs/hbb_common" }

1
vdi/host/README.md Normal file
View File

@ -0,0 +1 @@
# RustDesk protocol on QEMU D-Bus display

2
vdi/host/src/main.rs Normal file
View File

@ -0,0 +1,2 @@
fn main() {
}