Merge branch 'master' into modern-dialog

This commit is contained in:
NicKoehler 2023-03-01 18:00:56 +01:00
parent 55831948f8
commit ab4ef977f4
No known key found for this signature in database
GPG Key ID: BAE01394EB51AC58
88 changed files with 2293 additions and 658 deletions

View File

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

View File

@ -732,7 +732,7 @@ jobs:
x86_64)
# no need mock on x86_64
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
@ -900,7 +900,7 @@ jobs:
ln -s /usr/include /vcpkg/installed/arm64-linux/include
export VCPKG_ROOT=/vcpkg
# 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)
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
export VCPKG_ROOT=/vcpkg
# 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

2
Cargo.lock generated
View File

@ -4656,7 +4656,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc"
source = "git+https://github.com/fufesou/rdev#25a99ce71ab42843ad253dd51e6a35e83e87a8a4"
dependencies = [
"cocoa",
"core-foundation 0.9.3",

View File

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

187
build.py
View File

@ -18,14 +18,11 @@ exe_path = 'target/release/' + hbb_name
flutter_win_target_dir = 'flutter/build/windows/runner/Release/'
skip_cargo = False
def custom_os_system(cmd):
err = os._system(cmd)
def system2(cmd):
err = os.system(cmd)
if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.")
sys.exit(-1)
# replace prebuilt os.system
os._system = os.system
os.system = custom_os_system
def get_version():
with open("Cargo.toml", encoding="utf-8") as fh:
@ -144,8 +141,8 @@ def generate_build_script_for_docker():
# build rustdesk
./build.py --flutter --hwcodec
''')
os.system("chmod +x /tmp/build.sh")
os.system("bash /tmp/build.sh")
system2("chmod +x /tmp/build.sh")
system2("bash /tmp/build.sh")
def download_extract_features(features, res_dir):
@ -250,7 +247,7 @@ def get_features(args):
def generate_control_file(version):
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
Version: %s
@ -268,45 +265,45 @@ Description: A remote control software.
def ffi_bindgen_function_refactor():
# 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')
def build_flutter_deb(version, features):
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()
os.chdir('flutter')
os.system('flutter build linux --release')
os.system('mkdir -p tmpdeb/usr/bin/')
os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system('mkdir -p tmpdeb/usr/share/applications/')
os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions')
os.system('rm tmpdeb/usr/bin/rustdesk || true')
os.system(
system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/')
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/applications/')
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/')
os.system(
system2(
'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')
os.system(
system2(
'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')
os.system(
system2(
'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")
os.system('mkdir -p tmpdeb/DEBIAN')
system2('mkdir -p tmpdeb/DEBIAN')
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')
os.system('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
os.system('/bin/rm -rf tmpdeb/')
os.system('/bin/rm -rf ../res/DEBIAN/control')
system2('/bin/rm -rf tmpdeb/')
system2('/bin/rm -rf ../res/DEBIAN/control')
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..")
@ -314,46 +311,43 @@ def build_flutter_deb(version, features):
def build_flutter_dmg(version, features):
if not skip_cargo:
# 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
os.system(
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
# ffi_bindgen_function_refactor()
# limitations from flutter rust bridge
os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h')
os.chdir('flutter')
os.system('flutter build macos --release')
os.system(
"create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
system2('flutter build macos --release')
system2(
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
os.chdir("..")
def build_flutter_arch_manjaro(version, features):
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()
os.chdir('flutter')
os.system('flutter build linux --release')
os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so')
system2('flutter build linux --release')
system2('strip build/linux/x64/release/bundle/lib/librustdesk.so')
os.chdir('../res')
os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
def build_flutter_windows(version, features):
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"):
print("cargo build failed, please check rust source code.")
exit(-1)
os.chdir('flutter')
os.system('flutter build windows --release')
system2('flutter build windows --release')
os.chdir('..')
shutil.copy2('target/release/deps/dylib_virtual_display.dll',
flutter_win_target_dir)
os.chdir('libs/portable')
os.system('pip3 install -r requirements.txt')
os.system(
system2('pip3 install -r requirements.txt')
system2(
f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe')
os.chdir('../..')
if os.path.exists('./rustdesk_portable.exe'):
@ -374,22 +368,15 @@ def main():
parser = make_parser()
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):
os.unlink(exe_path)
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()
features = ','.join(get_features(args))
flutter = args.flutter
if not flutter:
os.system('python3 res/inline-sciter.py')
system2('python3 res/inline-sciter.py')
print(args.skip_cargo)
if args.skip_cargo:
skip_cargo = True
@ -397,55 +384,55 @@ def main():
if windows:
# build virtual display dynamic library
os.chdir('libs/virtual_display/dylib')
os.system('cargo build --release')
system2('cargo build --release')
os.chdir('../../..')
if flutter:
build_flutter_windows(version, features)
return
os.system('cargo build --release --features ' + features)
# os.system('upx.exe target/release/rustdesk.exe')
os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe')
system2('cargo build --release --features ' + features)
# system2('upx.exe target/release/rustdesk.exe')
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P')
if pa:
os.system(
system2(
f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com '
'target\\release\\rustdesk.exe')
else:
print('Not signed')
os.system(
system2(
f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe')
elif os.path.isfile('/usr/bin/pacman'):
# 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:
build_flutter_arch_manjaro(version, features)
else:
os.system('cargo build --release --features ' + features)
os.system('git checkout src/ui/common.tis')
os.system('strip target/release/rustdesk')
os.system('ln -s res/pacman_install && ln -s res/PKGBUILD')
os.system('HBB=`pwd` makepkg -f')
os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (
system2('cargo build --release --features ' + features)
system2('git checkout src/ui/common.tis')
system2('strip target/release/rustdesk')
system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
system2('HBB=`pwd` makepkg -f')
system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (
version, version))
# pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'):
os.system('cargo build --release --features ' + features)
os.system('strip target/release/rustdesk')
os.system(
system2('cargo build --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec')
os.system(
system2('HBB=`pwd` rpmbuild -ba res/rpm.spec')
system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % (
version, version))
# yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'):
os.system('cargo build --release --features ' + features)
os.system('strip target/release/rustdesk')
os.system(
system2('cargo build --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec')
os.system(
system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec')
system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (
version, version))
# yum localinstall rustdesk.rpm
@ -455,18 +442,18 @@ def main():
build_flutter_dmg(version, features)
pass
else:
# os.system(
# system2(
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
build_flutter_deb(version, features)
else:
os.system('cargo bundle --release --features ' + features)
system2('cargo bundle --release --features ' + features)
if osx:
os.system(
system2(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
os.system(
system2(
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# 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"
txt = open(plist).read()
with open(plist, "wt") as fh:
@ -476,7 +463,7 @@ def main():
</dict>"""))
pa = os.environ.get('P')
if pa:
os.system('''
system2('''
# buggy: rcodesign sign ... path/*, have to sign one by one
# install rcodesign via cargo install apple-codesign
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk
@ -486,11 +473,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
'''.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' %
version, 'rustdesk-%s.dmg' % version)
if pa:
os.system('''
system2('''
# https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html
# https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html
# https://developer.apple.com/developer-id/
@ -507,34 +494,32 @@ def main():
print('Not signed')
else:
# buid deb package
os.system(
system2(
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
os.system('dpkg-deb -R rustdesk.deb tmpdeb')
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system(
system2('dpkg-deb -R rustdesk.deb tmpdeb')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
'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')
os.system(
system2(
'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')
os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
os.system('strip tmpdeb/usr/bin/rustdesk')
os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
system2('strip tmpdeb/usr/bin/rustdesk')
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
system2('mv tmpdeb/usr/bin/rustdesk 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/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.system("mv Cargo.toml.bk Cargo.toml")
os.system("mv src/main.rs.bk src/main.rs")
def md5_file(fn):
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__":

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)
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)
@ -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 |
| 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
Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.

View File

@ -11,22 +11,25 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:icon="@mipmap/ic_launcher"
android:label="RustDesk"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true">
android:supportsRtl="true">
<receiver
android:name=".BootReceiver"
android:enabled="false"
android:exported="false">
android:enabled="true"
android:exported="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<!--ACTION_BOOT_COMPLETED for debug test on no root device-->
<action android:name="com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" />
</intent-filter>
</receiver>
@ -53,8 +56,6 @@
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -62,6 +63,11 @@
</intent-filter>
</activity>
<activity
android:name=".PermissionRequestTransparentActivity"
android:excludeFromRecents="true"
android:theme="@style/Transparent" />
<service
android:name=".MainService"
android:enabled="true"
@ -75,4 +81,4 @@
android:value="2" />
</application>
</manifest>
</manifest>

View File

@ -1,21 +1,45 @@
package com.carriez.flutter_hbb
import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.widget.Toast
import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity
const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED"
class BootReceiver : BroadcastReceiver() {
private val logTag = "tagBootReceiver"
override fun onReceive(context: Context, intent: Intent) {
if ("android.intent.action.BOOT_COMPLETED" == intent.action){
val it = Intent(context,MainService::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Log.d(logTag, "onReceive ${intent.action}")
if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) {
// check SharedPreferences config
val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) {
Log.d(logTag, "KEY_START_ON_BOOT_OPT is false")
return
}
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
// check pre-permission
if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){
Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted")
return
}
val it = Intent(context, MainService::class.java).apply {
action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
putExtra(EXT_INIT_FROM_BOOT, true)
}
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(it)
}else{
} else {
context.startService(it)
}
}

View File

@ -7,35 +7,29 @@ package com.carriez.flutter_hbb
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
*/
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.IBinder
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
import androidx.annotation.RequiresApi
import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
const val MEDIA_REQUEST_CODE = 42
class MainActivity : FlutterActivity() {
companion object {
lateinit var flutterMethodChannel: MethodChannel
var flutterMethodChannel: MethodChannel? = null
}
private val channelTag = "mChannel"
private val logTag = "mMainActivity"
private var mediaProjectionResultIntent: Intent? = null
private var mainService: MainService? = null
@RequiresApi(Build.VERSION_CODES.M)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
if (MainService.isReady) {
@ -46,169 +40,32 @@ class MainActivity : FlutterActivity() {
flutterMethodChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelTag
).apply {
// make sure result is set, otherwise flutter will await forever
setMethodCallHandler { call, result ->
when (call.method) {
"init_service" -> {
Intent(activity, MainService::class.java).also {
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
}
if (MainService.isReady) {
result.success(false)
return@setMethodCallHandler
}
getMediaProjection()
result.success(true)
}
"start_capture" -> {
mainService?.let {
result.success(it.startCapture())
} ?: let {
result.success(false)
}
}
"stop_service" -> {
Log.d(logTag, "Stop service")
mainService?.let {
it.destroy()
result.success(true)
} ?: let {
result.success(false)
}
}
"check_permission" -> {
if (call.arguments is String) {
result.success(checkPermission(context, call.arguments as String))
} else {
result.success(false)
}
}
"request_permission" -> {
if (call.arguments is String) {
requestPermission(context, call.arguments as String)
result.success(true)
} else {
result.success(false)
}
}
"check_video_permission" -> {
mainService?.let {
result.success(it.checkMediaPermission())
} ?: let {
result.success(false)
}
}
"check_service" -> {
flutterMethodChannel.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
flutterMethodChannel.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to MainService.isReady.toString())
)
result.success(true)
}
"init_input" -> {
initInput()
result.success(true)
}
"stop_input" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
InputService.ctx?.disableSelf()
}
InputService.ctx = null
flutterMethodChannel.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
result.success(true)
}
"cancel_notification" -> {
try {
val id = call.arguments as Int
mainService?.cancelNotification(id)
} finally {
result.success(true)
}
}
"enable_soft_keyboard" -> {
// https://blog.csdn.net/hanye2020/article/details/105553780
try {
if (call.arguments as Boolean) {
window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}
} finally {
result.success(true)
}
}
else -> {
result.error("-1", "No such method", null)
}
}
}
}
}
private fun getMediaProjection() {
val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val mIntent = mMediaProjectionManager.createScreenCaptureIntent()
startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
}
private fun initService() {
if (mediaProjectionResultIntent == null) {
Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
return
}
Log.d(logTag, "Init service")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = INIT_SERVICE
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
launchMainService(serviceIntent)
}
private fun launchMainService(intent: Intent) {
// TEST api < O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
private fun initInput() {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
)
initFlutterChannel(flutterMethodChannel!!)
}
override fun onResume() {
super.onResume()
val inputPer = InputService.isOpen
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(
flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to inputPer.toString())
)
}
}
private fun requestMediaProjection() {
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
action = ACT_REQUEST_MEDIA_PROJECTION
}
startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == MEDIA_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
mediaProjectionResultIntent = data
initService()
} else {
flutterMethodChannel.invokeMethod("on_media_projection_canceled", null)
}
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
}
}
@ -232,4 +89,138 @@ class MainActivity : FlutterActivity() {
mainService = null
}
}
private fun initFlutterChannel(flutterMethodChannel: MethodChannel) {
flutterMethodChannel.setMethodCallHandler { call, result ->
// make sure result will be invoked, otherwise flutter will await forever
when (call.method) {
"init_service" -> {
Intent(activity, MainService::class.java).also {
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
}
if (MainService.isReady) {
result.success(false)
return@setMethodCallHandler
}
requestMediaProjection()
result.success(true)
}
"start_capture" -> {
mainService?.let {
result.success(it.startCapture())
} ?: let {
result.success(false)
}
}
"stop_service" -> {
Log.d(logTag, "Stop service")
mainService?.let {
it.destroy()
result.success(true)
} ?: let {
result.success(false)
}
}
"check_permission" -> {
if (call.arguments is String) {
result.success(XXPermissions.isGranted(context, call.arguments as String))
} else {
result.success(false)
}
}
"request_permission" -> {
if (call.arguments is String) {
requestPermission(context, call.arguments as String)
result.success(true)
} else {
result.success(false)
}
}
START_ACTION -> {
if (call.arguments is String) {
startAction(context, call.arguments as String)
result.success(true)
} else {
result.success(false)
}
}
"check_video_permission" -> {
mainService?.let {
result.success(it.checkMediaPermission())
} ?: let {
result.success(false)
}
}
"check_service" -> {
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to MainService.isReady.toString())
)
result.success(true)
}
"stop_input" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
InputService.ctx?.disableSelf()
}
InputService.ctx = null
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
result.success(true)
}
"cancel_notification" -> {
if (call.arguments is Int) {
val id = call.arguments as Int
mainService?.cancelNotification(id)
} else {
result.success(true)
}
}
"enable_soft_keyboard" -> {
// https://blog.csdn.net/hanye2020/article/details/105553780
if (call.arguments as Boolean) {
window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}
result.success(true)
}
GET_START_ON_BOOT_OPT -> {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
}
SET_START_ON_BOOT_OPT -> {
if (call.arguments is Boolean) {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
val edit = prefs.edit()
edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean)
edit.apply()
result.success(true)
} else {
result.success(false)
}
}
SYNC_APP_DIR_CONFIG_PATH -> {
if (call.arguments is String) {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
val edit = prefs.edit()
edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String)
edit.apply()
result.success(true)
} else {
result.success(false)
}
}
else -> {
result.error("-1", "No such method", null)
}
}
}
}
}

View File

@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import java.util.concurrent.Executors
import kotlin.concurrent.thread
import org.json.JSONException
@ -43,10 +44,6 @@ import java.nio.ByteBuffer
import kotlin.math.max
import kotlin.math.min
const val EXTRA_MP_DATA = "mp_intent"
const val INIT_SERVICE = "init_service"
const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY"
const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY"
const val DEFAULT_NOTIFY_TITLE = "RustDesk"
const val DEFAULT_NOTIFY_TEXT = "Service is running"
@ -147,7 +144,11 @@ class MainService : Service() {
// jvm call rust
private external fun init(ctx: Context)
private external fun startServer()
/// When app start on boot, app_dir will not be passed from flutter
/// so pass a app_dir here to rust server
private external fun startServer(app_dir: String)
private external fun startService()
private external fun onVideoFrameUpdate(buf: ByteBuffer)
private external fun onAudioFrameUpdate(buf: ByteBuffer)
private external fun translateLocale(localeName: String, input: String): String
@ -195,6 +196,7 @@ class MainService : Service() {
override fun onCreate() {
super.onCreate()
Log.d(logTag,"MainService onCreate")
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
@ -202,7 +204,13 @@ class MainService : Service() {
}
updateScreenInfo(resources.configuration.orientation)
initNotification()
startServer()
// keep the config dir same with flutter
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
startServer(configPath)
createForegroundNotification()
}
override fun onDestroy() {
@ -277,22 +285,30 @@ class MainService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("whichService", "this service:${Thread.currentThread()}")
Log.d("whichService", "this service: ${Thread.currentThread()}")
super.onStartCommand(intent, flags, startId)
if (intent?.action == INIT_SERVICE) {
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
createForegroundNotification()
val mMediaProjectionManager =
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
startService()
}
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
mediaProjection =
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
checkMediaPermission()
init(this)
_isReady = true
} ?: let {
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
requestMediaProjection()
}
}
return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control
return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -300,6 +316,14 @@ class MainService : Service() {
updateScreenInfo(newConfig.orientation)
}
private fun requestMediaProjection() {
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
action = ACT_REQUEST_MEDIA_PROJECTION
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
@SuppressLint("WrongConstant")
private fun createSurface(): Surface? {
return if (useVP9) {
@ -400,13 +424,13 @@ class MainService : Service() {
fun checkMediaPermission(): Boolean {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to isReady.toString())
)
}
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
@ -653,8 +677,8 @@ class MainService : Service() {
@SuppressLint("UnspecifiedImmutableFlag")
private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent {
val intent = Intent(this, MainService::class.java).apply {
action = ACTION_LOGIN_REQ_NOTIFY
putExtra(EXTRA_LOGIN_REQ_NOTIFY, res)
action = ACT_LOGIN_REQ_NOTIFY
putExtra(EXT_LOGIN_REQ_NOTIFY, res)
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE)

View File

@ -0,0 +1,54 @@
package com.carriez.flutter_hbb
import android.app.Activity
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.util.Log
class PermissionRequestTransparentActivity: Activity() {
private val logTag = "permissionRequest"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}")
when (intent.action) {
ACT_REQUEST_MEDIA_PROJECTION -> {
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = mediaProjectionManager.createScreenCaptureIntent()
startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION)
}
else -> finish()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) {
if (resultCode == RESULT_OK && data != null) {
launchService(data)
} else {
setResult(RES_FAILED)
}
}
finish()
}
private fun launchService(mediaProjectionResultIntent: Intent) {
Log.d(logTag, "Launch MainService")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
}

View File

@ -1,5 +1,6 @@
package com.carriez.flutter_hbb
import android.Manifest.permission.*
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
@ -12,8 +13,8 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import android.provider.Settings
import android.provider.Settings.*
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission
@ -22,6 +23,31 @@ import java.nio.ByteBuffer
import java.util.*
// intent action, extra
const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION"
const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE"
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
// Activity requestCode
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
const val REQ_REQUEST_MEDIA_PROJECTION = 201
// Activity responseCode
const val RES_FAILED = -100
// Flutter channel
const val START_ACTION = "start_action"
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH"
@SuppressLint("ConstantLocale")
val LOCAL_NAME = Locale.getDefault().toString()
val SCREEN_INFO = Info(0, 0, 1, 200)
@ -30,61 +56,13 @@ data class Info(
var width: Int, var height: Int, var scale: Int, var dpi: Int
)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun testVP9Support(): Boolean {
return true
val res = MediaCodecList(MediaCodecList.ALL_CODECS)
.findEncoderForFormat(
MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_VP9,
SCREEN_INFO.width,
SCREEN_INFO.width
)
)
return res != null
}
@RequiresApi(Build.VERSION_CODES.M)
fun requestPermission(context: Context, type: String) {
val permission = when (type) {
"ignore_battery_optimizations" -> {
try {
context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:" + context.packageName)
})
} catch (e:Exception) {
e.printStackTrace()
}
return
}
"application_details_settings" -> {
try {
context.startActivity(Intent().apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = "android.settings.APPLICATION_DETAILS_SETTINGS"
data = Uri.parse("package:" + context.packageName)
})
} catch (e:Exception) {
e.printStackTrace()
}
return
}
"audio" -> {
Permission.RECORD_AUDIO
}
"file" -> {
Permission.MANAGE_EXTERNAL_STORAGE
}
else -> {
return
}
}
XXPermissions.with(context)
.permission(permission)
.permission(type)
.request { _, all ->
if (all) {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
MainActivity.flutterMethodChannel?.invokeMethod(
"on_android_permission_result",
mapOf("type" to type, "result" to all)
)
@ -93,24 +71,18 @@ fun requestPermission(context: Context, type: String) {
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun checkPermission(context: Context, type: String): Boolean {
val permission = when (type) {
"ignore_battery_optimizations" -> {
val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
return pw.isIgnoringBatteryOptimizations(context.packageName)
}
"audio" -> {
Permission.RECORD_AUDIO
}
"file" -> {
Permission.MANAGE_EXTERNAL_STORAGE
}
else -> {
return false
}
fun startAction(context: Context, action: String) {
try {
context.startActivity(Intent(action).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS
if (ACTION_ACCESSIBILITY_SETTINGS != action) {
data = Uri.parse("package:" + context.packageName)
}
})
} catch (e: Exception) {
e.printStackTrace()
}
return XXPermissions.isGranted(context, permission)
}
class AudioReader(val bufSize: Int, private val maxFrames: Int) {

View File

@ -15,4 +15,12 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Transparent" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

View File

@ -109,27 +109,32 @@ class IconFont {
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
const ColorThemeExtension({
required this.border,
required this.border2,
required this.highlight,
});
final Color? border;
final Color? border2;
final Color? highlight;
static const light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
border2: Color(0xFFBBBBBB),
highlight: Color(0xFFE5E5E5),
);
static const dark = ColorThemeExtension(
border: Color(0xFF555555),
border2: Color(0xFFE5E5E5),
highlight: Color(0xFF3F3F3F),
);
@override
ThemeExtension<ColorThemeExtension> copyWith(
{Color? border, Color? highlight}) {
{Color? border, Color? border2, Color? highlight}) {
return ColorThemeExtension(
border: border ?? this.border,
border2: border2 ?? this.border2,
highlight: highlight ?? this.highlight,
);
}
@ -142,6 +147,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
}
return ColorThemeExtension(
border: Color.lerp(border, other.border, t),
border2: Color.lerp(border2, other.border2, t),
highlight: Color.lerp(highlight, other.highlight, t),
);
}
@ -207,38 +213,30 @@ class MyTheme {
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
style: ButtonStyle(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
),
)
: null,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
MyTheme.accent,
),
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
style: ElevatedButton.styleFrom(
backgroundColor: MyTheme.accent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color(0xFFEEEEEE),
style: OutlinedButton.styleFrom(
backgroundColor: Color(
0xFFEEEEEE,
),
foregroundColor: MaterialStatePropertyAll(Colors.black87),
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
foregroundColor: Colors.black87,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
@ -306,46 +304,42 @@ class MyTheme {
tabBarTheme: const TabBarTheme(
labelColor: Colors.white70,
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
style: ButtonStyle(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
disabledForegroundColor: Colors.white70,
foregroundColor: Colors.white70,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
),
)
: null,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
MyTheme.accent,
),
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
style: ElevatedButton.styleFrom(
backgroundColor: MyTheme.accent,
disabledForegroundColor: Colors.white70,
disabledBackgroundColor: Colors.white10,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color(0xFF24252B),
),
side: MaterialStatePropertyAll(
BorderSide(color: Colors.white12, width: 0.5),
),
foregroundColor: MaterialStatePropertyAll(Colors.white70),
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
style: OutlinedButton.styleFrom(
backgroundColor: Color(0xFF24252B),
side: BorderSide(color: Colors.white12, width: 0.5),
disabledForegroundColor: Colors.white70,
foregroundColor: Colors.white70,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
@ -1045,21 +1039,14 @@ class AccessibilityListener extends StatelessWidget {
}
}
class PermissionManager {
class AndroidPermissionManager {
static Completer<bool>? _completer;
static Timer? _timer;
static var _current = "";
static final permissions = [
"audio",
"file",
"ignore_battery_optimizations",
"application_details_settings"
];
static bool isWaitingFile() {
if (_completer != null) {
return !_completer!.isCompleted && _current == "file";
return !_completer!.isCompleted && _current == kManageExternalStorage;
}
return false;
}
@ -1068,31 +1055,33 @@ class PermissionManager {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type)) {
return Future.error("Wrong permission!$type");
}
return gFFI.invokeMethod("check_permission", type);
}
// startActivity goto Android Setting's page to request permission manually by user
static void startAction(String action) {
gFFI.invokeMethod(AndroidChannel.kStartAction, action);
}
/// We use XXPermissions to request permissions,
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
static Future<bool> request(String type) {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type)) {
return Future.error("Wrong permission!$type");
}
gFFI.invokeMethod("request_permission", type);
if (type == "ignore_battery_optimizations") {
return Future.value(false);
// clear last task
if (_completer?.isCompleted == false) {
_completer?.complete(false);
}
_timer?.cancel();
_current = type;
_completer = Completer<bool>();
gFFI.invokeMethod("request_permission", type);
// timeout
_timer?.cancel();
_timer = Timer(Duration(seconds: 60), () {
_timer = Timer(Duration(seconds: 120), () {
if (_completer == null) return;
if (!_completer!.isCompleted) {
_completer!.complete(false);
@ -1622,8 +1611,8 @@ connect(BuildContext context, String id,
}
} else {
if (isFileTransfer) {
if (!await PermissionManager.check("file")) {
if (!await PermissionManager.request("file")) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
}
}

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const int kMainWindowId = 0;
@ -58,6 +59,12 @@ const double kDesktopFileTransferMaximumWidth = 300;
const double kDesktopFileTransferRowHeight = 30.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
const int $nbsp = 0x00A0;
@ -79,6 +86,7 @@ const kDefaultScrollAmountMultiplier = 5.0;
const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0;
const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
@ -129,6 +137,25 @@ const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true;
/// Android constants
const kActionApplicationDetailsSettings =
"android.settings.APPLICATION_DETAILS_SETTINGS";
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
const kRecordAudio = "android.permission.RECORD_AUDIO";
const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
const kRequestIgnoreBatteryOptimizations =
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
/// Android channel invoke type key
class AndroidChannel {
static final kStartAction = "start_action";
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
static final kSyncAppDirConfigPath = "sync_app_dir";
}
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
/// see [LogicalKeyboardKey.keyLabel]
const Map<int, String> logicalKeyMap = <int, String>{

View File

@ -19,7 +19,7 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
const double _kTabWidth = 235;
const double _kTabWidth = 200;
const double _kTabHeight = 42;
const double _kCardFixedWidth = 540;
const double _kCardLeftMargin = 15;
@ -538,6 +538,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
translate('Screen Share'),
translate('Deny remote access'),
],
enabled: enabled,
initialKey: initialKey,
onChanged: (mode) async {
String modeValue;
@ -667,6 +668,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return _Card(title: 'Password', children: [
_ComboBox(
enabled: !locked,
keys: modeKeys,
values: modeValues,
initialKey: modeInitialKey,
@ -1722,7 +1724,6 @@ class _ComboBox extends StatelessWidget {
required this.values,
required this.initialKey,
required this.onChanged,
// ignore: unused_element
this.enabled = true,
}) : super(key: key);
@ -1735,7 +1736,12 @@ class _ComboBox extends StatelessWidget {
var ref = values[index].obs;
current = keys[index];
return Container(
decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
decoration: BoxDecoration(
border: Border.all(
color: enabled
? MyTheme.color(context).border2 ?? MyTheme.border
: MyTheme.border,
)),
height: 30,
child: Obx(() => DropdownButton<String>(
isExpanded: true,
@ -1744,6 +1750,10 @@ class _ComboBox extends StatelessWidget {
underline: Container(
height: 25,
),
style: TextStyle(
color: enabled
? Theme.of(context).textTheme.titleMedium?.color
: _disabledTextColor(context, enabled)),
icon: const Icon(
Icons.expand_more_sharp,
size: 20,

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
@ -13,7 +15,51 @@ class InstallPage extends StatefulWidget {
State<InstallPage> createState() => _InstallPageState();
}
class _InstallPageState extends State<InstallPage> with WindowListener {
class _InstallPageState extends State<InstallPage> {
final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override
void initState() {
super.initState();
Get.put<DesktopTabController>(tabController);
const lable = "install";
tabController.add(TabInfo(
key: lable,
label: lable,
closable: false,
page: _InstallPageBody(
key: const ValueKey(lable),
)));
}
@override
void dispose() {
super.dispose();
Get.delete<DesktopTabController>();
}
@override
Widget build(BuildContext context) {
return DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
child: Container(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(controller: tabController)),
),
);
}
}
class _InstallPageBody extends StatefulWidget {
const _InstallPageBody({Key? key}) : super(key: key);
@override
State<_InstallPageBody> createState() => _InstallPageBodyState();
}
class _InstallPageBodyState extends State<_InstallPageBody>
with WindowListener {
late final TextEditingController controller;
final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs;
@ -46,15 +92,19 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
final double em = 13;
final btnFontSize = 0.9 * em;
final double button_radius = 6;
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
final buttonStyle = OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(button_radius)),
));
final inputBorder = OutlineInputBorder(
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(
backgroundColor: Colors.white,
backgroundColor: null,
body: SingleChildScrollView(
child: Column(
children: [
@ -91,8 +141,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
style: buttonStyle,
child: Text(translate('Change Path'),
style: TextStyle(
color: Colors.black87,
fontSize: btnFontSize)))
color: textColor, fontSize: btnFontSize)))
.marginOnly(left: em))
],
).marginSymmetric(vertical: 2 * em),
@ -127,8 +176,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
)).marginOnly(top: 2 * em),
Row(children: [Text(translate('agreement_tip'))])
.marginOnly(top: em),
Divider(color: Colors.black87)
.marginSymmetric(vertical: 0.5 * em),
Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em),
Row(
children: [
Expanded(
@ -143,8 +191,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
style: buttonStyle,
child: Text(translate('Cancel'),
style: TextStyle(
color: Colors.black87,
fontSize: btnFontSize)))
color: textColor, fontSize: btnFontSize)))
.marginOnly(right: 2 * em)),
Obx(() => ElevatedButton(
onPressed: btnEnabled.value ? install : null,
@ -167,8 +214,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
style: buttonStyle,
child: Text(translate('Run without install'),
style: TextStyle(
color: Colors.black87,
fontSize: btnFontSize)))
color: textColor, fontSize: btnFontSize)))
.marginOnly(left: 2 * em)),
),
],

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
@ -26,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
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(
params: params,
),

View File

@ -942,6 +942,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
disableClipboard(),
lockAfterSessionEnd(),
privacyMode(),
swapKey(),
]);
}
@ -975,12 +976,13 @@ class _DisplayMenuState extends State<_DisplayMenu> {
final canvasModel = widget.ffi.canvasModel;
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
canvasModel.windowBorderWidth * 2) *
CanvasModel.leftToEdge +
CanvasModel.rightToEdge) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
canvasModel.tabBarHeight +
canvasModel.windowBorderWidth * 2) *
CanvasModel.topToEdge +
CanvasModel.bottomToEdge) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
@ -1049,10 +1051,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
final canvasModel = widget.ffi.canvasModel;
final displayWidth = canvasModel.getDisplayWidth();
final displayHeight = canvasModel.getDisplayHeight();
final requiredWidth = displayWidth +
(canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2);
final requiredHeight = displayHeight +
(canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2);
final requiredWidth =
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
final requiredHeight =
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
return selfWidth > (requiredWidth * scale) &&
selfHeight > (requiredHeight * scale);
}
@ -1549,6 +1551,23 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
child: Text(translate('Privacy mode')));
}
swapKey() {
final visible = perms['keyboard'] != false &&
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS));
if (!visible) return Offstage();
final option = 'allow_swap_key';
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
return _CheckboxMenuButton(
value: value,
onChanged: (value) {
if (value == null) return;
bind.sessionToggleOption(id: widget.id, value: option);
},
ffi: widget.ffi,
child: Text(translate('Swap control-command key')));
}
}
class _KeyboardMenu extends StatelessWidget {
@ -1564,9 +1583,8 @@ class _KeyboardMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Do not check permission here?
// var ffiModel = Provider.of<FfiModel>(context);
// if (ffiModel.permissions['keyboard'] == false) return Offstage();
var ffiModel = Provider.of<FfiModel>(context);
if (ffiModel.permissions['keyboard'] == false) return Offstage();
if (stateGlobal.grabKeyboard) {
if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) {
bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode);

View File

@ -14,6 +14,7 @@ class DesktopScrollWrapper extends StatelessWidget {
return ImprovedScrolling(
scrollController: scrollController,
enableCustomMouseWheelScrolling: true,
// enableKeyboardScrolling: true, // strange behavior on mac
customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
scrollDuration: kDefaultScrollDuration,
scrollCurve: Curves.linearToEaseOut,

View File

@ -53,6 +53,7 @@ enum DesktopTabType {
remoteScreen,
fileTransfer,
portForward,
install,
}
class DesktopTabState {
@ -249,8 +250,9 @@ class DesktopTab extends StatelessWidget {
this.unSelectedTabBackgroundColor,
}) : super(key: key) {
tabType = controller.tabType;
isMainWindow =
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
isMainWindow = tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
}
static RxString labelGetterAlias(String peerId) {
@ -361,7 +363,8 @@ class DesktopTab extends StatelessWidget {
/// - hide single item when only has one item (home) on [DesktopTabPage].
bool isHideSingleItem() {
return state.value.tabs.length == 1 &&
controller.tabType == DesktopTabType.main;
(controller.tabType == DesktopTabType.main ||
controller.tabType == DesktopTabType.install);
}
Widget _buildBar() {
@ -523,12 +526,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
super.dispose();
}
void _setMaximize(bool maximize) {
stateGlobal.setMaximize(maximize);
setState(() {});
}
@override
void onWindowMaximize() {
// catch maximize from system
if (!widget.isMaximized.value) {
widget.isMaximized.value = true;
}
_setMaximize(true);
super.onWindowMaximize();
}
@ -538,6 +547,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
if (widget.isMaximized.value) {
widget.isMaximized.value = false;
}
_setMaximize(false);
super.onWindowUnmaximize();
}
@ -752,7 +762,8 @@ class _ListView extends StatelessWidget {
/// - hide single item when only has one item (home) on [DesktopTabPage].
bool isHideSingleItem() {
return state.value.tabs.length == 1 &&
controller.tabType == DesktopTabType.main;
controller.tabType == DesktopTabType.main ||
controller.tabType == DesktopTabType.install;
}
@override

View File

@ -153,6 +153,7 @@ void runMainApp(bool startService) async {
void runMobileApp() async {
await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath();
runApp(App());
}
@ -291,17 +292,20 @@ void _runApp(
void runInstallPage() async {
await windowManager.ensureInitialized();
await initEnv(kAppTypeMain);
_runApp('', const InstallPage(), ThemeMode.light);
windowManager.waitUntilReadyToShow(
WindowOptions(size: Size(800, 600), center: true), () async {
_runApp('', const InstallPage(), MyTheme.currentThemeMode());
WindowOptions windowOptions =
getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true);
windowManager.waitUntilReadyToShow(windowOptions, () async {
windowManager.show();
windowManager.focus();
windowManager.setOpacity(1);
windowManager.setAlignment(Alignment.center); // ensure
windowManager.setTitle(getWindowName());
});
}
WindowOptions getHiddenTitleBarWindowOptions({Size? size}) {
WindowOptions getHiddenTitleBarWindowOptions(
{Size? size, bool center = false}) {
var defaultTitleBarStyle = TitleBarStyle.hidden;
// we do not hide titlebar on win7 because of the frame overflow.
if (kUseCompatibleUiMode) {
@ -309,7 +313,7 @@ WindowOptions getHiddenTitleBarWindowOptions({Size? size}) {
}
return WindowOptions(
size: size,
center: false,
center: center,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: defaultTitleBarStyle,

View File

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/server_model.dart';
import 'home_page.dart';
@ -40,14 +41,14 @@ class ServerPage extends StatefulWidget implements PageShape {
value: "setTemporaryPasswordLength",
enabled:
gFFI.serverModel.verificationMethod != kUsePermanentPassword,
child: Text(translate("Set temporary password length")),
child: Text(translate("One-time password length")),
),
const PopupMenuDivider(),
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: kUseTemporaryPassword,
child: ListTile(
title: Text(translate("Use temporary password")),
title: Text(translate("Use one-time password")),
trailing: Icon(
Icons.check,
color: gFFI.serverModel.verificationMethod ==
@ -150,10 +151,11 @@ class _ServerPageState extends State<ServerPage> {
}
void checkService() async {
gFFI.invokeMethod("check_service"); // jvm
// for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page
if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
PermissionManager.complete("file", await PermissionManager.check("file"));
gFFI.invokeMethod("check_service");
// for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
AndroidPermissionManager.complete(kManageExternalStorage,
await AndroidPermissionManager.check(kManageExternalStorage));
debugPrint("file permission finished");
}
}
@ -567,7 +569,7 @@ void androidChannelInit() {
{
var type = arguments["type"] as String;
var result = arguments["result"] as bool;
PermissionManager.complete(type, result);
AndroidPermissionManager.complete(type, result);
break;
}
case "on_media_projection_canceled":

View File

@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
@ -31,18 +32,20 @@ class SettingsPage extends StatefulWidget implements PageShape {
}
const url = 'https://rustdesk.com/';
final _hasIgnoreBattery = androidVersion >= 26;
var _ignoreBatteryOpt = false;
var _enableAbr = false;
var _denyLANDiscovery = false;
var _onlyWhiteList = false;
var _enableDirectIPAccess = false;
var _enableRecordSession = false;
var _autoRecordIncomingSession = false;
var _localIP = "";
var _directAccessPort = "";
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
final _hasIgnoreBattery = androidVersion >= 26;
var _ignoreBatteryOpt = false;
var _enableStartOnBoot = false;
var _enableAbr = false;
var _denyLANDiscovery = false;
var _onlyWhiteList = false;
var _enableDirectIPAccess = false;
var _enableRecordSession = false;
var _autoRecordIncomingSession = false;
var _localIP = "";
var _directAccessPort = "";
@override
void initState() {
super.initState();
@ -50,11 +53,34 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
() async {
var update = false;
if (_hasIgnoreBattery) {
update = await updateIgnoreBatteryStatus();
if (await checkAndUpdateIgnoreBatteryStatus()) {
update = true;
}
}
final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N";
if (await checkAndUpdateStartOnBoot()) {
update = true;
}
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
var enableStartOnBoot =
await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt);
if (enableStartOnBoot) {
if (!await canStartOnBoot()) {
enableStartOnBoot = false;
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
}
}
if (enableStartOnBoot != _enableStartOnBoot) {
update = true;
_enableStartOnBoot = enableStartOnBoot;
}
final enableAbrRes = option2bool(
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
if (enableAbrRes != _enableAbr) {
update = true;
_enableAbr = enableAbrRes;
@ -125,15 +151,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
() async {
if (await updateIgnoreBatteryStatus()) {
final ibs = await checkAndUpdateIgnoreBatteryStatus();
final sob = await checkAndUpdateStartOnBoot();
if (ibs || sob) {
setState(() {});
}
}();
}
}
Future<bool> updateIgnoreBatteryStatus() async {
final res = await PermissionManager.check("ignore_battery_optimizations");
Future<bool> checkAndUpdateIgnoreBatteryStatus() async {
final res = await AndroidPermissionManager.check(
kRequestIgnoreBatteryOptimizations);
if (_ignoreBatteryOpt != res) {
_ignoreBatteryOpt = res;
return true;
@ -142,6 +171,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
}
}
Future<bool> checkAndUpdateStartOnBoot() async {
if (!await canStartOnBoot() && _enableStartOnBoot) {
_enableStartOnBoot = false;
debugPrint(
"checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false");
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
return true;
} else {
return false;
}
}
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
@ -265,7 +306,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
]),
onToggle: (v) async {
if (v) {
PermissionManager.request("ignore_battery_optimizations");
await AndroidPermissionManager.request(
kRequestIgnoreBatteryOptimizations);
} else {
final res = await gFFI.dialogManager
.show<bool>((setState, close) => CustomAlertDialog(
@ -282,11 +324,44 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
));
if (res == true) {
PermissionManager.request("application_details_settings");
AndroidPermissionManager.startAction(
kActionApplicationDetailsSettings);
}
}
}));
}
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: _enableStartOnBoot,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text("${translate('Start on Boot')} (beta)"),
Text(
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
style: Theme.of(context).textTheme.bodySmall),
]),
onToggle: (toValue) async {
if (toValue) {
// 1. request kIgnoreBatteryOptimizations
if (!await AndroidPermissionManager.check(
kRequestIgnoreBatteryOptimizations)) {
if (!await AndroidPermissionManager.request(
kRequestIgnoreBatteryOptimizations)) {
return;
}
}
// 2. request kSystemAlertWindow
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
return;
}
}
// (Optional) 3. request input permission
}
setState(() => _enableStartOnBoot = toValue);
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
}));
return SettingsList(
sections: [
@ -387,6 +462,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
);
}
Future<bool> canStartOnBoot() async {
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
if (_hasIgnoreBattery && !_ignoreBatteryOpt) {
return false;
}
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
return false;
}
return true;
}
}
void showServerSettings(OverlayDialogManager dialogManager) async {

View File

@ -458,10 +458,8 @@ class InputModel {
return;
}
evt['type'] = type;
if (isDesktop) {
y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value;
x -= stateGlobal.windowBorderWidth.value;
}
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel;
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
@ -503,8 +501,21 @@ class InputModel {
}
x += d.x;
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 (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
return;
@ -512,12 +523,12 @@ class InputModel {
}
if (type != '') {
x = 0;
y = 0;
evtX = 0;
evtY = 0;
}
evt['x'] = '${x.round()}';
evt['y'] = '${y.round()}';
evt['x'] = '$evtX';
evt['y'] = '$evtY';
var buttons = '';
switch (evt['buttons']) {
case kPrimaryMouseButton:

View File

@ -617,13 +617,28 @@ class ViewStyle {
final int displayWidth;
final int displayHeight;
ViewStyle({
this.style = '',
this.width = 0.0,
this.height = 0.0,
this.displayWidth = 0,
this.displayHeight = 0,
required this.style,
required this.width,
required this.height,
required this.displayWidth,
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();
@override
@ -652,9 +667,14 @@ class ViewStyle {
double get scale {
double s = 1.0;
if (style == kRemoteViewStyleAdaptive) {
final s1 = width / displayWidth;
final s2 = height / displayHeight;
s = s1 < s2 ? s1 : s2;
if (width != 0 &&
height != 0 &&
displayWidth != 0 &&
displayHeight != 0) {
final s1 = width / displayWidth;
final s2 = height / displayHeight;
s = s1 < s2 ? s1 : s2;
}
}
return s;
}
@ -680,7 +700,7 @@ class CanvasModel with ChangeNotifier {
// scroll offset y percent
double _scrollY = 0.0;
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
ViewStyle _lastViewStyle = ViewStyle();
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
final _imageOverflow = false.obs;
@ -707,12 +727,25 @@ class CanvasModel with ChangeNotifier {
double get scrollX => _scrollX;
double get scrollY => _scrollY;
static double get leftToEdge => (isDesktop || isWebDesktop)
? windowBorderWidth + kDragToResizeAreaPadding.left
: 0;
static double get rightToEdge => (isDesktop || isWebDesktop)
? windowBorderWidth + kDragToResizeAreaPadding.right
: 0;
static double get topToEdge => (isDesktop || isWebDesktop)
? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top
: 0;
static double get bottomToEdge => (isDesktop || isWebDesktop)
? windowBorderWidth + kDragToResizeAreaPadding.bottom
: 0;
updateViewStyle() async {
Size getSize() {
final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here.
double w = size.width - windowBorderWidth * 2;
double h = size.height - tabBarHeight - windowBorderWidth * 2;
double w = size.width - leftToEdge - rightToEdge;
double h = size.height - topToEdge - bottomToEdge;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
@ -786,10 +819,14 @@ class CanvasModel with ChangeNotifier {
return parent.target?.ffiModel.display.height ?? defaultHeight;
}
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
double get tabBarHeight => stateGlobal.tabBarHeight;
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
static double get tabBarHeight => stateGlobal.tabBarHeight;
moveDesktopMouse(double x, double y) {
if (size.width == 0 || size.height == 0) {
return;
}
// On mobile platforms, move the canvas with the cursor.
final dw = getDisplayWidth() * _scale;
final dh = getDisplayHeight() * _scale;
@ -803,7 +840,9 @@ class CanvasModel with ChangeNotifier {
dyOffset = (y - dh * (y / size.height) - _y).toInt();
}
} catch (e) {
// Unhandled Exception: Unsupported operation: Infinity or NaN toInt
debugPrintStack(
label:
'(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e');
return;
}

View File

@ -30,7 +30,7 @@ typedef F4Dart = int Function(Pointer<Utf8>);
typedef F5 = Void Function(Pointer<Utf8>);
typedef F5Dart = void Function(Pointer<Utf8>);
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
// pub fn session_register_texture(id: *const char, ptr: usize)
// pub fn session_register_texture(id: *const char, ptr: usize)
typedef F6 = Void Function(Pointer<Utf8>, Uint64);
typedef F6Dart = void Function(Pointer<Utf8>, int);
@ -56,7 +56,6 @@ class PlatformFFI {
F4Dart? _session_get_rgba_size;
F5Dart? _session_next_rgba;
F6Dart? _session_register_texture;
static get localeName => Platform.localeName;
@ -162,7 +161,8 @@ class PlatformFFI {
dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
_session_next_rgba =
dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
_session_register_texture = dylib.lookupFunction<F6, F6Dart>("session_register_texture");
_session_register_texture =
dylib.lookupFunction<F6, F6Dart>("session_register_texture");
try {
// SYSTEM user failed
_dir = (await getApplicationDocumentsDirectory()).path;
@ -301,4 +301,8 @@ class PlatformFFI {
if (!isAndroid) return Future<bool>(() => false);
return await _toAndroidChannel.invokeMethod(method, arguments);
}
void syncAndroidServiceAppDirConfigPath() {
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
}
}

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
@ -154,7 +155,8 @@ class ServerModel with ChangeNotifier {
/// file true by default (if permission on)
checkAndroidPermission() async {
// audio
if (androidVersion < 30 || !await PermissionManager.check("audio")) {
if (androidVersion < 30 ||
!await AndroidPermissionManager.check(kRecordAudio)) {
_audioOk = false;
bind.mainSetOption(key: "enable-audio", value: "N");
} else {
@ -163,7 +165,7 @@ class ServerModel with ChangeNotifier {
}
// file
if (!await PermissionManager.check("file")) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
_fileOk = false;
bind.mainSetOption(key: "enable-file-transfer", value: "N");
} else {
@ -229,10 +231,10 @@ class ServerModel with ChangeNotifier {
}
toggleAudio() async {
if (!_audioOk && !await PermissionManager.check("audio")) {
final res = await PermissionManager.request("audio");
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
final res = await AndroidPermissionManager.request(kRecordAudio);
if (!res) {
// TODO handle fail
showToast(translate('Failed'));
return;
}
}
@ -243,10 +245,12 @@ class ServerModel with ChangeNotifier {
}
toggleFile() async {
if (!_fileOk && !await PermissionManager.check("file")) {
final res = await PermissionManager.request("file");
if (!_fileOk &&
!await AndroidPermissionManager.check(kManageExternalStorage)) {
final res =
await AndroidPermissionManager.request(kManageExternalStorage);
if (!res) {
// TODO handle fail
showToast(translate('Failed'));
return;
}
}
@ -344,10 +348,6 @@ class ServerModel with ChangeNotifier {
}
}
Future<void> initInput() async {
await parent.target?.invokeMethod("init_input");
}
Future<bool> setPermanentPassword(String newPW) async {
await bind.mainSetPermanentPassword(password: newPW);
await Future.delayed(Duration(milliseconds: 500));
@ -561,7 +561,8 @@ class ServerModel with ChangeNotifier {
}
Future<void> closeAll() async {
await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
await Future.wait(
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
_clients.clear();
tabController.state.value.tabs.clear();
}
@ -684,7 +685,7 @@ String getLoginDialogTag(int id) {
showInputWarnAlert(FFI ffi) {
ffi.dialogManager.show((setState, close) {
submit() {
ffi.serverModel.initInput();
AndroidPermissionManager.startAction(kActionAccessibilitySettings);
close();
}

View File

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

View File

@ -487,7 +487,7 @@
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_IDENTITY = "Apple Development";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;

View File

@ -325,8 +325,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
ref: "3e2655677c54f421f9e378680d8171b95a211e0f"
resolved-ref: "3e2655677c54f421f9e378680d8171b95a211e0f"
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@ -1563,5 +1563,5 @@ packages:
source: hosted
version: "0.1.1"
sdks:
dart: ">=2.18.0 <4.0.0"
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.3.0"

View File

@ -59,7 +59,7 @@ dependencies:
desktop_multi_window:
git:
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
ref: 3e2655677c54f421f9e378680d8171b95a211e0f
freezed_annotation: ^2.0.3
flutter_custom_cursor: ^0.0.4
window_size:
@ -76,7 +76,7 @@ dependencies:
file_picker: ^5.1.0
flutter_svg: ^1.1.5
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).
# if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below).

View File

@ -0,0 +1,5 @@
extern crate hbb_common;
fn main() {
println!("{:?}", hbb_common::config::PeerConfig::load("455058072"));
}

View File

@ -110,10 +110,10 @@ macro_rules! serde_field_string {
}
macro_rules! serde_field_bool {
($struct_name: ident, $field_name: literal, $func: ident) => {
($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct $struct_name {
#[serde(rename = $field_name)]
#[serde(default = $default, rename = $field_name)]
pub v: bool,
}
impl Default for $struct_name {
@ -217,6 +217,8 @@ pub struct PeerConfig {
pub lock_after_session_end: LockAfterSessionEnd,
#[serde(flatten)]
pub privacy_mode: PrivacyMode,
#[serde(flatten)]
pub allow_swap_key: AllowSwapKey,
#[serde(default)]
pub port_forwards: Vec<(i32, String, i32)>,
#[serde(default)]
@ -1035,30 +1037,37 @@ impl PeerConfig {
serde_field_bool!(
ShowRemoteCursor,
"show_remote_cursor",
default_show_remote_cursor
default_show_remote_cursor,
"ShowRemoteCursor::default_show_remote_cursor"
);
serde_field_bool!(
ShowQualityMonitor,
"show_quality_monitor",
default_show_quality_monitor
default_show_quality_monitor,
"ShowQualityMonitor::default_show_quality_monitor"
);
serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio);
serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio, "DisableAudio::default_disable_audio");
serde_field_bool!(
EnableFileTransfer,
"enable_file_transfer",
default_enable_file_transfer
default_enable_file_transfer,
"EnableFileTransfer::default_enable_file_transfer"
);
serde_field_bool!(
DisableClipboard,
"disable_clipboard",
default_disable_clipboard
default_disable_clipboard,
"DisableClipboard::default_disable_clipboard"
);
serde_field_bool!(
LockAfterSessionEnd,
"lock_after_session_end",
default_lock_after_session_end
default_lock_after_session_end,
"LockAfterSessionEnd::default_lock_after_session_end"
);
serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode);
serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode, "PrivacyMode::default_privacy_mode");
serde_field_bool!(AllowSwapKey, "allow_swap_key", default_allow_swap_key, "AllowSwapKey::default_allow_swap_key");
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct LocalConfig {

View File

@ -36,11 +36,11 @@ def main():
def expand():
for fn in glob.glob('./src/lang/*'):
lang = os.path.basename(fn)[:-3]
if lang in ['en','cn']: continue
if lang in ['en','template']: continue
print(lang)
dict = get_lang(lang)
fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8')
for line in open('./src/lang/cn.rs', encoding='utf8'):
for line in open('./src/lang/template.rs', encoding='utf8'):
line_strip = line.strip()
if line_strip.startswith('("'):
k, v = line_split(line_strip)

View File

@ -1230,6 +1230,8 @@ impl LoginConfigHandler {
option.block_input = BoolOption::No.into();
} else if name == "show-quality-monitor" {
config.show_quality_monitor.v = !config.show_quality_monitor.v;
} else if name == "allow_swap_key" {
config.allow_swap_key.v = !config.allow_swap_key.v;
} else {
let is_set = self
.options
@ -1383,6 +1385,8 @@ impl LoginConfigHandler {
self.config.disable_clipboard.v
} else if name == "show-quality-monitor" {
self.config.show_quality_monitor.v
} else if name == "allow_swap_key" {
self.config.allow_swap_key.v
} else {
!self.get_option(name).is_empty()
}
@ -1807,6 +1811,7 @@ pub fn send_mouse(
if check_scroll_on_mac(mask, x, y) {
mouse_event.modifiers.push(ControlKey::Scroll.into());
}
interface.swap_modifier_mouse(&mut mouse_event);
msg_out.set_mouse_event(mouse_event);
interface.send(Data::Message(msg_out));
}
@ -2033,6 +2038,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn is_force_relay(&self) -> bool {
self.get_login_config_handler().read().unwrap().force_relay
}
fn swap_modifier_mouse(&self, _msg : &mut hbb_common::protos::message::MouseEvent) {}
}
/// Data used by the client interface.

View File

@ -1361,7 +1361,7 @@ pub fn send_url_scheme(_url: String) {
#[cfg(target_os = "android")]
pub mod server_side {
use hbb_common::log;
use hbb_common::{log, config};
use jni::{
objects::{JClass, JString},
sys::jstring,
@ -1374,11 +1374,25 @@ pub mod server_side {
pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer(
env: JNIEnv,
_class: JClass,
app_dir: JString,
) {
log::debug!("startServer from java");
log::debug!("startServer from jvm");
if let Ok(app_dir) = env.get_string(app_dir) {
*config::APP_DIR.write().unwrap() = app_dir.into();
}
std::thread::spawn(move || start_server(true));
}
#[no_mangle]
pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startService(
env: JNIEnv,
_class: JClass,
) {
log::debug!("startService from jvm");
config::Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::RendezvousMediator::restart();
}
#[no_mangle]
pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale(
env: JNIEnv,

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"),
("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Connexió no disponible"),
("Legacy mode", "Mode heretat"),
("Map mode", "Mode mapa"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "保持 RustDesk 后台服务"),
("Ignore Battery Optimizations", "忽略电池优化"),
("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"),
("Start on Boot", "开机自启动"),
("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"),
("Connection not allowed", "对方不允许连接"),
("Legacy mode", "传统模式"),
("Map mode", "11 传输"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", ""),
("Legacy mode", ""),
("Map mode", ""),
@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
("No transfers in progress", ""),
("Codec", ""),
("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"),
("Ignore Battery Optimizations", "Ignorer betteri optimeringer"),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Forbindelse ikke tilladt"),
("Legacy mode", "Bagudkompatibilitetstilstand"),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"),
("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"),
("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Verbindung abgelehnt"),
("Legacy mode", "Kompatibilitätsmodus"),
("Map mode", "Kartenmodus"),
@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("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."),
("Reconnect", "Erneut verbinden"),
("Codec", ""),
("Resolution", ""),
("No transfers in progress", ""),
("Codec", "Codec"),
("Resolution", "Auflösung"),
("No transfers in progress", "Keine Übertragungen im Gange"),
].iter().cloned().collect();
}

View File

@ -39,8 +39,8 @@ 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."),
("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."),
("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."),
("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."),
("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."),
("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."),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", ""),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"),
("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"),
("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Conexión no disponible"),
("Legacy mode", "Modo heredado"),
("Map mode", "Modo mapa"),
@ -456,6 +458,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", "Reconectar"),
("Codec", "Códec"),
("Resolution", "Resolución"),
("No transfers in progress", ""),
("No transfers in progress", "No hay transferencias en curso"),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"),
("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"),
("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "اتصال مجاز نیست"),
("Legacy mode", "legacy حالت"),
("Map mode", "map حالت"),
@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "توقف تماس صوتی"),
("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"),
("Reconnect", "اتصال مجدد"),
("No transfers in progress", ""),
("Codec", ""),
("Resolution", ""),
("Codec", "کدک"),
("Resolution", "وضوح"),
("No transfers in progress", "هیچ انتقالی در حال انجام نیست"),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"),
("Ignore Battery Optimizations", "Ignorer les optimisations batterie"),
("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Connexion non autorisée"),
("Legacy mode", "Mode hérité"),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"),
("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"),
("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Η σύνδεση απορρίφθηκε"),
("Legacy mode", "Λειτουργία συμβατότητας"),
("Map mode", "Map mode"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk futtatása a háttérben"),
("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"),
("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "A csatlakozás nem engedélyezett"),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada background service"),
("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Koneksi tidak dijinkan"),
("Legacy mode", "Mode lama"),
("Map mode", "Mode peta"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"),
("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"),
("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Connessione non consentita"),
("Legacy mode", "Modalità legacy"),
("Map mode", "Modalità mappa"),
@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("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."),
("Reconnect", "Riconnetti"),
("No transfers in progress", "Nessun trasferimento in corso"),
("Codec", "Codec"),
("Resolution", "Risoluzione"),
("No transfers in progress", "Nessun trasferimento in corso"),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk バックグラウンドサービスを維持"),
("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"),
("android_open_battery_optimizations_tip", "この機能を使わない場合は、次のRestDeskアプリ設定ページから「バッテリー」に進み、「制限なし」の選択を外してください"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "接続が許可されていません"),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk 백그라운드 서비스로 유지하기"),
("Ignore Battery Optimizations", "배터리 최적화 무시하기"),
("android_open_battery_optimizations_tip", "해당 기능을 비활성화하려면 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]에서 [제한 없음] 선택을 해제하십시오."),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "연결이 허용되지 않음"),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"),
("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"),
("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Қосылу рұқсат етілмеген"),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk achtergronddienst behouden"),
("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"),
("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Verbinding niet toegestaan"),
("Legacy mode", "Verouderde modus"),
("Map mode", "Map mode"),
@ -444,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Default View Style", "Standaard Weergave Stijl"),
("Default Scroll Style", "Standaard Scroll Stijl"),
("Default Image Quality", "Standaard Beeldkwaliteit"),
("Default Codec", "tandaard Codec"),
("Default Codec", "Standaard Codec"),
("Bitrate", "Bitrate"),
("FPS", "FPS"),
("Auto", "Auto"),
@ -452,10 +454,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Spraakoproep"),
("Text chat", "Tekst chat"),
("Stop voice call", "Stop spraakoproep"),
("relay_hint_tip", ""),
("Reconnect", ""),
("Codec", ""),
("Resolution", ""),
("No transfers in progress", ""),
("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."),
("Reconnect", "Herverbinden"),
("Codec", "Codec"),
("Resolution", "Resolutie"),
("No transfers in progress", "Geen overdrachten in uitvoering"),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"),
("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"),
("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ń]"),
("Start on Boot", "Autostart"),
("Start the screen sharing service on boot, requires special permissions", "Uruchom usługę udostępniania ekranu podczas startu, wymaga specjalnych uprawnień"),
("Connection not allowed", "Połączenie niedozwolone"),
("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"),
("Map mode", "Tryb mapowania"),
@ -456,9 +458,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reconnect", "Połącz ponownie"),
("Codec", "Kodek"),
("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", ""),
("No transfers in progress", "Brak transferów w toku"),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Manter o serviço RustDesk em funcionamento"),
("Ignore Battery Optimizations", "Ignorar optimizações de Bateria"),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Ligação não autorizada"),
("Legacy mode", ""),
("Map mode", ""),
@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
("No transfers in progress", ""),
("Codec", ""),
("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"),
("Ignore Battery Optimizations", "Ignorar otimizações de bateria"),
("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Conexão não permitida"),
("Legacy mode", "Modo legado"),
("Map mode", "Modo mapa"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"),
("Ignore Battery Optimizations", "Ignoră optimizările de baterie"),
("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Conexiune neautoriztă"),
("Legacy mode", "Mod legacy"),
("Map mode", "Mod hartă"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Держать в фоне службу RustDesk"),
("Ignore Battery Optimizations", "Игнорировать оптимизацию батареи"),
("android_open_battery_optimizations_tip", "Перейдите на следующую страницу настроек"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Подключение не разрешено"),
("Legacy mode", "Устаревший режим"),
("Map mode", "Режим сопоставления"),
@ -454,8 +456,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Завершить голосовой вызов"),
("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."),
("Reconnect", "Переподключить"),
("Codec", ""),
("Resolution", ""),
("No transfers in progress", ""),
("Codec", "Кодек"),
("Resolution", "Разрешение"),
("No transfers in progress", "Передача не осуществляется"),
].iter().cloned().collect();
}

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", ""),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"),
("Ignore Battery Optimizations", "Prezri optimizacije baterije"),
("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Povezava ni dovoljena"),
("Legacy mode", "Stari način"),
("Map mode", "Način preslikave"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Mbaje shërbimin e sfondit të RustDesk"),
("Ignore Battery Optimizations", "Injoro optimizimet e baterisë"),
("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Lidhja nuk lejohet"),
("Legacy mode", "Modaliteti i trashëgimisë"),
("Map mode", "Modaliteti i hartës"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"),
("Ignore Battery Optimizations", "Zanemari optimizacije baterije"),
("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Konekcija nije dozvoljena"),
("Legacy mode", "Zastareli mod"),
("Map mode", "Mod mapiranja"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Behåll RustDesk i bakgrunden"),
("Ignore Battery Optimizations", "Ignorera batterioptimering"),
("android_open_battery_optimizations_tip", "Om du vill stänga av denna funktion, gå till nästa RustDesk programs inställningar, hitta [Batteri], Checka ur [Obegränsad]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Anslutning ej tillåten"),
("Legacy mode", "Legacy mode"),
("Map mode", "Kartläge"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", ""),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"),
("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"),
("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"),
("Legacy mode", ""),
("Map mode", ""),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"),
("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"),
("android_open_battery_optimizations_tip", ""),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "bağlantıya izin verilmedi"),
("Legacy mode", "Eski mod"),
("Map mode", "Haritalama modu"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "保持RustDesk後台服務"),
("Ignore Battery Optimizations", "忽略電池優化"),
("android_open_battery_optimizations_tip", "如需關閉此功能請在接下來的RustDesk應用設置頁面中找到並進入 [電源] 頁面,取消勾選 [不受限制]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "對方不允許連接"),
("Legacy mode", "傳統模式"),
("Map mode", "11傳輸"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Зберегти фонову службу RustDesk"),
("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"),
("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Підключення не дозволено"),
("Legacy mode", "Застарілий режим"),
("Map mode", "Режим карти"),

View File

@ -312,6 +312,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Giữ dịch vụ nền RustDesk"),
("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"),
("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"),
("Start on Boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Kết nối không đuợc phép"),
("Legacy mode", ""),
("Map mode", ""),

View File

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

View File

@ -1,6 +1,9 @@
#import <AVFoundation/AVFoundation.h>
#import <AppKit/AppKit.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
@ -35,6 +38,33 @@ extern "C" bool InputMonitoringAuthStatus(bool prompt) {
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() {
NSScreen* s = [NSScreen mainScreen];
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/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) {
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, 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) {
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
if (allModes == NULL) {
CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display);
if (currentMode == NULL) {
return false;
}
*numModes = CFArrayGetCount(allModes);
for (uint32_t i = 0; i < *numModes && i < max; i++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
widths[i] = (uint32_t)CGDisplayModeGetWidth(mode);
heights[i] = (uint32_t)CGDisplayModeGetHeight(mode);
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
if (allModes == NULL) {
CGDisplayModeRelease(currentMode);
return false;
}
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);
return true;
}
@ -80,31 +149,8 @@ extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t
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;
CGDisplayConfigRef config;
rc = CGBeginDisplayConfiguration(&config);
@ -122,7 +168,6 @@ bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) {
return true;
}
extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height)
{
bool ret = false;
@ -140,8 +185,8 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
if (width == CGDisplayModeGetWidth(mode) &&
height == CGDisplayModeGetHeight(mode) &&
bitDepth(currentMode) == bitDepth(mode) &&
CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) {
CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) &&
bitDepth(currentMode) == bitDepth(mode)) {
ret = setDisplayToMode(display, mode);
break;
}

View File

@ -34,6 +34,7 @@ extern "C" {
static kAXTrustedCheckOptionPrompt: CFStringRef;
fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL;
fn InputMonitoringAuthStatus(_: BOOL) -> BOOL;
fn MacCheckAdminAuthorization() -> BOOL;
fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL;
fn MacGetModes(
display: u32,
@ -665,3 +666,10 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<
}
Ok(())
}
pub fn check_super_user_permission() -> ResultType<bool> {
unsafe {
Ok(MacCheckAdminAuthorization() == YES)
}
}

View File

@ -976,7 +976,7 @@ fn get_after_install(exe: &str) -> String {
}
pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> {
let uninstall_str = get_uninstall();
let uninstall_str = get_uninstall(false);
let mut path = path.trim_end_matches('\\').to_owned();
let (subkey, _path, start_menu, exe) = get_default_install_info();
let mut exe = exe;
@ -1188,30 +1188,35 @@ pub fn run_after_install() -> ResultType<()> {
}
pub fn run_before_uninstall() -> ResultType<()> {
run_cmds(get_before_uninstall(), true, "before_install")
run_cmds(get_before_uninstall(true), true, "before_install")
}
fn get_before_uninstall() -> String {
fn get_before_uninstall(kill_self: bool) -> String {
let app_name = crate::get_app_name();
let ext = app_name.to_lowercase();
let filter = if kill_self {
"".to_string()
} else {
format!(" /FI \"PID ne {}\"", get_current_pid())
};
format!(
"
chcp 65001
sc stop {app_name}
sc delete {app_name}
taskkill /F /IM {broker_exe}
taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\"
taskkill /F /IM {app_name}.exe{filter}
reg delete HKEY_CLASSES_ROOT\\.{ext} /f
netsh advfirewall firewall delete rule name=\"{app_name} Service\"
",
app_name = app_name,
broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE,
ext = ext,
cur_pid = get_current_pid(),
filter = filter,
)
}
fn get_uninstall() -> String {
fn get_uninstall(kill_self: bool) -> String {
let (subkey, path, start_menu, _) = get_install_info();
format!(
"
@ -1222,7 +1227,7 @@ fn get_uninstall() -> String {
if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\"
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
",
before_uninstall=get_before_uninstall(),
before_uninstall=get_before_uninstall(kill_self),
subkey=subkey,
app_name = crate::get_app_name(),
path = path,
@ -1231,11 +1236,20 @@ fn get_uninstall() -> String {
}
pub fn uninstall_me() -> ResultType<()> {
run_cmds(get_uninstall(), true, "uninstall")
run_cmds(get_uninstall(true), true, "uninstall")
}
fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathBuf> {
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));
let mut file = std::fs::File::create(&tmp)?;
// in case cmds mixed with \r\n and \n, make sure all ending with \r\n
@ -1872,3 +1886,19 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<
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

@ -117,17 +117,7 @@ impl SharedMemory {
}
fn flink(name: String) -> ResultType<String> {
let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string());
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");
}
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
if !dir.exists() {
std::fs::create_dir(&dir)?;

View File

@ -198,6 +198,7 @@ class Header: Reactor.Component {
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
{keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
{keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ? <li #allow_swap_key .toggle-option><span>{svg_checkmark}</span>{translate('Swap control-command key')}</li> : ""}
</menu>
</popup>;
}
@ -440,7 +441,7 @@ function toggleMenuState() {
for (var el in $$(menu#keyboard-options>li)) {
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
}
for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) {
for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key"]) {
var el = self.select('#' + id);
if (el) {
var value = handler.get_toggle_option(id);

View File

@ -707,10 +707,10 @@ pub fn is_root() -> bool {
pub fn check_super_user_permission() -> bool {
#[cfg(feature = "flatpak")]
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);
#[cfg(not(any(windows, target_os = "linux")))]
true
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
return true;
}
#[allow(dead_code)]

View File

@ -373,10 +373,87 @@ impl<T: InvokeUiSession> Session<T> {
return "".to_owned();
}
pub fn swab_modifier_key(&self, msg: &mut KeyEvent) {
let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string());
if allow_swap_key {
if let Some(key_event::Union::ControlKey(ck)) = msg.union {
let ck = ck.enum_value_or_default();
let ck = match ck {
ControlKey::Control => ControlKey::Meta,
ControlKey::Meta => ControlKey::Control,
ControlKey::RControl => ControlKey::Meta,
ControlKey::RWin => ControlKey::Control,
_ => ck,
};
msg.set_control_key(ck);
}
msg.modifiers = msg.modifiers.iter().map(|ck| {
let ck = ck.enum_value_or_default();
let ck = match ck {
ControlKey::Control => ControlKey::Meta,
ControlKey::Meta => ControlKey::Control,
ControlKey::RControl => ControlKey::Meta,
ControlKey::RWin => ControlKey::Control,
_ => ck,
};
hbb_common::protobuf::EnumOrUnknown::new(ck)
}).collect();
let code = msg.chr();
if code != 0 {
let mut peer = self.peer_platform().to_lowercase();
peer.retain(|c| !c.is_whitespace());
let key = match peer.as_str() {
"windows" => {
let key = rdev::win_key_from_scancode(code);
let key = match key {
rdev::Key::ControlLeft => rdev::Key::MetaLeft,
rdev::Key::MetaLeft => rdev::Key::ControlLeft,
rdev::Key::ControlRight => rdev::Key::MetaLeft,
rdev::Key::MetaRight => rdev::Key::ControlLeft,
_ => key,
};
rdev::win_scancode_from_key(key).unwrap_or_default()
}
"macos" => {
let key = rdev::macos_key_from_code(code);
let key = match key {
rdev::Key::ControlLeft => rdev::Key::MetaLeft,
rdev::Key::MetaLeft => rdev::Key::ControlLeft,
rdev::Key::ControlRight => rdev::Key::MetaLeft,
rdev::Key::MetaRight => rdev::Key::ControlLeft,
_ => key,
};
rdev::macos_keycode_from_key(key).unwrap_or_default()
}
_ => {
let key = rdev::linux_key_from_code(code);
let key = match key {
rdev::Key::ControlLeft => rdev::Key::MetaLeft,
rdev::Key::MetaLeft => rdev::Key::ControlLeft,
rdev::Key::ControlRight => rdev::Key::MetaLeft,
rdev::Key::MetaRight => rdev::Key::ControlLeft,
_ => key,
};
rdev::linux_keycode_from_key(key).unwrap_or_default()
}
};
msg.set_chr(key);
}
}
}
pub fn send_key_event(&self, evt: &KeyEvent) {
// mode: legacy(0), map(1), translate(2), auto(3)
let mut msg = evt.clone();
self.swab_modifier_key(&mut msg);
let mut msg_out = Message::new();
msg_out.set_key_event(evt.clone());
msg_out.set_key_event(msg);
self.send(Data::Message(msg_out));
}
@ -934,6 +1011,23 @@ impl<T: InvokeUiSession> Interface for Session<T> {
handle_test_delay(t, peer).await;
}
}
fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) {
let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string());
if allow_swap_key {
msg.modifiers = msg.modifiers.iter().map(|ck| {
let ck = ck.enum_value_or_default();
let ck = match ck {
ControlKey::Control => ControlKey::Meta,
ControlKey::Meta => ControlKey::Control,
ControlKey::RControl => ControlKey::Meta,
ControlKey::RWin => ControlKey::Control,
_ => ck,
};
hbb_common::protobuf::EnumOrUnknown::new(ck)
}).collect();
};
}
}
impl<T: InvokeUiSession> Session<T> {

View File

@ -4,8 +4,8 @@
"dockerfile": "./Dockerfile",
"context": "."
},
"workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache",
"workspaceFolder": "/home/vscode/rustdesk",
"workspaceMount": "source=${localWorkspaceFolder}/../..,target=/home/vscode/rustdesk,type=bind,consistency=cache",
"workspaceFolder": "/home/vscode/rustdesk/vdi/host",
"customizations": {
"vscode": {
"extensions": [
@ -15,6 +15,7 @@
"tamasfe.even-better-toml",
"serayuzgur.crates",
"mhutchie.git-graph",
"formulahendry.terminal",
"eamodio.gitlens"
],
"settings": {

970
vdi/host/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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