mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-06-07 01:42:49 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
424a48a5c1
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,11 +1,12 @@
|
||||
name: 🐞 Bug report
|
||||
description: Thanks for taking the time to fill out this bug report! Please fill the form in **English**
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
description: A clear and concise description of what the bug is (if it's a keyboard issue, provide the keyboard mode you're using. e.g. legacy, map, translate)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
1
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -1,5 +1,6 @@
|
||||
name: 🛠️ Feature request
|
||||
description: Suggest an idea for RustDesk
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: desc
|
||||
|
19
.github/workflows/flutter-ci.yml
vendored
19
.github/workflows/flutter-ci.yml
vendored
@ -18,11 +18,12 @@ 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"
|
||||
VERSION: "1.2.0"
|
||||
NDK_VERSION: "r23"
|
||||
|
||||
jobs:
|
||||
build-for-windows:
|
||||
@ -62,7 +63,7 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
@ -260,7 +261,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-18.04,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
@ -330,13 +331,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:
|
||||
@ -354,7 +355,7 @@ jobs:
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r22b
|
||||
ndk-version: ${{ env.NDK_VERSION }}
|
||||
add-to-path: true
|
||||
|
||||
- name: Download deps
|
||||
@ -907,19 +908,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 }
|
||||
|
15
.github/workflows/flutter-nightly.yml
vendored
15
.github/workflows/flutter-nightly.yml
vendored
@ -14,6 +14,7 @@ env:
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
|
||||
VERSION: "1.2.0"
|
||||
NDK_VERSION: "r23"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
|
||||
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
|
||||
@ -86,7 +87,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
run: python3 .\build.py --portable --hwcodec --flutter
|
||||
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
|
||||
|
||||
- name: Sign rustdesk files
|
||||
uses: GermanBluefox/code-sign-action@v7
|
||||
@ -430,7 +431,7 @@ jobs:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-18.04,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "",
|
||||
}
|
||||
# - {
|
||||
@ -454,7 +455,7 @@ jobs:
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r22b
|
||||
ndk-version: ${{ env.NDK_VERSION }}
|
||||
add-to-path: true
|
||||
|
||||
- name: Download deps
|
||||
@ -732,7 +733,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 +901,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 +911,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
|
||||
|
||||
@ -1511,7 +1512,7 @@ jobs:
|
||||
pushd flatpak
|
||||
git clone https://github.com/flathub/shared-modules.git --depth=1
|
||||
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk
|
||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak com.rustdesk.RustDesk
|
||||
|
||||
- name: Publish flatpak package
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
1944
Cargo.lock
generated
1944
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@ -45,33 +45,33 @@ lazy_static = "1.4"
|
||||
sha2 = "0.10"
|
||||
repng = "0.2"
|
||||
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
|
||||
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
|
||||
runas = "0.2"
|
||||
runas = "1.0"
|
||||
magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" }
|
||||
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
|
||||
rubato = { version = "0.12", optional = true }
|
||||
samplerate = { version = "0.2", optional = true }
|
||||
async-trait = "0.1"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
clap = "3.0"
|
||||
clap = "4.1"
|
||||
rpassword = "7.0"
|
||||
base64 = "0.13"
|
||||
base64 = "0.21"
|
||||
num_cpus = "1.13"
|
||||
bytes = { version = "1.2", features = ["serde"] }
|
||||
default-net = "0.12.0"
|
||||
wol-rs = "0.9.1"
|
||||
wol-rs = "1.0"
|
||||
flutter_rust_bridge = { version = "1.61.1", optional = true }
|
||||
errno = "0.2.8"
|
||||
errno = "0.3"
|
||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
||||
url = { version = "2.1", features = ["serde"] }
|
||||
dlopen = "0.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
|
||||
chrono = "0.4.23"
|
||||
cidr-utils = "0.5.9"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
|
||||
cpal = "0.13.5"
|
||||
cpal = "0.14"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
machine-uid = "0.2"
|
||||
@ -81,14 +81,14 @@ sys-locale = "0.2"
|
||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
arboard = "2.0"
|
||||
arboard = "3.2"
|
||||
#minreq = { version = "2.4", features = ["punycode", "https-native"] }
|
||||
system_shutdown = "3.0.0"
|
||||
system_shutdown = "4.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
|
||||
winit = "0.26"
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt"] }
|
||||
winreg = "0.10"
|
||||
windows-service = "0.4"
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
@ -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."
|
||||
@ -147,6 +148,7 @@ cc = "1.0"
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
simple_rc = { path = "libs/simple_rc", optional = true }
|
||||
flutter_rust_bridge_codegen = "1.61.1"
|
||||
os-version = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
hound = "3.5"
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>]<br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> to your native language</b>
|
||||
</p>
|
||||
|
||||
@ -37,9 +37,9 @@ Below are the servers you are using for free, they may change over time. If you
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
|
189
build.py
189
build.py
@ -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:
|
||||
@ -40,7 +37,7 @@ def parse_rc_features(feature):
|
||||
'IddDriver': {
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5',
|
||||
'exclude': ['README.md'],
|
||||
'exclude': ['README.md', 'certmgr.exe', 'install_cert_runas_admin.bat'],
|
||||
},
|
||||
'PrivacyMode': {
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1'
|
||||
@ -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__":
|
||||
|
9
build.rs
9
build.rs
@ -9,7 +9,14 @@ fn build_windows() {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_mac() {
|
||||
let file = "src/platform/macos.mm";
|
||||
cc::Build::new().file(file).compile("macos");
|
||||
let mut b = cc::Build::new();
|
||||
if let Ok(os_version::OsVersion::MacOS(v)) = os_version::detect() {
|
||||
let v = v.version;
|
||||
if v.contains("10.14") {
|
||||
b.flag("-DNO_InputMonitoringAuthStatus=1");
|
||||
}
|
||||
}
|
||||
b.file(file).compile("macos");
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
}
|
||||
|
||||
|
50
docs/CONTRIBUTING-DE.md
Normal file
50
docs/CONTRIBUTING-DE.md
Normal 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
14
docs/DEVCONTAINER-DE.md
Normal 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
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b> لغتك الأم, <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> و <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>, README نحن بحاجة إلى مساعدتك لترجمة هذا </b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Struktura</a> •
|
||||
<a href="#snapshot">Ukázky</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#sådan-bygger-du-med-docker">Docker</a> •
|
||||
<a href="#filstruktur">Filstruktur</a> •
|
||||
<a href="#skærmbilleder">Skærmbilleder</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Vi har brug for din hjælp til at oversætte denne README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href=" https://github.com/rustdesk/doc.rustdesk.com">Dokument</a> til dit modersmål</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,21 +5,21 @@
|
||||
<a href="#auf-docker-kompilieren">Docker</a> •
|
||||
<a href="#dateistruktur">Dateistruktur</a> •
|
||||
<a href="#screenshots">Screenshots</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>]<br>
|
||||
<b>Wir brauchen deine Hilfe, um dieses README, die <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk-Benutzeroberfläche</a> und die <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentation</a> in deine Muttersprache zu übersetzen.</b>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Wir brauchen Ihre Hilfe, um dieses README, die <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk-Benutzeroberfläche</a> und die <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentation</a> in Ihre Muttersprache zu übersetzen.</b>
|
||||
</p>
|
||||
|
||||
Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Sie haben die volle Kontrolle über Ihre Daten und müssen sich keine Sorgen um die Sicherheit machen. Sie können unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
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. Schauen Sie sich [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn Sie Unterstützung beim Start brauchen.
|
||||
|
||||
[**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)
|
||||
|
||||
@ -31,21 +31,29 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`
|
||||
|
||||
## Freie öffentliche Server
|
||||
|
||||
Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
|
||||
Nachfolgend sind die Server gelistet, die Sie kostenlos nutzen können. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls Sie nicht in der Nähe einer dieser Server sind, kann es sein, dass Ihre Verbindung langsam sein wird.
|
||||
| Standort | Anbieter | Spezifikation |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Südkorea (Seoul) | AWS lightsail | 1 vCPU / 0,5 GB RAM |
|
||||
| Deutschland | Hetzner | 2 vCPU / 4 GB RAM |
|
||||
| Deutschland | Codext | 4 vCPU / 8 GB RAM |
|
||||
| Finnland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
|
||||
| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM |
|
||||
| Südkorea (Seoul) | [AWS lightsail](https://aws.amazon.com/de/) | 1 vCPU / 0,5 GB RAM |
|
||||
| Deutschland | [Hetzner](https://www.hetzner.com/de/) | 2 vCPU / 4 GB RAM |
|
||||
| Deutschland | [Codext](https://codext.de/) | 4 vCPU / 8 GB RAM |
|
||||
| Finnland (Helsinki) | [Netlock](https://netlockendpoint.com/de/index.html) | 4 vCPU / 8 GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com/de/index.html) | 4 vCPU / 8 GB RAM |
|
||||
| Ukraine (Kiew) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## Dev-Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Wenn Sie VS Code und Docker bereits installiert haben, können Sie auf das Abzeichen oben klicken, um loszulegen. Wenn Sie darauf klicken, 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 finden Sie 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.
|
||||
|
||||
Bitte lade die dynamische Bibliothek Sciter selbst herunter.
|
||||
Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
@ -53,14 +61,14 @@ Bitte lade die dynamische Bibliothek Sciter selbst herunter.
|
||||
|
||||
## Grobe Schritte zum Kompilieren
|
||||
|
||||
- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor
|
||||
- Bereiten Sie Ihre Rust-Entwicklungsumgebung und C++-Build-Umgebung vor
|
||||
|
||||
- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die Systemumgebungsvariable `VCPKG_ROOT` hinzu
|
||||
- Installieren Sie [vcpkg](https://github.com/microsoft/vcpkg) und fügen Sie die Systemumgebungsvariable `VCPKG_ROOT` hinzu
|
||||
|
||||
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
|
||||
- Linux/macOS: `vcpkg install libvpx libyuv opus`
|
||||
|
||||
- Nutze `cargo run`
|
||||
- Nutzen Sie `cargo run`
|
||||
|
||||
## [Erstellen](https://rustdesk.com/docs/de/dev/build/)
|
||||
|
||||
@ -159,7 +167,7 @@ method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=2
|
||||
|
||||
## Auf Docker kompilieren
|
||||
|
||||
Beginne damit, das Repository zu klonen und den Docker-Container zu bauen:
|
||||
Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@ -167,25 +175,25 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Führe jedes Mal, wenn du das Programm kompilieren musst, folgenden Befehl aus:
|
||||
Führen Sie jedes Mal, wenn Sie das Programm kompilieren müssen, folgenden Befehl aus:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Bedenke, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn du verschiedene Argumente für den Kompilierbefehl angeben musst, kannst du dies am Ende des Befehls an der Position `<OPTIONAL-ARGS>` tun. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm findest du im Zielordner auf deinem System. Du kannst es mit folgendem Befehl ausführen:
|
||||
Bedenken Sie, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn Sie verschiedene Argumente für den Kompilierbefehl angeben müssen, können Sie dies am Ende des Befehls an der Position `<OPTIONAL-ARGS>` tun. Wenn Sie zum Beispiel eine optimierte Releaseversion kompilieren wollen, können Sie `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm finden Sie im Zielordner auf Ihrem System. Sie können es mit folgendem Befehl ausführen:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Oder, wenn du eine Releaseversion benutzt:
|
||||
Oder, wenn Sie eine Releaseversion benutzen:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Bitte stelle sicher, dass du diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
|
||||
Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzen. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenken Sie auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf Ihrem eigentlichen System.
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#kiel-kompili-kun-docker">Docker</a> •
|
||||
<a href="#dosierstrukturo">Strukturo</a> •
|
||||
<a href="#ekrankopio">Ekrankopio</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Ni bezonas helpon traduki tiun README kaj <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">la interfacon</a> al via denaska lingvo</b>
|
||||
</p>
|
||||
|
||||
@ -27,8 +27,9 @@ Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependantaĵoj
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#como-compilar-con-docker">Docker</a> •
|
||||
<a href="#estructura-de-archivos">Estructura</a> •
|
||||
<a href="#capturas-de-pantalla">Capturas de pantalla</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Necesitamos tu ayuda para traducir este README a tu idioma</b>
|
||||
</p>
|
||||
|
||||
@ -34,8 +34,9 @@ A continuación se muestran los servidores gratuitos, pueden cambiar a medida qu
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependencias
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<a href="#ساخت">ساخت</a> •
|
||||
<a href="#سرورهای-عمومی-رایگان">سرور</a>
|
||||
</p>
|
||||
<p align="center" dir="auto">[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]</p>
|
||||
<p align="center" dir="auto">[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]</p>
|
||||
<p dir="rtl" align="center"><b>برای ترجمه این سند (README)، <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang" dir="rtl">رابط کاربری RustDesk</a>، <a href="https://github.com/rustdesk/doc.rustdesk.com" dir="rtl">و مستندات آن</a> به زبان مادری شما به کمکتان نیازمندیم. </b></p>
|
||||
|
||||
با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Rakenne</a> •
|
||||
<a href="#snapshot">Tilannevedos</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b>
|
||||
</p>
|
||||
|
||||
@ -27,8 +27,9 @@ Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Riippuvuudet
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#comment-construire-avec-docker">Docker</a> -
|
||||
<a href="#structure-du-projet">Structure</a> -
|
||||
<a href="#images">Images</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>.
|
||||
</p>
|
||||
|
||||
|
219
docs/README-GR.md
Normal file
219
docs/README-GR.md
Normal file
@ -0,0 +1,219 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#Δωρεάν-δημόσιοι-διακομιστές">Διακομιστές</a> •
|
||||
<a href="#Γενικά-βήματα-ώστε-να-κάνετε-build">Build</a> •
|
||||
<a href="#Πως-να-κάνετε-build-στο-Docker">Docker</a> •
|
||||
<a href="#Δομή-φακέλων">Δομή</a> •
|
||||
<a href="#Στιγμιότυπα">Στιγμιότυπα</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>]<br>
|
||||
<b>Χρειαζόμαστε τη βοήθειά σας για να μεταφράσουμε αυτό το αρχείο README, το <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> και το <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> στη μητρική σας γλώσσα</b>
|
||||
</p>
|
||||
|
||||
Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Ένα λογισμικό απομακρυσμένης επιφάνειας εργασίας, γραμμένο σε γλώσσα Rust. Δεν χρειάζεται κάποια παραμετροποίηση, λειτουργεί αμέσως μετά την εγκατάσταση. Έχετε τον πλήρη έλεγχο των δεδομένων σας, χωρίς να ανησυχείτε για την ασφάλειά τους. Μπορείτε να χρησιμοποιήσετε τους προκαθορισμένους διακομιστές rendezvous/αναμετάδοσης, [να εγκαταστήσετε τον δικό σας διακομιστή](https://rustdesk.com/server), ή [να αναπτύξετε ένα δικό σας διακομιστή rendezvous/αναμετάδοσης](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
|
||||
|
||||
[**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**Κατεβάστε τα αρχεία**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Δωρεάν δημόσιοι διακομιστές
|
||||
|
||||
Παρακάτω είναι οι διακομιστές που χρησιμοποιούνται δωρεάν, ενδέχεται να αλλάξουν με την πάροδο του χρόνου. Εάν δεν είστε κοντά σε ένα από αυτούς, το δίκτυό σας ίσως να είναι αργό.
|
||||
| Περιοχή | Πάροχος | Προδιαγραφές |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Σεούλ | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Γερμανία | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Γερμανία | Codext | 4 vCPU / 8GB RAM |
|
||||
| Φινλανδία (Ελσίνκι) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| ΗΠΑ (Άσμπερν) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Αν έχετε εγκατεστημένα το VS Code και το Docker, μπορείτε να ξεκινήσετε κάνοντας κλικ στην παραπάνω εικόνα. Αυτό θα έχει ως αποτέλεσμα, το VS Code να εγκαταστήσει αυτόματα την επέκταση Dev Containers, εάν χρειάζεται, θα κλωνοποιήσει τον πηγαίο κώδικα σε έναν νέο container και θα εκκινήσει ένα Dev Container για χρήση προγραμματισμού.
|
||||
|
||||
Για περισσότερες πληροφορίες μεταβείτε στο [DEVCONTAINER.md](docs/DEVCONTAINER.md).
|
||||
|
||||
## Προαπαιτούμενα για build
|
||||
|
||||
Στις παραθυρικές εκδόσεις χρησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παρακάτω βήματα είναι μόνο για το Sciter.
|
||||
|
||||
Παρακαλώ κατεβάστε μόνοι σας την δυναμική βιβλιοθήκη sciter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Γενικά βήματα ώστε να κάνετε build
|
||||
|
||||
- Προετοιμάστε τα περιβάλλοντα προγραμματισμού Rust και C++
|
||||
|
||||
- Εγκαταστήσετε το [vcpkg](https://github.com/microsoft/vcpkg), και ρυθμίστε σωστά την παράμετρο συστήματος `VCPKG_ROOT`
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- Εκτελέστε `cargo run`
|
||||
|
||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Πως να το κάνετε build στο Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
```
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Εγκατάσταση vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Διόρθωση libvpx (για Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Αλλαγή του Wayland σε X11 (Xorg)
|
||||
|
||||
Το RustDesk δεν υποστηρίζει το πρωτόκολλο Wayland. Διαβάστε [εδώ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) ώστε να ορίσετε το Xorg ως το προκαθορισμένο GNOME περιβάλλον.
|
||||
|
||||
## Υποστήριξη Wayland
|
||||
|
||||
Το Wayland προς το παρόν δεν διαθέτει κάποιο API το οποίο να στέλνει τα πατήματα πλήκτρων στα υπόλοιπα παράθυρα. Για τον λόγο αυτό, το Rustdesk χρησιμοποιεί ένα API από κατώτερο επίπεδο, όπως το `/dev/uinput` (Linux kernel level).
|
||||
|
||||
Σε περίπτωση που το Wayland είναι η ελεγχόμενη πλευρά, θα πρέπει να ξεκινήσετε με τον παρακάτω τρόπο:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Σημείωση**: Η εγγραφή οθόνης του Wayland χρησιμοποιεί διαφορετικές διεπαφές. Το RustDesk προς το παρόν υποστηρίζει μόνο org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Πως να κάνετε build στο Docker
|
||||
|
||||
Ξεκινήστε κλωνοποιώντας το αποθετήριο και κάνοντας build το docker container:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Στη συνέχεια, κάθε φορά που επιθυμείτε να κάνετε build την εφαρμογή, εκτελέστε την ακόλουθη εντολή:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Σημειώστε ότι το πρώτο build μπορεί να διαρκέσει περισσότερο, ώστε να αποθηκευτούν στην προσωρινή μνήμη οι εξαρτήσεις, τα επόμενα build θα είναι ταχύτερα. Επιπλέον, εάν πρέπει να καθορίσετε διαφορετικές παραμέτρους στην εντολή build, μπορείτε να το κάνετε στο τέλος της εντολής με την χρήση `<OPTIONAL-ARGS>`. Για παράδειγμα, εάν επιθυμείτε να δημιουργήσετε μια βελτιστοποιημένη έκδοση της εφαρμογής, θα εκτελέσετε την παραπάνω εντολή ακολουθούμενη από το `--release`. Το εκτελέσιμο αρχείο θα είναι διαθέσιμο στον προκαθορισμένο φάκελο στο σύστημά σας και μπορεί να εκτελεστεί με:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Ή στην περίπτωση μιας βελτιστοποιημένης έκδοσης της εφαρμογής εκτελέστε:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αρχική διαδρομή του αποθετηρίου του Rustdesk, διαφορετικά η εφαρμογή ενδέχεται να μην είναι σε θέση να βρεί τους απαιτούμενους πόρους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηρίζονται επί του παρόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το πρόγραμμα εντός του container αντί του κεντρικού υπολογιστή.
|
||||
|
||||
## Δομή φακέλων
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
|
||||
|
||||
## Στιγμιότυπα
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
@ -5,7 +5,7 @@
|
||||
<a href="#hogyan-éptís-dockerrel">Docker</a> •
|
||||
<a href="#fájl-struktúra">Struktúra</a> •
|
||||
<a href="#képernyőképek">Képernyőképek</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Kell a segítséged, hogy lefordítsuk ezt a README-t, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">a RustDesk UI-t</a> és a <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentációt</a> az anyanyelvedre</b>
|
||||
</p>
|
||||
|
||||
@ -35,8 +35,9 @@ Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingy
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke bahasa asli anda</b>
|
||||
</p>
|
||||
|
||||
@ -27,8 +27,9 @@ Di bawah ini adalah server yang bisa Anda gunakan secara gratis, dapat berubah s
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#come-compilare-con-docker">Docker</a> •
|
||||
<a href="#struttura-dei-file">Struttura</a> •
|
||||
<a href="#screenshots">Screenshots</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Abbiamo bisogno del tuo aiuto per tradurre questo README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> nella tua lingua nativa</b>
|
||||
</p>
|
||||
|
||||
@ -27,8 +27,9 @@ Qui sotto trovate i server che possono essere usati gratuitamente, la lista potr
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dipendenze
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#jak-kompilować-za-pomocą-dockera">Docker</a> •
|
||||
<a href="#struktura-plików">Struktura</a> •
|
||||
<a href="#migawkisnapshoty">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b>
|
||||
</p>
|
||||
|
||||
@ -27,8 +27,9 @@ Poniżej znajdują się serwery, z których można korzystać za darmo, może si
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Zależności
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#como-compilar-com-docker">Docker</a> •
|
||||
<a href="#estrutura-de-arquivos">Estrutura</a> •
|
||||
<a href="#screenshots">Screenshots</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Нам нужна ваша помощь для перевода этого README и <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на ваш родной язык</B>
|
||||
</p>
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваш віддалений робочий стіл"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
|
||||
<a href="#первинні-кроки-для-складання">Складання</a> •
|
||||
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
||||
<a href="#структура-файлів">Структура</a> •
|
||||
<a href="#знімки">Знімки</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README і <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на вашу рідну мову</B>
|
||||
</p>
|
||||
|
||||
@ -19,24 +19,37 @@
|
||||
|
||||
RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) для допомоги на початку роботи.
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**Як працює RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
|
||||
[**ЗАВАНТАЖИТИ ДОДАТОК**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**ЗАВАНТАЖИТИ ЗАСТОСУНОК**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Безкоштовні загальнодоступні сервери
|
||||
|
||||
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
|
||||
| Місцезнаходження | Постачальник | Технічні характеристики |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Південна Корея (Сеул) | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Сінгапур | Vultr | 1 vCPU / 1GB RAM |
|
||||
| Даллас | Vultr | 1 vCPU / 1GB RAM
|
||||
Німеччина | Hetzner | 2 vCPU / 4GB RAM | 2 VCPU / 4GB RAM | Німеччина | Hetzner | 2 VCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
|
||||
| США (Даллас) | Vultr | 1 vCPU / 1GB RAM
|
||||
| Німеччина | Hetzner | 2 VCPU / 4GB RAM |
|
||||
| Німеччина | Codext | 4 vCPU / 8GB RAM |
|
||||
| Фінляндія (Гельсінкі) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| США (Ешберн) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Якщо у вас уже встановлено VS Code і Docker, ви можете натиснути значок вище, щоб почати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує виcхідний код у том контейнера та розгорне контейнер dev для використання.
|
||||
|
||||
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інфо.
|
||||
|
||||
## Залежності
|
||||
|
||||
@ -64,9 +77,16 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
```
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
@ -91,30 +111,6 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
````sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Встановлення vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ...
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Виправлення libvpx (для Fedora)
|
||||
|
||||
```sh
|
||||
@ -183,8 +179,10 @@ target/release/rustdesk
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: зв'яжіться з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дочекайтеся віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту
|
||||
|
||||
## Знімки
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Cấu trúc tệp tin</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
|
||||
</p>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a href="#使用Docker编译">Docker</a> •
|
||||
<a href="#文件结构">结构</a> •
|
||||
<a href="#截图">截图</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
</p>
|
||||
|
||||
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
@ -1,9 +1,10 @@
|
||||
{
|
||||
"app-id": "org.rustdesk.rustdesk",
|
||||
"id": "com.rustdesk.RustDesk",
|
||||
"runtime": "org.freedesktop.Platform",
|
||||
"runtime-version": "21.08",
|
||||
"sdk": "org.freedesktop.Sdk",
|
||||
"command": "rustdesk",
|
||||
"icon": "share/rustdesk/files/rustdesk.png",
|
||||
"modules": [
|
||||
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
||||
"xdotool.json",
|
||||
@ -13,13 +14,22 @@
|
||||
"build-commands": [
|
||||
"bsdtar -zxvf rustdesk-1.2.0.deb",
|
||||
"tar -xvf ./data.tar.xz",
|
||||
"cp -r ./usr /app/",
|
||||
"mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/rustdesk /app/bin/rustdesk"
|
||||
"cp -r ./usr/* /app/",
|
||||
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
||||
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
|
||||
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
|
||||
],
|
||||
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../rustdesk-1.2.0.deb"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../res/logo.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -35,4 +45,4 @@
|
||||
"--socket=pulseaudio",
|
||||
"--talk-name=org.freedesktop.Flatpak"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -11,21 +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>
|
||||
|
||||
@ -52,8 +56,6 @@
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -61,6 +63,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PermissionRequestTransparentActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/Transparent" />
|
||||
|
||||
<service
|
||||
android:name=".MainService"
|
||||
android:enabled="true"
|
||||
@ -74,4 +81,4 @@
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
)
|
||||
@ -599,7 +623,7 @@ class MainService : Service() {
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentTitle(DEFAULT_NOTIFY_TITLE)
|
||||
.setContentText(translate(DEFAULT_NOTIFY_TEXT) + '!')
|
||||
.setContentText(translate(DEFAULT_NOTIFY_TEXT))
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||
@ -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)
|
||||
@ -665,7 +689,7 @@ class MainService : Service() {
|
||||
|
||||
private fun setTextNotification(_title: String?, _text: String?) {
|
||||
val title = _title ?: DEFAULT_NOTIFY_TITLE
|
||||
val text = _text ?: translate(DEFAULT_NOTIFY_TEXT) + '!'
|
||||
val text = _text ?: translate(DEFAULT_NOTIFY_TEXT)
|
||||
val notification = notificationBuilder
|
||||
.clearActions()
|
||||
.setStyle(null)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
2
flutter/assets/transfer.svg
Normal file
2
flutter/assets/transfer.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="298.171 98.799 32 32" width="32pt" height="32pt"><g><path d=" M 298.171 98.799 L 330.171 98.799 L 330.171 130.799 L 298.171 130.799 L 298.171 98.799 Z " fill="none"/><path d=" M 310.278 119.949 L 321.825 119.949 C 322.772 119.949 323.542 119.18 323.542 118.233 L 323.542 118.233 C 323.542 117.285 322.772 116.516 321.825 116.516 L 307.25 116.516 C 304.69 116.516 304.031 118.035 305.779 119.905 L 311.537 126.064 C 312.207 126.734 313.295 126.734 313.965 126.064 L 313.965 126.064 C 314.635 125.394 314.635 124.306 313.965 123.636 L 310.278 119.949 Z M 318.065 109.649 L 306.518 109.649 C 305.57 109.649 304.801 110.419 304.801 111.366 L 304.801 111.366 C 304.801 112.313 305.57 113.083 306.518 113.083 L 321.092 113.083 C 323.653 113.083 324.312 111.564 322.563 109.693 L 316.806 103.535 C 316.136 102.865 315.048 102.865 314.378 103.535 L 314.378 103.535 C 313.708 104.205 313.708 105.293 314.378 105.963 L 318.065 109.649 Z " fill-rule="evenodd" fill="rgb(0,0,0)"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -109,29 +109,41 @@ class IconFont {
|
||||
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
const ColorThemeExtension({
|
||||
required this.border,
|
||||
required this.border2,
|
||||
required this.highlight,
|
||||
required this.drag_indicator,
|
||||
});
|
||||
|
||||
final Color? border;
|
||||
final Color? border2;
|
||||
final Color? highlight;
|
||||
final Color? drag_indicator;
|
||||
|
||||
static const light = ColorThemeExtension(
|
||||
static final light = ColorThemeExtension(
|
||||
border: Color(0xFFCCCCCC),
|
||||
border2: Color(0xFFBBBBBB),
|
||||
highlight: Color(0xFFE5E5E5),
|
||||
drag_indicator: Colors.grey[800],
|
||||
);
|
||||
|
||||
static const dark = ColorThemeExtension(
|
||||
static final dark = ColorThemeExtension(
|
||||
border: Color(0xFF555555),
|
||||
border2: Color(0xFFE5E5E5),
|
||||
highlight: Color(0xFF3F3F3F),
|
||||
drag_indicator: Colors.grey,
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ColorThemeExtension> copyWith(
|
||||
{Color? border, Color? highlight}) {
|
||||
{Color? border,
|
||||
Color? border2,
|
||||
Color? highlight,
|
||||
Color? drag_indicator}) {
|
||||
return ColorThemeExtension(
|
||||
border: border ?? this.border,
|
||||
highlight: highlight ?? this.highlight,
|
||||
);
|
||||
border: border ?? this.border,
|
||||
border2: border2 ?? this.border2,
|
||||
highlight: highlight ?? this.highlight,
|
||||
drag_indicator: drag_indicator ?? this.drag_indicator);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -142,7 +154,9 @@ 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),
|
||||
drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -150,8 +164,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
class MyTheme {
|
||||
MyTheme._();
|
||||
|
||||
static const Color grayBg = Color(0xFFEEEEEE);
|
||||
static const Color white = Color(0xFFFFFFFF);
|
||||
static const Color grayBg = Color(0xFFEFEFF2);
|
||||
static const Color accent = Color(0xFF0071FF);
|
||||
static const Color accent50 = Color(0x770071FF);
|
||||
static const Color accent80 = Color(0xAA0071FF);
|
||||
@ -167,7 +180,28 @@ class MyTheme {
|
||||
static ThemeData lightTheme = ThemeData(
|
||||
brightness: Brightness.light,
|
||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||
scaffoldBackgroundColor: Color(0xFFFFFFFF),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
dialogBackgroundColor: Colors.white,
|
||||
dialogTheme: DialogTheme(
|
||||
elevation: 15,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: grayBg,
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
fillColor: grayBg,
|
||||
filled: true,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.all(15),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
|
||||
titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
|
||||
@ -175,7 +209,7 @@ class MyTheme {
|
||||
bodyMedium:
|
||||
TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
|
||||
labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
|
||||
cardColor: Color(0xFFEEEEEE),
|
||||
cardColor: grayBg,
|
||||
hintColor: Color(0xFFAAAAAA),
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
tabBarTheme: const TabBarTheme(
|
||||
@ -186,13 +220,51 @@ class MyTheme {
|
||||
splashFactory: isDesktop ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: isDesktop
|
||||
? TextButtonThemeData(
|
||||
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith(
|
||||
brightness: Brightness.light,
|
||||
background: Color(0xFFEEEEEE),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MyTheme.accent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: grayBg,
|
||||
foregroundColor: Colors.black87,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkboxTheme: const CheckboxThemeData(
|
||||
splashRadius: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
menuBarTheme: MenuBarThemeData(
|
||||
style:
|
||||
MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))),
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Colors.blue, secondary: accent, background: grayBg),
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.light,
|
||||
@ -203,6 +275,27 @@ class MyTheme {
|
||||
brightness: Brightness.dark,
|
||||
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
||||
scaffoldBackgroundColor: Color(0xFF18191E),
|
||||
dialogBackgroundColor: Color(0xFF18191E),
|
||||
dialogTheme: DialogTheme(
|
||||
elevation: 15,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: Color(0xFF24252B),
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
fillColor: Color(0xFF24252B),
|
||||
filled: true,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.all(15),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
titleLarge: TextStyle(fontSize: 19),
|
||||
titleSmall: TextStyle(fontSize: 14),
|
||||
@ -215,23 +308,69 @@ 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,
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style:
|
||||
OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))),
|
||||
textButtonTheme: isDesktop
|
||||
? TextButtonThemeData(
|
||||
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
disabledForegroundColor: Colors.white70,
|
||||
foregroundColor: Colors.white70,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
checkboxTheme:
|
||||
const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.blue,
|
||||
).copyWith(background: Color(0xFF24252B)),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MyTheme.accent,
|
||||
foregroundColor: Colors.white,
|
||||
disabledForegroundColor: Colors.white70,
|
||||
disabledBackgroundColor: Colors.white10,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkboxTheme: const CheckboxThemeData(
|
||||
splashRadius: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
menuBarTheme: MenuBarThemeData(
|
||||
style: MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))),
|
||||
colorScheme: ColorScheme.dark(
|
||||
primary: Colors.blue,
|
||||
secondary: accent,
|
||||
background: Color(0xFF24252B),
|
||||
),
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.dark,
|
||||
@ -245,7 +384,7 @@ class MyTheme {
|
||||
|
||||
static void changeDarkMode(ThemeMode mode) async {
|
||||
Get.changeThemeMode(mode);
|
||||
if (desktopType == DesktopType.main) {
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||
if (mode == ThemeMode.system) {
|
||||
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
|
||||
} else {
|
||||
@ -307,7 +446,7 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom(
|
||||
);
|
||||
|
||||
List<Locale> supportedLocales = const [
|
||||
// specify CN/TW to fix CJK issue in flutter
|
||||
Locale('en', 'US'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
Locale('zh', 'SG'),
|
||||
@ -329,7 +468,7 @@ List<Locale> supportedLocales = const [
|
||||
Locale('vi'),
|
||||
Locale('pl'),
|
||||
Locale('kz'),
|
||||
Locale('en', 'US'),
|
||||
Locale('es'),
|
||||
];
|
||||
|
||||
String formatDurationToTime(Duration duration) {
|
||||
@ -456,7 +595,7 @@ class OverlayDialogManager {
|
||||
BackButtonInterceptor.removeByName(dialogTag);
|
||||
}
|
||||
|
||||
dialog.entry = OverlayEntry(builder: (_) {
|
||||
dialog.entry = OverlayEntry(builder: (context) {
|
||||
bool innerClicked = false;
|
||||
return Listener(
|
||||
onPointerUp: (_) {
|
||||
@ -466,7 +605,9 @@ class OverlayDialogManager {
|
||||
innerClicked = false;
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.black12,
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black12
|
||||
: Colors.black45,
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
return Listener(
|
||||
onPointerUp: (_) => innerClicked = true,
|
||||
@ -648,7 +789,7 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!scopeNode.hasFocus) scopeNode.requestFocus();
|
||||
});
|
||||
const double padding = 16;
|
||||
const double padding = 30;
|
||||
bool tabTapped = false;
|
||||
return FocusScope(
|
||||
node: scopeNode,
|
||||
@ -677,18 +818,19 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
scrollable: true,
|
||||
title: title,
|
||||
titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0),
|
||||
contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25,
|
||||
contentPadding ?? padding, actions is List ? 10 : padding),
|
||||
contentPadding: EdgeInsets.fromLTRB(
|
||||
contentPadding ?? padding,
|
||||
25,
|
||||
contentPadding ?? padding,
|
||||
actions is List ? 10 : padding,
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: contentBoxConstraints,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
isDense: true, contentPadding: EdgeInsets.all(15))),
|
||||
child: content),
|
||||
child: content,
|
||||
),
|
||||
actions: actions,
|
||||
actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -820,7 +962,6 @@ Widget msgboxContent(String type, String title, String text) {
|
||||
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
||||
Widget content, List<Widget> buttons,
|
||||
{bool hasCancel = true}) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(
|
||||
translate(title),
|
||||
@ -903,21 +1044,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;
|
||||
}
|
||||
@ -926,31 +1060,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);
|
||||
@ -1453,10 +1589,12 @@ connectMainDesktop(String id,
|
||||
connect(BuildContext context, String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false}) async {
|
||||
bool isRDP = false}) async {
|
||||
if (id == '') return;
|
||||
id = id.replaceAll(' ', '');
|
||||
final oldId = id;
|
||||
id = await bind.mainHandleRelayId(id: id);
|
||||
final forceRelay = id != oldId;
|
||||
assert(!(isFileTransfer && isTcpTunneling && isRDP),
|
||||
"more than one connect type");
|
||||
|
||||
@ -1478,8 +1616,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;
|
||||
}
|
||||
}
|
||||
@ -1706,28 +1844,43 @@ class ServerConfig {
|
||||
Widget dialogButton(String text,
|
||||
{required VoidCallback? onPressed,
|
||||
bool isOutline = false,
|
||||
Widget? icon,
|
||||
TextStyle? style,
|
||||
ButtonStyle? buttonStyle}) {
|
||||
if (isDesktop) {
|
||||
if (isOutline) {
|
||||
return OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(translate(text), style: style),
|
||||
);
|
||||
return icon == null
|
||||
? OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(translate(text), style: style),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
label: Text(translate(text), style: style),
|
||||
);
|
||||
} else {
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
|
||||
onPressed: onPressed,
|
||||
child: Text(translate(text), style: style),
|
||||
);
|
||||
return icon == null
|
||||
? ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
|
||||
onPressed: onPressed,
|
||||
child: Text(translate(text), style: style),
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
icon: icon,
|
||||
style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
|
||||
onPressed: onPressed,
|
||||
label: Text(translate(text), style: style),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(
|
||||
translate(text),
|
||||
style: style,
|
||||
));
|
||||
onPressed: onPressed,
|
||||
child: Text(
|
||||
translate(text),
|
||||
style: style,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ class PrivacyModeState {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +35,8 @@ class BlockInputState {
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +58,8 @@ class CurrentDisplayState {
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +129,8 @@ class ShowRemoteCursorState {
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +153,8 @@ class KeyboardEnabledState {
|
||||
// Server side, default true
|
||||
final RxBool state = true.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,9 +174,10 @@ class RemoteCursorMovedState {
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
// Server side, default true
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,9 +197,10 @@ class RemoteCountState {
|
||||
static void init() {
|
||||
final key = tag();
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
// Server side, default true
|
||||
final RxInt state = 1.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,6 +222,8 @@ class PeerBoolOption {
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool value = RxBool(init_getter());
|
||||
Get.put(value, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = init_getter();
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,6 +246,8 @@ class PeerStringOption {
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxString value = RxString(init_getter());
|
||||
Get.put(value, tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = init_getter();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ class ChatPage extends StatelessWidget implements PageShape {
|
||||
? InputDecoration(
|
||||
isDense: true,
|
||||
hintText:
|
||||
"${translate('Write a message')}...",
|
||||
"${translate('Write a message')}",
|
||||
filled: true,
|
||||
fillColor:
|
||||
Theme.of(context).colorScheme.background,
|
||||
@ -88,7 +88,7 @@ class ChatPage extends StatelessWidget implements PageShape {
|
||||
)
|
||||
: defaultInputDecoration(
|
||||
hintText:
|
||||
"${translate('Write a message')}...",
|
||||
"${translate('Write a message')}",
|
||||
fillColor:
|
||||
Theme.of(context).colorScheme.background),
|
||||
sendButtonBuilder: defaultSendButton(
|
||||
|
@ -63,8 +63,9 @@ void changeIdDialog() {
|
||||
final Iterable violations = rules.where((r) => !r.validate(newId));
|
||||
if (violations.isNotEmpty) {
|
||||
setState(() {
|
||||
msg =
|
||||
'${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
|
||||
msg = isDesktop
|
||||
? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'
|
||||
: violations.map((r) => r.name).join(', ');
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -87,7 +88,9 @@ void changeIdDialog() {
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
msg = '${translate('Prompt')}: ${translate(status)}';
|
||||
msg = isDesktop
|
||||
? '${translate('Prompt')}: ${translate(status)}'
|
||||
: translate(status);
|
||||
});
|
||||
}
|
||||
|
||||
@ -103,7 +106,7 @@ void changeIdDialog() {
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Your new ID'),
|
||||
border: const OutlineInputBorder(),
|
||||
border: isDesktop ? const OutlineInputBorder() : null,
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
suffixText: '${rxId.value.length}/16',
|
||||
suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
@ -123,27 +126,26 @@ void changeIdDialog() {
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Obx(() => Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 4,
|
||||
children: rules.map((e) {
|
||||
var checked = e.validate(rxId.value);
|
||||
return Chip(
|
||||
label: Text(
|
||||
e.name,
|
||||
style: TextStyle(
|
||||
color: checked
|
||||
? const Color(0xFF0A9471)
|
||||
: Color.fromARGB(255, 198, 86, 157)),
|
||||
),
|
||||
backgroundColor: checked
|
||||
? const Color(0xFFD0F7ED)
|
||||
: Color.fromARGB(255, 247, 205, 232));
|
||||
}).toList(),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
isDesktop
|
||||
? Obx(() => Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 4,
|
||||
children: rules.map((e) {
|
||||
var checked = e.validate(rxId.value);
|
||||
return Chip(
|
||||
label: Text(
|
||||
e.name,
|
||||
style: TextStyle(
|
||||
color: checked
|
||||
? const Color(0xFF0A9471)
|
||||
: Color.fromARGB(255, 198, 86, 157)),
|
||||
),
|
||||
backgroundColor: checked
|
||||
? const Color(0xFFD0F7ED)
|
||||
: Color.fromARGB(255, 247, 205, 232));
|
||||
}).toList(),
|
||||
)).marginOnly(bottom: 8)
|
||||
: SizedBox.shrink(),
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
@ -180,7 +182,7 @@ void changeWhiteList({Function()? callback}) async {
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
border: isDesktop ? const OutlineInputBorder() : null,
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
|
@ -331,7 +331,7 @@ class QualityMonitor extends StatelessWidget {
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: AutoSizeText(info,
|
||||
style: TextStyle(color: MyTheme.darkGray),
|
||||
style: TextStyle(color: Color.fromARGB(255, 210, 210, 210)),
|
||||
textAlign: TextAlign.right,
|
||||
maxLines: 1)),
|
||||
Spacer(flex: 1),
|
||||
@ -353,7 +353,7 @@ class QualityMonitor extends StatelessWidget {
|
||||
? Container(
|
||||
constraints: BoxConstraints(maxWidth: 200),
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: MyTheme.canvasColor.withAlpha(120),
|
||||
color: MyTheme.canvasColor.withAlpha(150),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -42,6 +42,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
var _menuPos = RelativeRect.fill;
|
||||
final double _cardRadius = 16;
|
||||
final double _tileRadius = 5;
|
||||
final double _borderWidth = 2;
|
||||
|
||||
@override
|
||||
@ -116,27 +117,32 @@ class _PeerCardState extends State<_PeerCard>
|
||||
|
||||
Widget _buildDesktop() {
|
||||
final peer = super.widget.peer;
|
||||
var deco = Rx<BoxDecoration?>(BoxDecoration(
|
||||
var deco = Rx<BoxDecoration?>(
|
||||
BoxDecoration(
|
||||
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
||||
borderRadius: peerCardUiType.value == PeerUiType.grid
|
||||
? BorderRadius.circular(_cardRadius)
|
||||
: null));
|
||||
borderRadius: BorderRadius.circular(
|
||||
peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
|
||||
),
|
||||
),
|
||||
);
|
||||
return MouseRegion(
|
||||
onEnter: (evt) {
|
||||
deco.value = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: _borderWidth),
|
||||
borderRadius: peerCardUiType.value == PeerUiType.grid
|
||||
? BorderRadius.circular(_cardRadius)
|
||||
: null);
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: _borderWidth),
|
||||
borderRadius: BorderRadius.circular(
|
||||
peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
onExit: (evt) {
|
||||
deco.value = BoxDecoration(
|
||||
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
||||
borderRadius: peerCardUiType.value == PeerUiType.grid
|
||||
? BorderRadius.circular(_cardRadius)
|
||||
: null);
|
||||
border: Border.all(color: Colors.transparent, width: _borderWidth),
|
||||
borderRadius: BorderRadius.circular(
|
||||
peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () => widget.connect(context, peer.id),
|
||||
@ -163,6 +169,10 @@ class _PeerCardState extends State<_PeerCard>
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(_tileRadius),
|
||||
bottomLeft: Radius.circular(_tileRadius),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: 42,
|
||||
@ -171,7 +181,12 @@ class _PeerCardState extends State<_PeerCard>
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(_tileRadius),
|
||||
bottomRight: Radius.circular(_tileRadius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -532,19 +547,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
proc: () {
|
||||
() async {
|
||||
if (isLan) {
|
||||
bind.mainRemoveDiscovered(id: id);
|
||||
} else {
|
||||
final favs = (await bind.mainGetFav()).toList();
|
||||
if (favs.remove(id)) {
|
||||
await bind.mainStoreFav(favs: favs);
|
||||
}
|
||||
await bind.mainRemovePeer(id: id);
|
||||
}
|
||||
removePreference(id);
|
||||
await reloadFunc();
|
||||
}();
|
||||
_delete(id, isLan, reloadFunc);
|
||||
},
|
||||
padding: menuPadding,
|
||||
dismissOnClicked: true,
|
||||
@ -673,7 +676,13 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Rename')),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.edit_rounded, color: MyTheme.accent),
|
||||
Text(translate('Rename')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -682,9 +691,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: translate('Name')),
|
||||
decoration: InputDecoration(labelText: translate('Name')),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -694,8 +701,17 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: submit),
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
@ -705,6 +721,58 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
@protected
|
||||
void _update();
|
||||
|
||||
void _delete(String id, bool isLan, Function reloadFunc) async {
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close) {
|
||||
submit() async {
|
||||
if (isLan) {
|
||||
bind.mainRemoveDiscovered(id: id);
|
||||
} else {
|
||||
final favs = (await bind.mainGetFav()).toList();
|
||||
if (favs.remove(id)) {
|
||||
await bind.mainStoreFav(favs: favs);
|
||||
}
|
||||
await bind.mainRemovePeer(id: id);
|
||||
}
|
||||
removePreference(id);
|
||||
await reloadFunc();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
Text(translate('Delete')).paddingOnly(
|
||||
left: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox.shrink(),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecentPeerCard extends BasePeerCard {
|
||||
@ -837,13 +905,10 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
|
||||
final inRecent = await bind.mainIsInRecentPeers(id: peer.id);
|
||||
if (inRecent) {
|
||||
if (!favs.contains(peer.id)) {
|
||||
menuItems.add(_addFavAction(peer.id));
|
||||
} else {
|
||||
menuItems.add(_rmFavAction(peer.id, () async {}));
|
||||
}
|
||||
if (!favs.contains(peer.id)) {
|
||||
menuItems.add(_addFavAction(peer.id));
|
||||
} else {
|
||||
menuItems.add(_rmFavAction(peer.id, () async {}));
|
||||
}
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
@ -1065,7 +1130,7 @@ void _rdpDialog(String id) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text('RDP ${translate('Settings')}'),
|
||||
title: Text(translate('RDP Settings')),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
@ -1076,56 +1141,67 @@ void _rdpDialog(String id) async {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Port')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
isDesktop
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Port')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), hintText: '3389'),
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop ? null : translate('Port'),
|
||||
border: isDesktop ? const OutlineInputBorder() : null,
|
||||
hintText: '3389'),
|
||||
controller: portController,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 8),
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
isDesktop
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration:
|
||||
const InputDecoration(border: OutlineInputBorder()),
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop ? null : translate('Username'),
|
||||
border: isDesktop ? const OutlineInputBorder() : null),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 8),
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
isDesktop
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: isDesktop ? null : translate('Password'),
|
||||
border:
|
||||
isDesktop ? const OutlineInputBorder() : null,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
@ -1135,7 +1211,7 @@ void _rdpDialog(String id) async {
|
||||
)),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 8),
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
@ -39,6 +41,8 @@ EdgeInsets? _menuPadding() {
|
||||
|
||||
class _PeerTabPageState extends State<PeerTabPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _hideSort = bind.getLocalFlutterConfig(k: 'peer-tab-index') == '0';
|
||||
|
||||
final List<_TabEntry> entries = [
|
||||
_TabEntry(
|
||||
RecentPeersView(
|
||||
@ -83,6 +87,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
if (tabIndex < entries.length) {
|
||||
gFFI.peerTabModel.setCurrentTab(tabIndex);
|
||||
entries[tabIndex].load();
|
||||
_hideSort = tabIndex == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,22 +100,27 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
SizedBox(
|
||||
height: 28,
|
||||
child: Container(
|
||||
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
|
||||
constraints: isDesktop ? null : kMobilePageConstraints,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: visibleContextMenuListener(
|
||||
_createSwitchBar(context))),
|
||||
buildScrollJumper(),
|
||||
const PeerSearchBar(),
|
||||
Offstage(
|
||||
offstage: !isDesktop,
|
||||
child: _createPeerViewTypeSwitch(context)
|
||||
.marginOnly(left: 13)),
|
||||
],
|
||||
)),
|
||||
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
|
||||
constraints: isDesktop ? null : kMobilePageConstraints,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
visibleContextMenuListener(_createSwitchBar(context))),
|
||||
buildScrollJumper(),
|
||||
const PeerSearchBar(),
|
||||
Offstage(
|
||||
offstage: !isDesktop,
|
||||
child: _createPeerViewTypeSwitch(context)
|
||||
.marginOnly(left: 13)),
|
||||
Offstage(
|
||||
offstage: _hideSort,
|
||||
child: PeerSortDropdown().marginOnly(left: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_createPeersView(),
|
||||
],
|
||||
@ -158,7 +168,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
color: model.currentTab == t
|
||||
? Theme.of(context).colorScheme.background
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
@ -231,32 +241,32 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
|
||||
Widget _createPeerViewTypeSwitch(BuildContext context) {
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
final activeDeco =
|
||||
BoxDecoration(color: Theme.of(context).colorScheme.background);
|
||||
return Row(
|
||||
children: [PeerUiType.grid, PeerUiType.list]
|
||||
.map((type) => Obx(
|
||||
() => Container(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
decoration: peerCardUiType.value == type ? activeDeco : null,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: 'peer-card-ui-type', v: type.index.toString());
|
||||
peerCardUiType.value = type;
|
||||
},
|
||||
child: Icon(
|
||||
type == PeerUiType.grid
|
||||
? Icons.grid_view_rounded
|
||||
: Icons.list,
|
||||
size: 18,
|
||||
color:
|
||||
peerCardUiType.value == type ? textColor : textColor
|
||||
?..withOpacity(0.5),
|
||||
)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
final deco = BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
);
|
||||
final types = [PeerUiType.grid, PeerUiType.list];
|
||||
|
||||
return Obx(
|
||||
() => Container(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
decoration: deco,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final type = types.elementAt(
|
||||
peerCardUiType.value == types.elementAt(0) ? 1 : 0);
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: 'peer-card-ui-type', v: type.index.toString());
|
||||
peerCardUiType.value = type;
|
||||
},
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.list_rounded
|
||||
: Icons.grid_view_rounded,
|
||||
size: 18,
|
||||
color: textColor,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -417,3 +427,98 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PeerSortDropdown extends StatefulWidget {
|
||||
const PeerSortDropdown({super.key});
|
||||
|
||||
@override
|
||||
State<PeerSortDropdown> createState() => _PeerSortDropdownState();
|
||||
}
|
||||
|
||||
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
@override
|
||||
void initState() {
|
||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterConfig(
|
||||
k: "peer-sorting",
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final deco = BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
);
|
||||
|
||||
final translated_text =
|
||||
PeerSortType.values.map((e) => translate(e)).toList();
|
||||
|
||||
final double max_width =
|
||||
50 + translated_text.map((e) => e.length).reduce(max) * 10;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
decoration: deco,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
onChanged: (v) async {
|
||||
if (v != null) {
|
||||
setState(() => peerSort.value = v);
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: "peer-sorting",
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
},
|
||||
customButton: Icon(
|
||||
Icons.sort,
|
||||
size: 18,
|
||||
),
|
||||
isExpanded: true,
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
width: max_width,
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
translate("Sort by"),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
enabled: false,
|
||||
),
|
||||
...translated_text
|
||||
.map<DropdownMenuItem<String>>(
|
||||
(String value) => DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
value == peerSort.value
|
||||
? Icons.radio_button_checked_rounded
|
||||
: Icons.radio_button_off_rounded,
|
||||
size: 18,
|
||||
).paddingOnly(right: 12),
|
||||
Text(
|
||||
value,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
@ -16,8 +16,36 @@ import 'peer_card.dart';
|
||||
typedef PeerFilter = bool Function(Peer peer);
|
||||
typedef PeerCardBuilder = Widget Function(Peer peer);
|
||||
|
||||
class PeerSortType {
|
||||
static const String remoteId = 'Remote ID';
|
||||
static const String remoteHost = 'Remote Host';
|
||||
static const String username = 'Username';
|
||||
// static const String status = 'Status';
|
||||
|
||||
static List<String> values = [
|
||||
PeerSortType.remoteId,
|
||||
PeerSortType.remoteHost,
|
||||
PeerSortType.username,
|
||||
// PeerSortType.status
|
||||
];
|
||||
}
|
||||
|
||||
class LoadEvent {
|
||||
static const String recent = 'load_recent_peers';
|
||||
static const String favorite = 'load_fav_peers';
|
||||
static const String lan = 'load_lan_peers';
|
||||
static const String addressBook = 'load_address_book_peers';
|
||||
}
|
||||
|
||||
/// for peer search text, global obs value
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
/// for peer sort, global obs value
|
||||
final peerSort = bind.getLocalFlutterConfig(k: 'peer-sorting').obs;
|
||||
|
||||
// list for listener
|
||||
final obslist = [peerSearchText, peerSort].obs;
|
||||
|
||||
final peerSearchTextController =
|
||||
TextEditingController(text: peerSearchText.value);
|
||||
|
||||
@ -40,12 +68,18 @@ class _PeersView extends StatefulWidget {
|
||||
/// State for the peer widget.
|
||||
class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
static const int _maxQueryCount = 3;
|
||||
final HashMap<String, String> _emptyMessages = HashMap.from({
|
||||
LoadEvent.recent: 'empty_recent_tip',
|
||||
LoadEvent.favorite: 'empty_favorite_tip',
|
||||
LoadEvent.lan: 'empty_lan_tip',
|
||||
LoadEvent.addressBook: 'empty_address_book_tip',
|
||||
});
|
||||
final space = isDesktop ? 12.0 : 8.0;
|
||||
final _curPeers = <String>{};
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = <String>{};
|
||||
var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1));
|
||||
var _queryCoun = 0;
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
|
||||
late final mobileWidth = () {
|
||||
@ -78,12 +112,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
_queryCoun = 0;
|
||||
_queryCount = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
_queryCoun = _maxQueryCount;
|
||||
_queryCount = _maxQueryCount;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -91,17 +125,48 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => widget.peers,
|
||||
child: Consumer<Peers>(
|
||||
builder: (context, peers, child) => peers.peers.isEmpty
|
||||
? Container(
|
||||
margin: EdgeInsets.only(top: kEmptyMarginTop),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(translate("Empty")))
|
||||
: _buildPeersView(peers)),
|
||||
builder: (context, peers, child) => peers.peers.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.sentiment_very_dissatisfied_rounded,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
size: 40,
|
||||
).paddingOnly(bottom: 10),
|
||||
Text(
|
||||
translate(
|
||||
_emptyMessages[widget.peers.loadEvent] ?? 'Empty',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _buildPeersView(peers),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onVisibilityChanged(VisibilityInfo info) {
|
||||
final peerId = _peerId((info.key as ValueKey).value);
|
||||
if (info.visibleFraction > 0.00001) {
|
||||
_curPeers.add(peerId);
|
||||
} else {
|
||||
_curPeers.remove(peerId);
|
||||
}
|
||||
_lastChangeTime = DateTime.now();
|
||||
}
|
||||
|
||||
String _cardId(String id) => widget.peers.name + id;
|
||||
String _peerId(String cardId) => cardId.replaceAll(widget.peers.name, '');
|
||||
|
||||
Widget _buildPeersView(Peers peers) {
|
||||
final body = ObxValue<RxString>((searchText) {
|
||||
final body = ObxValue<RxList>((filters) {
|
||||
return FutureBuilder<List<Peer>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
@ -109,16 +174,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
final cards = <Widget>[];
|
||||
for (final peer in peers) {
|
||||
final visibilityChild = VisibilityDetector(
|
||||
key: ValueKey(peer.id),
|
||||
onVisibilityChanged: (info) {
|
||||
final peerId = (info.key as ValueKey).value;
|
||||
if (info.visibleFraction > 0.00001) {
|
||||
_curPeers.add(peerId);
|
||||
} else {
|
||||
_curPeers.remove(peerId);
|
||||
}
|
||||
_lastChangeTime = DateTime.now();
|
||||
},
|
||||
key: ValueKey(_cardId(peer.id)),
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
child: widget.peerCardBuilder(peer),
|
||||
);
|
||||
cards.add(isDesktop
|
||||
@ -139,9 +196,9 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
);
|
||||
}
|
||||
},
|
||||
future: matchPeers(searchText.value, peers.peers),
|
||||
future: matchPeers(filters[0].value, filters[1].value, peers.peers),
|
||||
);
|
||||
}, peerSearchText);
|
||||
}, obslist);
|
||||
|
||||
return body;
|
||||
}
|
||||
@ -149,6 +206,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
// ignore: todo
|
||||
// TODO: variables walk through async tasks?
|
||||
void _startCheckOnlines() {
|
||||
final queryInterval = const Duration(seconds: 20);
|
||||
() async {
|
||||
while (!_exit) {
|
||||
final now = DateTime.now();
|
||||
@ -158,18 +216,18 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
platformFFI.ffiBind
|
||||
.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
_lastQueryTime = DateTime.now();
|
||||
_queryCoun = 0;
|
||||
_lastQueryTime = DateTime.now().subtract(queryInterval);
|
||||
_queryCount = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_queryCoun < _maxQueryCount) {
|
||||
if (now.difference(_lastQueryTime) > const Duration(seconds: 20)) {
|
||||
if (_queryCount < _maxQueryCount) {
|
||||
if (now.difference(_lastQueryTime) >= queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
platformFFI.ffiBind
|
||||
.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryTime = DateTime.now();
|
||||
_queryCoun += 1;
|
||||
_queryCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,11 +237,40 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
}();
|
||||
}
|
||||
|
||||
Future<List<Peer>>? matchPeers(String searchText, List<Peer> peers) async {
|
||||
Future<List<Peer>>? matchPeers(
|
||||
String searchText, String sortedBy, List<Peer> peers) async {
|
||||
if (widget.peerFilter != null) {
|
||||
peers = peers.where((peer) => widget.peerFilter!(peer)).toList();
|
||||
}
|
||||
|
||||
// fallback to id sorting
|
||||
if (!PeerSortType.values.contains(sortedBy)) {
|
||||
sortedBy = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterConfig(
|
||||
k: "peer-sorting",
|
||||
v: sortedBy,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.peers.loadEvent != LoadEvent.recent) {
|
||||
switch (sortedBy) {
|
||||
case PeerSortType.remoteId:
|
||||
peers.sort((p1, p2) => p1.getId().compareTo(p2.getId()));
|
||||
break;
|
||||
case PeerSortType.remoteHost:
|
||||
peers.sort((p1, p2) =>
|
||||
p1.hostname.toLowerCase().compareTo(p2.hostname.toLowerCase()));
|
||||
break;
|
||||
case PeerSortType.username:
|
||||
peers.sort((p1, p2) =>
|
||||
p1.username.toLowerCase().compareTo(p2.username.toLowerCase()));
|
||||
break;
|
||||
// case PeerSortType.status:
|
||||
// peers.sort((p1, p2) => p1.online ? -1 : 1);
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
searchText = searchText.trim();
|
||||
if (searchText.isEmpty) {
|
||||
return peers;
|
||||
@ -197,6 +284,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
filteredList.add(peers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
}
|
||||
@ -232,7 +320,7 @@ class RecentPeersView extends BasePeersView {
|
||||
: super(
|
||||
key: key,
|
||||
name: 'recent peer',
|
||||
loadEvent: 'load_recent_peers',
|
||||
loadEvent: LoadEvent.recent,
|
||||
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
@ -254,7 +342,7 @@ class FavoritePeersView extends BasePeersView {
|
||||
: super(
|
||||
key: key,
|
||||
name: 'favorite peer',
|
||||
loadEvent: 'load_fav_peers',
|
||||
loadEvent: LoadEvent.favorite,
|
||||
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
@ -276,7 +364,7 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
: super(
|
||||
key: key,
|
||||
name: 'discovered peer',
|
||||
loadEvent: 'load_lan_peers',
|
||||
loadEvent: LoadEvent.lan,
|
||||
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
@ -301,7 +389,7 @@ class AddressBookPeersView extends BasePeersView {
|
||||
: super(
|
||||
key: key,
|
||||
name: 'address book peer',
|
||||
loadEvent: 'load_address_book_peers',
|
||||
loadEvent: LoadEvent.addressBook,
|
||||
peerFilter: (Peer peer) =>
|
||||
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
||||
|
@ -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;
|
||||
@ -13,7 +14,10 @@ const String kPeerPlatformAndroid = "Android";
|
||||
|
||||
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
|
||||
const String kAppTypeMain = "main";
|
||||
|
||||
/// [kAppTypeConnectionManager] only for 'Desktop CM Page'
|
||||
const String kAppTypeConnectionManager = "cm";
|
||||
|
||||
const String kAppTypeDesktopRemote = "remote";
|
||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||
const String kAppTypeDesktopPortForward = "port forward";
|
||||
@ -58,6 +62,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 +89,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 +140,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>{
|
||||
|
@ -151,10 +151,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false}) {
|
||||
var id = _idController.id;
|
||||
var forceRelay = id.endsWith(r'/r');
|
||||
if (forceRelay) id = id.substring(0, id.length - 2);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer, forceRelay: forceRelay);
|
||||
connect(context, id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
@ -164,9 +161,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
width: 320 + 20 * 2,
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.background)),
|
||||
child: Ink(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -203,6 +199,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
cursorColor:
|
||||
Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
counterText: '',
|
||||
hintText: _idInputFocused.value
|
||||
? null
|
||||
|
@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart' hide MenuItem;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/custom_password.dart';
|
||||
@ -14,7 +14,6 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -55,10 +54,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildLeftPane(context),
|
||||
const VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: buildRightPane(context),
|
||||
),
|
||||
@ -158,7 +154,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 20),
|
||||
contentPadding: EdgeInsets.only(top: 10, bottom: 10),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
@ -242,7 +238,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 2),
|
||||
contentPadding:
|
||||
EdgeInsets.only(top: 14, bottom: 10),
|
||||
),
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
@ -254,9 +251,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD), // TODO
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
).marginOnly(right: 8, bottom: 2),
|
||||
).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
onTap: () => bind.mainUpdateTemporaryPassword(),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
@ -265,11 +262,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD), // TODO
|
||||
color:
|
||||
editHover.value ? textColor : Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
).marginOnly(right: 8, bottom: 2),
|
||||
).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(1),
|
||||
onHover: (value) => editHover.value = value,
|
||||
|
@ -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;
|
||||
@ -120,7 +120,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
const VerticalDivider(thickness: 1, width: 1),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
@ -381,8 +381,13 @@ class _GeneralState extends State<_General> {
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
String? initialDirectory;
|
||||
if (await Directory.fromUri(Uri.directory(dir))
|
||||
.exists()) {
|
||||
initialDirectory = dir;
|
||||
}
|
||||
String? selectedDirectory = await FilePicker.platform
|
||||
.getDirectoryPath(initialDirectory: dir);
|
||||
.getDirectoryPath(initialDirectory: initialDirectory);
|
||||
if (selectedDirectory != null) {
|
||||
await bind.mainSetOption(
|
||||
key: 'video-save-directory',
|
||||
@ -538,6 +543,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 +673,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
return _Card(title: 'Password', children: [
|
||||
_ComboBox(
|
||||
enabled: !locked,
|
||||
keys: modeKeys,
|
||||
values: modeValues,
|
||||
initialKey: modeInitialKey,
|
||||
@ -1722,7 +1729,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 +1741,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 +1755,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,
|
||||
|
@ -75,7 +75,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
isClose: false,
|
||||
),
|
||||
)));
|
||||
return Platform.isMacOS
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => DragToResizeArea(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -98,7 +98,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
labelGetter: DesktopTab.labelGetterAlias,
|
||||
)),
|
||||
);
|
||||
return Platform.isMacOS
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
|
@ -1,7 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
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,10 +17,55 @@ 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;
|
||||
final RxBool driverCert = true.obs;
|
||||
final RxBool showProgress = false.obs;
|
||||
final RxBool btnEnabled = true.obs;
|
||||
|
||||
@ -46,15 +95,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,30 +144,66 @@ 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),
|
||||
Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: startmenu.value,
|
||||
onChanged: (b) {
|
||||
if (b != null) startmenu.value = b;
|
||||
})),
|
||||
Text(translate('Create start menu shortcuts'))
|
||||
],
|
||||
TextButton(
|
||||
onPressed: () => startmenu.value = !startmenu.value,
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: startmenu.value,
|
||||
onChanged: (b) {
|
||||
if (b != null) startmenu.value = b;
|
||||
})),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: translate('Create start menu shortcuts'),
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: desktopicon.value,
|
||||
onChanged: (b) {
|
||||
if (b != null) desktopicon.value = b;
|
||||
})),
|
||||
Text(translate('Create desktop icon'))
|
||||
],
|
||||
TextButton(
|
||||
onPressed: () => desktopicon.value = !desktopicon.value,
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: desktopicon.value,
|
||||
onChanged: (b) {
|
||||
if (b != null) desktopicon.value = b;
|
||||
})),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: translate('Create desktop icon'),
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !Platform.isWindows,
|
||||
child: TextButton(
|
||||
onPressed: () => driverCert.value = !driverCert.value,
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: driverCert.value,
|
||||
onChanged: (b) {
|
||||
if (b != null) driverCert.value = b;
|
||||
})),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: translate('idd_driver_tip'),
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => launchUrlString('http://rustdesk.com/privacy'),
|
||||
@ -127,8 +216,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 +231,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 +254,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)),
|
||||
),
|
||||
],
|
||||
@ -179,12 +265,47 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
}
|
||||
|
||||
void install() {
|
||||
btnEnabled.value = false;
|
||||
showProgress.value = true;
|
||||
String args = '';
|
||||
if (startmenu.value) args += ' startmenu';
|
||||
if (desktopicon.value) args += ' desktopicon';
|
||||
bind.installInstallMe(options: args, path: controller.text);
|
||||
do_install() {
|
||||
btnEnabled.value = false;
|
||||
showProgress.value = true;
|
||||
String args = '';
|
||||
if (startmenu.value) args += ' startmenu';
|
||||
if (desktopicon.value) args += ' desktopicon';
|
||||
if (driverCert.value) args += ' driverCert';
|
||||
bind.installInstallMe(options: args, path: controller.text);
|
||||
}
|
||||
|
||||
if (driverCert.isTrue) {
|
||||
final tag = 'install-info-install-cert-confirm';
|
||||
final btns = [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
onPressed: () => gFFI.dialogManager.dismissByTag(tag),
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
onPressed: () {
|
||||
gFFI.dialogManager.dismissByTag(tag);
|
||||
do_install();
|
||||
},
|
||||
isOutline: false,
|
||||
),
|
||||
];
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(
|
||||
child:
|
||||
msgboxContent('info', 'Warning', 'confirm_idd_driver_tip')),
|
||||
actions: btns,
|
||||
onCancel: close,
|
||||
),
|
||||
tag: tag,
|
||||
);
|
||||
} else {
|
||||
do_install();
|
||||
}
|
||||
}
|
||||
|
||||
void selectInstallPath() async {
|
||||
|
@ -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) {
|
||||
|
@ -308,6 +308,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void leaveView(PointerExitEvent evt) {
|
||||
if (_ffi.ffiModel.keyboard()) {
|
||||
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
||||
}
|
||||
|
||||
_cursorOverImage.value = false;
|
||||
_firstEnterImage.value = false;
|
||||
if (_onEnterOrLeaveImage4Menubar != null) {
|
||||
@ -329,8 +333,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
PointerExitEventListener? onExit,
|
||||
) {
|
||||
return RawPointerMouseRegion(
|
||||
onEnter: enterView,
|
||||
onExit: leaveView,
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
onPointerDown: (event) {
|
||||
// A double check for blur status.
|
||||
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
|
||||
|
@ -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,
|
||||
));
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -37,7 +37,7 @@ class _MenuButtonState extends State<MenuButton> {
|
||||
message: widget.tooltip,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Ink(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
color: _isHover ? widget.hoverColor : widget.color,
|
||||
|
@ -23,6 +23,10 @@ import '../../common/shared_state.dart';
|
||||
import './popup_menu.dart';
|
||||
import './kb_layout_type_chooser.dart';
|
||||
|
||||
const _kKeyLegacyMode = 'legacy';
|
||||
const _kKeyMapMode = 'map';
|
||||
const _kKeyTranslateMode = 'translate';
|
||||
|
||||
class MenubarState {
|
||||
final kStoreKey = 'remoteMenubarState';
|
||||
late RxBool show;
|
||||
@ -104,6 +108,7 @@ class _MenubarTheme {
|
||||
static const double buttonHMargin = 3;
|
||||
static const double buttonVMargin = 6;
|
||||
static const double iconRadius = 8;
|
||||
static const double elevation = 3;
|
||||
}
|
||||
|
||||
typedef DismissFunc = void Function();
|
||||
@ -365,10 +370,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
alignment: FractionalOffset(_fractionX.value, 0),
|
||||
child: Offstage(
|
||||
offstage: _dragging.isTrue,
|
||||
child: _DraggableShowHide(
|
||||
dragging: _dragging,
|
||||
fractionX: _fractionX,
|
||||
show: show,
|
||||
child: Material(
|
||||
elevation: _MenubarTheme.elevation,
|
||||
child: _DraggableShowHide(
|
||||
dragging: _dragging,
|
||||
fractionX: _fractionX,
|
||||
show: show,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -402,22 +410,27 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
Material(
|
||||
elevation: _MenubarTheme.elevation,
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
color: Theme.of(context)
|
||||
.menuBarTheme
|
||||
.style
|
||||
?.backgroundColor
|
||||
?.resolve(MaterialState.values.toSet()),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Theme(
|
||||
data: themeData(),
|
||||
child: MenuBar(
|
||||
children: [
|
||||
SizedBox(width: _MenubarTheme.buttonHMargin),
|
||||
...menubarItems,
|
||||
SizedBox(width: _MenubarTheme.buttonHMargin)
|
||||
],
|
||||
),
|
||||
)),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Theme(
|
||||
data: themeData(),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: _MenubarTheme.buttonHMargin * 2),
|
||||
...menubarItems,
|
||||
SizedBox(width: _MenubarTheme.buttonHMargin * 2)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDraggableShowHide(context),
|
||||
],
|
||||
@ -427,11 +440,22 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
ThemeData themeData() {
|
||||
return Theme.of(context).copyWith(
|
||||
menuButtonTheme: MenuButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
minimumSize: MaterialStatePropertyAll(Size(64, 36)),
|
||||
textStyle: MaterialStatePropertyAll(
|
||||
TextStyle(fontWeight: FontWeight.normal)))),
|
||||
style: ButtonStyle(
|
||||
minimumSize: MaterialStatePropertyAll(Size(64, 36)),
|
||||
textStyle: MaterialStatePropertyAll(
|
||||
TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
),
|
||||
),
|
||||
dividerTheme: DividerThemeData(space: 4),
|
||||
menuBarTheme: MenuBarThemeData(
|
||||
style: MenuStyle(
|
||||
padding: MaterialStatePropertyAll(EdgeInsets.zero),
|
||||
elevation: MaterialStatePropertyAll(0),
|
||||
shape: MaterialStatePropertyAll(BeveledRectangleBorder()),
|
||||
).copyWith(
|
||||
backgroundColor:
|
||||
Theme.of(context).menuBarTheme.style?.backgroundColor)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -501,8 +525,12 @@ class _MonitorMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (stateGlobal.displaysCount.value < 2) return Offstage();
|
||||
if (PrivacyModeState.find(id).isTrue ||
|
||||
stateGlobal.displaysCount.value < 2) {
|
||||
return Offstage();
|
||||
}
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Select Monitor',
|
||||
icon: icon(),
|
||||
ffi: ffi,
|
||||
color: _MenubarTheme.blueColor,
|
||||
@ -541,6 +569,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
final pi = ffi.ffiModel.pi;
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
rowChildren.add(_IconMenuButton(
|
||||
topLevel: false,
|
||||
color: _MenubarTheme.blueColor,
|
||||
hoverColor: _MenubarTheme.hoverBlueColor,
|
||||
tooltip: "",
|
||||
@ -593,6 +622,7 @@ class _ControlMenu extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Control Actions',
|
||||
svg: "assets/actions.svg",
|
||||
color: _MenubarTheme.blueColor,
|
||||
hoverColor: _MenubarTheme.hoverBlueColor,
|
||||
@ -651,26 +681,44 @@ class _ControlMenu extends StatelessWidget {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('OS Password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit),
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
@ -896,6 +944,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
Widget build(BuildContext context) {
|
||||
_updateScreen();
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Display Settings',
|
||||
svg: "assets/display.svg",
|
||||
ffi: widget.ffi,
|
||||
color: _MenubarTheme.blueColor,
|
||||
@ -916,6 +965,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
disableClipboard(),
|
||||
lockAfterSessionEnd(),
|
||||
privacyMode(),
|
||||
swapKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -949,12 +999,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;
|
||||
@ -1023,10 +1074,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);
|
||||
}
|
||||
@ -1400,6 +1451,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
}
|
||||
|
||||
showRemoteCursor() {
|
||||
if (widget.ffi.ffiModel.pi.platform == kPeerPlatformAndroid) {
|
||||
return Offstage();
|
||||
}
|
||||
final visible = !widget.ffi.canvasModel.cursorEmbedded;
|
||||
if (!visible) return Offstage();
|
||||
final state = ShowRemoteCursorState.find(widget.id);
|
||||
@ -1417,6 +1471,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
}
|
||||
|
||||
zoomCursor() {
|
||||
if (widget.ffi.ffiModel.pi.platform == kPeerPlatformAndroid) {
|
||||
return Offstage();
|
||||
}
|
||||
final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal;
|
||||
if (!visible) return Offstage();
|
||||
final option = 'zoom-cursor';
|
||||
@ -1518,11 +1575,38 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
value: rxValue.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (widget.ffi.ffiModel.pi.currentDisplay != 0) {
|
||||
msgBox(
|
||||
widget.id,
|
||||
'custom-nook-nocancel-hasclose',
|
||||
'info',
|
||||
'Please switch to Display 1 first',
|
||||
'',
|
||||
widget.ffi.dialogManager);
|
||||
return;
|
||||
}
|
||||
bind.sessionToggleOption(id: widget.id, value: option);
|
||||
},
|
||||
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 {
|
||||
@ -1540,12 +1624,17 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var ffiModel = Provider.of<FfiModel>(context);
|
||||
if (ffiModel.permissions['keyboard'] == false) return Offstage();
|
||||
// Do not support peer 1.1.9.
|
||||
if (stateGlobal.grabKeyboard) {
|
||||
bind.sessionSetKeyboardMode(id: id, value: 'map');
|
||||
if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) {
|
||||
bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode);
|
||||
} else if (bind.sessionIsKeyboardModeSupported(
|
||||
id: id, mode: _kKeyLegacyMode)) {
|
||||
bind.sessionSetKeyboardMode(id: id, value: _kKeyLegacyMode);
|
||||
}
|
||||
return Offstage();
|
||||
}
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Keyboard Settings',
|
||||
svg: "assets/keyboard.svg",
|
||||
ffi: ffi,
|
||||
color: _MenubarTheme.blueColor,
|
||||
@ -1555,13 +1644,13 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
|
||||
mode() {
|
||||
return futureBuilder(future: () async {
|
||||
return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy';
|
||||
return await bind.sessionGetKeyboardMode(id: id) ?? _kKeyLegacyMode;
|
||||
}(), hasData: (data) {
|
||||
final groupValue = data as String;
|
||||
List<KeyboardModeMenu> modes = [
|
||||
KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'),
|
||||
KeyboardModeMenu(key: 'map', menu: 'Map mode'),
|
||||
KeyboardModeMenu(key: 'translate', menu: 'Translate mode'),
|
||||
KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
|
||||
KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
|
||||
KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
|
||||
];
|
||||
List<_RadioMenuButton> list = [];
|
||||
onChanged(String? value) async {
|
||||
@ -1571,13 +1660,13 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
|
||||
for (KeyboardModeMenu mode in modes) {
|
||||
if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) {
|
||||
if (mode.key == 'translate') {
|
||||
if (mode.key == _kKeyTranslateMode) {
|
||||
if (Platform.isLinux || pi.platform == kPeerPlatformLinux) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var text = translate(mode.menu);
|
||||
if (mode.key == 'translate') {
|
||||
if (mode.key == _kKeyTranslateMode) {
|
||||
text = '$text beta';
|
||||
}
|
||||
list.add(_RadioMenuButton<String>(
|
||||
@ -1633,6 +1722,7 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
@ -1751,22 +1841,24 @@ class _CloseMenu extends StatelessWidget {
|
||||
class _IconMenuButton extends StatefulWidget {
|
||||
final String? assetName;
|
||||
final Widget? icon;
|
||||
final String tooltip;
|
||||
final String? tooltip;
|
||||
final Color color;
|
||||
final Color hoverColor;
|
||||
final VoidCallback? onPressed;
|
||||
final double? hMargin;
|
||||
final double? vMargin;
|
||||
final bool topLevel;
|
||||
const _IconMenuButton({
|
||||
Key? key,
|
||||
this.assetName,
|
||||
this.icon,
|
||||
required this.tooltip,
|
||||
this.tooltip,
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.onPressed,
|
||||
this.hMargin,
|
||||
this.vMargin,
|
||||
this.topLevel = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -1786,36 +1878,40 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
|
||||
width: _MenubarTheme.buttonSize,
|
||||
height: _MenubarTheme.buttonSize,
|
||||
);
|
||||
return SizedBox(
|
||||
final button = SizedBox(
|
||||
width: _MenubarTheme.buttonSize,
|
||||
height: _MenubarTheme.buttonSize,
|
||||
child: MenuItemButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.transparent),
|
||||
padding: MaterialStatePropertyAll(EdgeInsets.zero),
|
||||
overlayColor: MaterialStatePropertyAll(Colors.transparent)),
|
||||
onHover: (value) => setState(() {
|
||||
hover = value;
|
||||
}),
|
||||
onPressed: widget.onPressed,
|
||||
child: Tooltip(
|
||||
message: translate(widget.tooltip),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(_MenubarTheme.iconRadius),
|
||||
color: hover ? widget.hoverColor : widget.color,
|
||||
),
|
||||
child: icon))),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_MenubarTheme.iconRadius),
|
||||
color: hover ? widget.hoverColor : widget.color,
|
||||
),
|
||||
child: icon)),
|
||||
),
|
||||
).marginSymmetric(
|
||||
horizontal: widget.hMargin ?? _MenubarTheme.buttonHMargin,
|
||||
vertical: widget.vMargin ?? _MenubarTheme.buttonVMargin);
|
||||
if (widget.topLevel) {
|
||||
return MenuBar(children: [button]);
|
||||
} else {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IconSubmenuButton extends StatefulWidget {
|
||||
final String tooltip;
|
||||
final String? svg;
|
||||
final Widget? icon;
|
||||
final Color color;
|
||||
@ -1828,6 +1924,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
{Key? key,
|
||||
this.svg,
|
||||
this.icon,
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.menuChildren,
|
||||
@ -1852,32 +1949,35 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
width: _MenubarTheme.buttonSize,
|
||||
height: _MenubarTheme.buttonSize,
|
||||
);
|
||||
return SizedBox(
|
||||
width: _MenubarTheme.buttonSize,
|
||||
height: _MenubarTheme.buttonSize,
|
||||
child: SubmenuButton(
|
||||
menuStyle: widget.menuStyle,
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStatePropertyAll(EdgeInsets.zero),
|
||||
overlayColor: MaterialStatePropertyAll(Colors.transparent)),
|
||||
onHover: (value) => setState(() {
|
||||
hover = value;
|
||||
}),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(_MenubarTheme.iconRadius),
|
||||
color: hover ? widget.hoverColor : widget.color,
|
||||
),
|
||||
child: icon)),
|
||||
menuChildren: widget.menuChildren
|
||||
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
|
||||
.toList()))
|
||||
.marginSymmetric(
|
||||
horizontal: _MenubarTheme.buttonHMargin,
|
||||
vertical: _MenubarTheme.buttonVMargin);
|
||||
final button = SizedBox(
|
||||
width: _MenubarTheme.buttonSize,
|
||||
height: _MenubarTheme.buttonSize,
|
||||
child: SubmenuButton(
|
||||
menuStyle: widget.menuStyle,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.transparent),
|
||||
padding: MaterialStatePropertyAll(EdgeInsets.zero),
|
||||
overlayColor: MaterialStatePropertyAll(Colors.transparent)),
|
||||
onHover: (value) => setState(() {
|
||||
hover = value;
|
||||
}),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(_MenubarTheme.iconRadius),
|
||||
color: hover ? widget.hoverColor : widget.color,
|
||||
),
|
||||
child: icon)),
|
||||
menuChildren: widget.menuChildren
|
||||
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
|
||||
.toList()));
|
||||
return MenuBar(children: [
|
||||
button.marginSymmetric(
|
||||
horizontal: _MenubarTheme.buttonHMargin,
|
||||
vertical: _MenubarTheme.buttonVMargin)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2016,7 +2116,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
child: Icon(
|
||||
Icons.drag_indicator,
|
||||
size: 20,
|
||||
color: Colors.grey[800],
|
||||
color: MyTheme.color(context).drag_indicator,
|
||||
),
|
||||
feedback: widget,
|
||||
onDragStarted: (() {
|
||||
@ -2068,7 +2168,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
data: TextButtonThemeData(style: buttonStyle),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context)
|
||||
.menuBarTheme
|
||||
.style
|
||||
?.backgroundColor
|
||||
?.resolve(MaterialState.values.toSet()),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(5),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
@ -278,7 +280,6 @@ class DesktopTab extends StatelessWidget {
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -361,7 +362,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 +525,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 +546,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
if (widget.isMaximized.value) {
|
||||
widget.isMaximized.value = false;
|
||||
}
|
||||
_setMaximize(false);
|
||||
super.onWindowUnmaximize();
|
||||
}
|
||||
|
||||
@ -599,7 +608,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
offstage: !widget.showMaximize || Platform.isMacOS,
|
||||
child: Obx(() => ActionIcon(
|
||||
message:
|
||||
widget.isMaximized.value ? "Restore" : "Maximize",
|
||||
widget.isMaximized.value ? 'Restore' : 'Maximize',
|
||||
icon: widget.isMaximized.value
|
||||
? IconFont.restore
|
||||
: IconFont.max,
|
||||
@ -752,7 +761,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
|
||||
@ -946,7 +956,6 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
indent: _kDividerIndent,
|
||||
endIndent: _kDividerIndent,
|
||||
color: MyTheme.tabbar(context).dividerColor,
|
||||
thickness: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -153,6 +153,7 @@ void runMainApp(bool startService) async {
|
||||
void runMobileApp() async {
|
||||
await initEnv(kAppTypeMain);
|
||||
if (isAndroid) androidChannelInit();
|
||||
platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||
runApp(App());
|
||||
}
|
||||
|
||||
@ -216,7 +217,6 @@ void runMultiWindow(
|
||||
|
||||
void runConnectionManagerScreen(bool hide) async {
|
||||
await initEnv(kAppTypeConnectionManager);
|
||||
await bind.cmStartListenIpcThread();
|
||||
_runApp(
|
||||
'',
|
||||
const DesktopServerPage(),
|
||||
@ -291,17 +291,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 +312,7 @@ WindowOptions getHiddenTitleBarWindowOptions({Size? size}) {
|
||||
}
|
||||
return WindowOptions(
|
||||
size: size,
|
||||
center: false,
|
||||
center: center,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: defaultTitleBarStyle,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
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 +43,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 ==
|
||||
@ -138,9 +141,11 @@ class _ServerPageState extends State<ServerPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ServerInfo(),
|
||||
const PermissionChecker(),
|
||||
gFFI.serverModel.isStart
|
||||
? ServerInfo()
|
||||
: ServiceNotRunningNotification(),
|
||||
const ConnectionManager(),
|
||||
const PermissionChecker(),
|
||||
SizedBox.fromSize(size: const Size(0, 15.0)),
|
||||
],
|
||||
),
|
||||
@ -150,14 +155,42 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceNotRunningNotification extends StatelessWidget {
|
||||
ServiceNotRunningNotification({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
|
||||
return PaddingCard(
|
||||
title: translate("Service is not running"),
|
||||
titleIcon:
|
||||
const Icon(Icons.warning_amber_sharp, color: Colors.redAccent),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("android_start_service_tip"),
|
||||
style:
|
||||
const TextStyle(fontSize: 12, color: MyTheme.darkGray))
|
||||
.marginOnly(bottom: 8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: serverModel.toggleService,
|
||||
label: Text(translate("Start Service")))
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class ServerInfo extends StatelessWidget {
|
||||
final model = gFFI.serverModel;
|
||||
final emptyController = TextEditingController(text: "-");
|
||||
@ -167,78 +200,104 @@ class ServerInfo extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPermanent = model.verificationMethod == kUsePermanentPassword;
|
||||
return model.isStart
|
||||
? PaddingCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 25.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: MyTheme.accent),
|
||||
controller: model.serverId,
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(Icons.perm_identity),
|
||||
labelText: translate("ID"),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: MyTheme.accent80),
|
||||
),
|
||||
onSaved: (String? value) {},
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
|
||||
const Color colorPositive = Colors.green;
|
||||
const Color colorNegative = Colors.red;
|
||||
const double iconMarginRight = 15;
|
||||
const double iconSize = 24;
|
||||
const TextStyle textStyleHeading = TextStyle(
|
||||
fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.grey);
|
||||
const TextStyle textStyleValue =
|
||||
TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold);
|
||||
|
||||
void copyToClipboard(String value) {
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
showToast(translate('Copied'));
|
||||
}
|
||||
|
||||
Widget ConnectionStateNotification() {
|
||||
if (serverModel.connectStatus == -1) {
|
||||
return Row(children: [
|
||||
const Icon(Icons.warning_amber_sharp,
|
||||
color: colorNegative, size: iconSize)
|
||||
.marginOnly(right: iconMarginRight),
|
||||
Expanded(child: Text(translate('not_ready_status')))
|
||||
]);
|
||||
} else if (serverModel.connectStatus == 0) {
|
||||
return Row(children: [
|
||||
SizedBox(width: 20, height: 20, child: CircularProgressIndicator())
|
||||
.marginOnly(left: 4, right: iconMarginRight),
|
||||
Expanded(child: Text(translate('connecting_status')))
|
||||
]);
|
||||
} else {
|
||||
return Row(children: [
|
||||
const Icon(Icons.check, color: colorPositive, size: iconSize)
|
||||
.marginOnly(right: iconMarginRight),
|
||||
Expanded(child: Text(translate('Ready')))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return PaddingCard(
|
||||
title: translate('Your Device'),
|
||||
child: Column(
|
||||
// ID
|
||||
children: [
|
||||
Row(children: [
|
||||
const Icon(Icons.perm_identity,
|
||||
color: Colors.grey, size: iconSize)
|
||||
.marginOnly(right: iconMarginRight),
|
||||
Text(
|
||||
translate('ID'),
|
||||
style: textStyleHeading,
|
||||
)
|
||||
]),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(
|
||||
model.serverId.value.text,
|
||||
style: textStyleValue,
|
||||
),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 25.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: MyTheme.accent),
|
||||
controller: isPermanent ? emptyController : model.serverPasswd,
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(Icons.lock),
|
||||
labelText: translate("Password"),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: MyTheme.accent80),
|
||||
suffix: isPermanent
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () =>
|
||||
bind.mainUpdateTemporaryPassword())),
|
||||
onSaved: (String? value) {},
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(Icons.copy_outlined),
|
||||
onPressed: () {
|
||||
copyToClipboard(model.serverId.value.text.trim());
|
||||
})
|
||||
]).marginOnly(left: 39, bottom: 10),
|
||||
// Password
|
||||
Row(children: [
|
||||
const Icon(Icons.lock_outline, color: Colors.grey, size: iconSize)
|
||||
.marginOnly(right: iconMarginRight),
|
||||
Text(
|
||||
translate('One-time Password'),
|
||||
style: textStyleHeading,
|
||||
)
|
||||
]),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(
|
||||
isPermanent ? '-' : model.serverPasswd.value.text,
|
||||
style: textStyleValue,
|
||||
),
|
||||
],
|
||||
))
|
||||
: PaddingCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 24),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate("Service is not running"),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
color: MyTheme.accent,
|
||||
),
|
||||
))
|
||||
],
|
||||
)),
|
||||
const SizedBox(height: 5),
|
||||
Center(
|
||||
child: Text(
|
||||
translate("android_start_service_tip"),
|
||||
style: const TextStyle(fontSize: 12, color: MyTheme.darkGray),
|
||||
))
|
||||
],
|
||||
));
|
||||
isPermanent
|
||||
? SizedBox.shrink()
|
||||
: Row(children: [
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword()),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(Icons.copy_outlined),
|
||||
onPressed: () {
|
||||
copyToClipboard(
|
||||
model.serverPasswd.value.text.trim());
|
||||
})
|
||||
])
|
||||
]).marginOnly(left: 40, bottom: 15),
|
||||
ConnectionStateNotification()
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,78 +313,37 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
Widget build(BuildContext context) {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
final hasAudioPermission = androidVersion >= 30;
|
||||
final String status;
|
||||
if (serverModel.connectStatus == -1) {
|
||||
status = 'not_ready_status';
|
||||
} else if (serverModel.connectStatus == 0) {
|
||||
status = 'connecting_status';
|
||||
} else {
|
||||
status = 'Ready';
|
||||
}
|
||||
return PaddingCard(
|
||||
title: translate("Permissions"),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PermissionRow(translate("Screen Capture"), serverModel.mediaOk,
|
||||
serverModel.toggleService),
|
||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
||||
serverModel.toggleInput),
|
||||
PermissionRow(translate("Transfer File"), serverModel.fileOk,
|
||||
serverModel.toggleFile),
|
||||
hasAudioPermission
|
||||
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
||||
serverModel.toggleAudio)
|
||||
: Text(
|
||||
"* ${translate("android_version_audio_tip")}",
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
serverModel.mediaOk
|
||||
? ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Colors.red)),
|
||||
icon: const Icon(Icons.stop),
|
||||
onPressed: serverModel.toggleService,
|
||||
label: Text(translate("Stop service")))
|
||||
.marginOnly(bottom: 8)
|
||||
: SizedBox.shrink(),
|
||||
PermissionRow(translate("Screen Capture"), serverModel.mediaOk,
|
||||
serverModel.toggleService),
|
||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
||||
serverModel.toggleInput),
|
||||
PermissionRow(translate("Transfer File"), serverModel.fileOk,
|
||||
serverModel.toggleFile),
|
||||
hasAudioPermission
|
||||
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
||||
serverModel.toggleAudio)
|
||||
: Row(children: [
|
||||
Icon(Icons.info_outline).marginOnly(right: 15),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate("android_version_audio_tip"),
|
||||
style: const TextStyle(color: MyTheme.darkGray),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: serverModel.mediaOk
|
||||
? ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Colors.red)),
|
||||
icon: const Icon(Icons.stop),
|
||||
onPressed: serverModel.toggleService,
|
||||
label: Text(translate("Stop service")))
|
||||
: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: serverModel.toggleService,
|
||||
label: Text(translate("Start Service")))),
|
||||
Expanded(
|
||||
child: serverModel.mediaOk
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20, right: 5),
|
||||
child: Icon(Icons.circle,
|
||||
color: serverModel.connectStatus > 0
|
||||
? Colors.greenAccent
|
||||
: Colors.deepOrangeAccent,
|
||||
size: 10))),
|
||||
Expanded(
|
||||
child: Text(translate(status),
|
||||
softWrap: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: MyTheme.accent80)))
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink())
|
||||
],
|
||||
),
|
||||
],
|
||||
));
|
||||
))
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,37 +357,14 @@ class PermissionRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
Text(name, style: Theme.of(context).textTheme.labelLarge))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(isOk ? translate("ON") : translate("OFF"),
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: isOk ? Colors.green : Colors.grey))),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(
|
||||
translate(isOk ? "CLOSE" : "OPEN"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
)))),
|
||||
],
|
||||
);
|
||||
return SwitchListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
title: Text(name),
|
||||
value: isOk,
|
||||
onChanged: (bool value) {
|
||||
onPressed();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,70 +381,66 @@ class ConnectionManager extends StatelessWidget {
|
||||
? "File Connection"
|
||||
: "Screen Connection"),
|
||||
titleIcon: client.isFileTransfer
|
||||
? Icons.folder_outlined
|
||||
: Icons.mobile_screen_share,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(child: ClientInfo(client)),
|
||||
Expanded(
|
||||
flex: -1,
|
||||
child: client.isFileTransfer || !client.authorized
|
||||
? const SizedBox.shrink()
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
gFFI.chatModel.changeCurrentID(client.id);
|
||||
final bar =
|
||||
navigationBarKey.currentWidget;
|
||||
if (bar != null) {
|
||||
bar as BottomNavigationBar;
|
||||
bar.onTap!(1);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.chat,
|
||||
color: MyTheme.accent,
|
||||
)))
|
||||
],
|
||||
),
|
||||
client.authorized
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
translate("android_new_connection_tip"),
|
||||
style: Theme.of(globalKey.currentContext!)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
client.authorized
|
||||
? ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Colors.red)),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
bind.cmCloseConnection(connId: client.id);
|
||||
gFFI.invokeMethod(
|
||||
"cancel_notification", client.id);
|
||||
},
|
||||
label: Text(translate("Close")))
|
||||
: Row(children: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(client, false);
|
||||
}),
|
||||
const SizedBox(width: 20),
|
||||
ElevatedButton(
|
||||
child: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(client, true);
|
||||
}),
|
||||
]),
|
||||
],
|
||||
)))
|
||||
? Icon(Icons.folder_outlined)
|
||||
: Icon(Icons.mobile_screen_share),
|
||||
child: Column(children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(child: ClientInfo(client)),
|
||||
Expanded(
|
||||
flex: -1,
|
||||
child: client.isFileTransfer || !client.authorized
|
||||
? const SizedBox.shrink()
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
gFFI.chatModel.changeCurrentID(client.id);
|
||||
final bar = navigationBarKey.currentWidget;
|
||||
if (bar != null) {
|
||||
bar as BottomNavigationBar;
|
||||
bar.onTap!(1);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.chat)))
|
||||
],
|
||||
),
|
||||
client.authorized
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
translate("android_new_connection_tip"),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).marginOnly(bottom: 5),
|
||||
client.authorized
|
||||
? Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Colors.red)),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
bind.cmCloseConnection(connId: client.id);
|
||||
gFFI.invokeMethod(
|
||||
"cancel_notification", client.id);
|
||||
},
|
||||
label: Text(translate("Disconnect"))))
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(
|
||||
client, false);
|
||||
}).marginOnly(right: 15),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(client, true);
|
||||
}),
|
||||
]),
|
||||
])))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
@ -459,7 +450,7 @@ class PaddingCard extends StatelessWidget {
|
||||
: super(key: key);
|
||||
|
||||
final String? title;
|
||||
final IconData? titleIcon;
|
||||
final Icon? titleIcon;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
@ -469,23 +460,16 @@ class PaddingCard extends StatelessWidget {
|
||||
children.insert(
|
||||
0,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
padding: const EdgeInsets.fromLTRB(0, 5, 0, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
titleIcon != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child:
|
||||
Icon(titleIcon, color: MyTheme.accent, size: 30))
|
||||
: const SizedBox.shrink(),
|
||||
Text(
|
||||
title!,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: MyTheme.accent,
|
||||
),
|
||||
titleIcon?.marginOnly(right: 10) ?? const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Text(title!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(fontWeight: FontWeight.bold))),
|
||||
)
|
||||
],
|
||||
)));
|
||||
@ -493,12 +477,14 @@ class PaddingCard extends StatelessWidget {
|
||||
return SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(13),
|
||||
),
|
||||
margin: const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 0),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0),
|
||||
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
@ -514,7 +500,7 @@ class ClientInfo extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
child: Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -522,21 +508,19 @@ class ClientInfo extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: CircleAvatar(
|
||||
backgroundColor:
|
||||
str2color(client.name).withOpacity(0.7),
|
||||
backgroundColor: str2color(
|
||||
client.name,
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? 255
|
||||
: 150),
|
||||
child: Text(client.name[0])))),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(client.name,
|
||||
style: const TextStyle(
|
||||
color: MyTheme.idColor, fontSize: 18)),
|
||||
Text(client.name, style: const TextStyle(fontSize: 18)),
|
||||
const SizedBox(width: 8),
|
||||
Text(client.peerId,
|
||||
style: const TextStyle(
|
||||
color: MyTheme.idColor, fontSize: 10))
|
||||
Text(client.peerId, style: const TextStyle(fontSize: 10))
|
||||
]))
|
||||
],
|
||||
),
|
||||
@ -567,7 +551,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":
|
||||
|
@ -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: [
|
||||
@ -322,8 +397,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
showLanguageSettings(gFFI.dialogManager);
|
||||
}),
|
||||
SettingsTile.navigation(
|
||||
title: Text(translate('Dark Theme')),
|
||||
leading: Icon(Icons.dark_mode),
|
||||
title: Text(translate(
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? 'Dark Theme'
|
||||
: 'Light Theme')),
|
||||
leading: Icon(Theme.of(context).brightness == Brightness.light
|
||||
? Icons.dark_mode
|
||||
: Icons.light_mode),
|
||||
onPressed: (context) {
|
||||
showThemeSettings(gFFI.dialogManager);
|
||||
},
|
||||
@ -387,6 +467,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 {
|
||||
|
@ -25,19 +25,26 @@ void showRestartRemoteDevice(
|
||||
final res =
|
||||
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(translate("Restart Remote Device")),
|
||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||
Text(translate("Restart Remote Device")).paddingOnly(left: 10),
|
||||
]),
|
||||
content: Text(
|
||||
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: () => close(true),
|
||||
),
|
||||
],
|
||||
onCancel: close,
|
||||
onSubmit: () => close(true),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: () => close(true)),
|
||||
],
|
||||
));
|
||||
if (res == true) bind.sessionRestartRemoteDevice(id: id);
|
||||
}
|
||||
@ -62,7 +69,13 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Set your own password')),
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('Set your own password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
@ -112,11 +125,13 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: (validateLength && validateSame) ? submit : null,
|
||||
),
|
||||
],
|
||||
@ -147,7 +162,7 @@ void setTemporaryPasswordLengthDialog(
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set temporary password length")),
|
||||
title: Text(translate("Set one-time password length")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children:
|
||||
@ -178,7 +193,13 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Password Required')),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('Password Required')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
@ -197,8 +218,17 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: cancel, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit),
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
@ -437,7 +467,7 @@ void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) {
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||
hintText: 'eg: admin',
|
||||
hintText: translate('eg: admin'),
|
||||
errorText: errUser.isEmpty ? null : errUser.value),
|
||||
onChanged: (s) {
|
||||
if (s.isNotEmpty) {
|
||||
|
@ -43,7 +43,7 @@ class ChatModel with ChangeNotifier {
|
||||
|
||||
final ChatUser me = ChatUser(
|
||||
id: "",
|
||||
firstName: "Me",
|
||||
firstName: translate("Me"),
|
||||
);
|
||||
|
||||
late final Map<int, MessageBody> _messages = {}..[clientModeID] =
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -419,7 +419,50 @@ class InputModel {
|
||||
'type': _kMouseEventMove,
|
||||
});
|
||||
|
||||
void handleMouse(Map<String, dynamic> evt) {
|
||||
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
|
||||
{
|
||||
'x': pos.dx,
|
||||
'y': pos.dy,
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
},
|
||||
onExit: true,
|
||||
);
|
||||
|
||||
int trySetNearestRange(int v, int min, int max, int n) {
|
||||
if (v < min && v >= min - n) {
|
||||
v = min;
|
||||
}
|
||||
if (v > max && v <= max + n) {
|
||||
v = max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
Offset setNearestEdge(double x, double y, Display d) {
|
||||
double left = x - d.x;
|
||||
double right = d.x + d.width - 1 - x;
|
||||
double top = y - d.y;
|
||||
double bottom = d.y + d.height - 1 - y;
|
||||
if (left < right && left < top && left < bottom) {
|
||||
x = d.x;
|
||||
}
|
||||
if (right < left && right < top && right < bottom) {
|
||||
x = d.x + d.width - 1;
|
||||
}
|
||||
if (top < left && top < right && top < bottom) {
|
||||
y = d.y;
|
||||
}
|
||||
if (bottom < left && bottom < right && bottom < top) {
|
||||
y = d.y + d.height - 1;
|
||||
}
|
||||
return Offset(x, y);
|
||||
}
|
||||
|
||||
void handleMouse(
|
||||
Map<String, dynamic> evt, {
|
||||
bool onExit = false,
|
||||
}) {
|
||||
double x = evt['x'];
|
||||
double y = max(0.0, evt['y']);
|
||||
final cursorModel = parent.target!.cursorModel;
|
||||
@ -458,18 +501,21 @@ class InputModel {
|
||||
return;
|
||||
}
|
||||
evt['type'] = type;
|
||||
if (isDesktop) {
|
||||
y = y - stateGlobal.tabBarHeight;
|
||||
}
|
||||
y -= CanvasModel.topToEdge;
|
||||
x -= CanvasModel.leftToEdge;
|
||||
final canvasModel = parent.target!.canvasModel;
|
||||
final nearThr = 3;
|
||||
var nearRight = (canvasModel.size.width - x) < nearThr;
|
||||
var nearBottom = (canvasModel.size.height - y) < nearThr;
|
||||
|
||||
final ffiModel = parent.target!.ffiModel;
|
||||
if (isMove) {
|
||||
canvasModel.moveDesktopMouse(x, y);
|
||||
}
|
||||
final d = ffiModel.display;
|
||||
final imageWidth = d.width * canvasModel.scale;
|
||||
final imageHeight = d.height * canvasModel.scale;
|
||||
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidth = d.width * canvasModel.scale;
|
||||
final imageHeight = d.height * canvasModel.scale;
|
||||
x += imageWidth * canvasModel.scrollX;
|
||||
y += imageHeight * canvasModel.scrollY;
|
||||
|
||||
@ -487,10 +533,42 @@ class InputModel {
|
||||
|
||||
x /= canvasModel.scale;
|
||||
y /= canvasModel.scale;
|
||||
if (canvasModel.scale > 0 && canvasModel.scale < 1) {
|
||||
final step = 1.0 / canvasModel.scale - 1;
|
||||
if (nearRight) {
|
||||
x += step;
|
||||
}
|
||||
if (nearBottom) {
|
||||
y += step;
|
||||
}
|
||||
}
|
||||
x += d.x;
|
||||
y += d.y;
|
||||
|
||||
if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) {
|
||||
if (onExit) {
|
||||
final pos = setNearestEdge(x, y, d);
|
||||
x = pos.dx;
|
||||
y = pos.dy;
|
||||
}
|
||||
|
||||
var evtX = 0;
|
||||
var evtY = 0;
|
||||
try {
|
||||
evtX = x.round();
|
||||
evtY = y.round();
|
||||
} catch (e) {
|
||||
debugPrintStack(
|
||||
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
|
||||
return;
|
||||
}
|
||||
|
||||
int minX = d.x.toInt();
|
||||
int maxX = (d.x + d.width).toInt() - 1;
|
||||
int minY = d.y.toInt();
|
||||
int maxY = (d.y + d.height).toInt() - 1;
|
||||
evtX = trySetNearestRange(evtX, minX, maxX, 5);
|
||||
evtY = trySetNearestRange(evtY, minY, maxY, 5);
|
||||
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
|
||||
// If left mouse up, no early return.
|
||||
if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
|
||||
return;
|
||||
@ -498,12 +576,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:
|
||||
|
@ -156,7 +156,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'clipboard') {
|
||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||
} else if (name == 'permission') {
|
||||
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||
updatePermission(evt, peerId);
|
||||
} else if (name == 'chat_client_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? '');
|
||||
@ -166,17 +166,18 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'file_dir') {
|
||||
parent.target?.fileModel.receiveFileDir(evt);
|
||||
} else if (name == 'job_progress') {
|
||||
parent.target?.fileModel.tryUpdateJobProgress(evt);
|
||||
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
||||
} else if (name == 'job_done') {
|
||||
parent.target?.fileModel.jobDone(evt);
|
||||
parent.target?.fileModel.jobController.jobDone(evt);
|
||||
parent.target?.fileModel.refreshAll();
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobError(evt);
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
parent.target?.fileModel.overrideFileConfirm(evt);
|
||||
} else if (name == 'load_last_job') {
|
||||
parent.target?.fileModel.loadLastJob(evt);
|
||||
parent.target?.fileModel.jobController.loadLastJob(evt);
|
||||
} else if (name == 'update_folder_files') {
|
||||
parent.target?.fileModel.updateFolderFiles(evt);
|
||||
parent.target?.fileModel.jobController.updateFolderFiles(evt);
|
||||
} else if (name == 'add_connection') {
|
||||
parent.target?.serverModel.addConnection(evt);
|
||||
} else if (name == 'on_client_remove') {
|
||||
@ -241,36 +242,33 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
|
||||
final oldOrientation = _display.width > _display.height;
|
||||
var old = _pi.currentDisplay;
|
||||
_pi.currentDisplay = int.parse(evt['display']);
|
||||
_display.x = double.parse(evt['x']);
|
||||
_display.y = double.parse(evt['y']);
|
||||
_display.width = int.parse(evt['width']);
|
||||
_display.height = int.parse(evt['height']);
|
||||
_display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
|
||||
if (old != _pi.currentDisplay) {
|
||||
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
||||
_updateCurDisplay(String peerId, Display newDisplay) {
|
||||
if (newDisplay != _display) {
|
||||
if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
|
||||
parent.target?.cursorModel
|
||||
.updateDisplayOrigin(newDisplay.x, newDisplay.y);
|
||||
}
|
||||
_display = newDisplay;
|
||||
_updateSessionWidthHeight(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(peerId, display.width, display.height);
|
||||
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
|
||||
_pi.currentDisplay = int.parse(evt['display']);
|
||||
var newDisplay = Display();
|
||||
newDisplay.x = double.parse(evt['x']);
|
||||
newDisplay.y = double.parse(evt['y']);
|
||||
newDisplay.width = int.parse(evt['width']);
|
||||
newDisplay.height = int.parse(evt['height']);
|
||||
newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
|
||||
|
||||
_updateCurDisplay(peerId, newDisplay);
|
||||
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
// remote is mobile, and orientation changed
|
||||
if ((_display.width > _display.height) != oldOrientation) {
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
}
|
||||
if (_pi.platform == kPeerPlatformLinux ||
|
||||
_pi.platform == kPeerPlatformWindows ||
|
||||
_pi.platform == kPeerPlatformMacOS) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
}
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
notifyListeners();
|
||||
@ -372,8 +370,13 @@ class FfiModel with ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(String id, int width, int height) {
|
||||
bind.sessionSetSize(id: id, width: display.width, height: display.height);
|
||||
_updateSessionWidthHeight(String id) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
if (display.width <= 0 || display.height <= 0) {
|
||||
debugPrintStack(label: 'invalid display size (${display.width},${display.height})');
|
||||
} else {
|
||||
bind.sessionSetSize(id: id, width: display.width, height: display.height);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
@ -429,7 +432,7 @@ class FfiModel with ChangeNotifier {
|
||||
stateGlobal.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay < _pi.displays.length) {
|
||||
_display = _pi.displays[_pi.currentDisplay];
|
||||
_updateSessionWidthHeight(peerId, display.width, display.height);
|
||||
_updateSessionWidthHeight(peerId);
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
parent.target?.dialogManager.showLoading(
|
||||
@ -488,7 +491,7 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.displays = newDisplays;
|
||||
stateGlobal.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
|
||||
_display = _pi.displays[_pi.currentDisplay];
|
||||
_updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
@ -619,13 +622,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
|
||||
@ -654,9 +672,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;
|
||||
}
|
||||
@ -682,7 +705,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;
|
||||
|
||||
@ -709,12 +732,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);
|
||||
}
|
||||
|
||||
@ -788,21 +824,33 @@ 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;
|
||||
var dxOffset = 0;
|
||||
var dyOffset = 0;
|
||||
if (dw > size.width) {
|
||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||
}
|
||||
if (dh > size.height) {
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
try {
|
||||
if (dw > size.width) {
|
||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||
}
|
||||
if (dh > size.height) {
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrintStack(
|
||||
label:
|
||||
'(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e');
|
||||
return;
|
||||
}
|
||||
|
||||
_x += dxOffset;
|
||||
_y += dyOffset;
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
@ -1528,9 +1576,6 @@ class FFI {
|
||||
}();
|
||||
// every instance will bind a stream
|
||||
this.id = id;
|
||||
if (isFileTransfer) {
|
||||
fileModel.initFileFetcher();
|
||||
}
|
||||
}
|
||||
|
||||
/// Login with [password], choose if the client should [remember] it.
|
||||
@ -1579,6 +1624,19 @@ class Display {
|
||||
? kDesktopDefaultDisplayHeight
|
||||
: kMobileDefaultDisplayHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Display &&
|
||||
other.runtimeType == runtimeType &&
|
||||
_innerEqual(other);
|
||||
|
||||
bool _innerEqual(Display other) =>
|
||||
other.x == x &&
|
||||
other.y == y &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.cursorEmbedded == cursorEmbedded;
|
||||
}
|
||||
|
||||
class Resolution {
|
||||
|
@ -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;
|
||||
@ -234,6 +234,9 @@ class PlatformFFI {
|
||||
debugPrint(
|
||||
'_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir');
|
||||
}
|
||||
if (desktopType == DesktopType.cm) {
|
||||
await _ffiBind.cmStartListenIpcThread();
|
||||
}
|
||||
await _ffiBind.mainDeviceId(id: id);
|
||||
await _ffiBind.mainDeviceName(name: name);
|
||||
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
||||
@ -301,4 +304,8 @@ class PlatformFFI {
|
||||
if (!isAndroid) return Future<bool>(() => false);
|
||||
return await _toAndroidChannel.invokeMethod(method, arguments);
|
||||
}
|
||||
|
||||
void syncAndroidServiceAppDirConfigPath() {
|
||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,13 @@ class Peer {
|
||||
String rdpUsername;
|
||||
bool online = false;
|
||||
|
||||
String getId() {
|
||||
if (alias != '') {
|
||||
return alias;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
Peer.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'] ?? '',
|
||||
username = json['username'] ?? '',
|
||||
|
@ -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';
|
||||
@ -127,10 +128,13 @@ class ServerModel with ChangeNotifier {
|
||||
_connectStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
final res = await bind.cmCheckClientsLength(length: _clients.length);
|
||||
if (res != null) {
|
||||
debugPrint("clients not match!");
|
||||
updateClientState(res);
|
||||
|
||||
if (desktopType == DesktopType.cm) {
|
||||
final res = await bind.cmCheckClientsLength(length: _clients.length);
|
||||
if (res != null) {
|
||||
debugPrint("clients not match!");
|
||||
updateClientState(res);
|
||||
}
|
||||
}
|
||||
|
||||
updatePasswordModel();
|
||||
@ -154,7 +158,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 +168,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 +234,13 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
toggleAudio() async {
|
||||
if (!_audioOk && !await PermissionManager.check("audio")) {
|
||||
final res = await PermissionManager.request("audio");
|
||||
if (clients.isNotEmpty) {
|
||||
await showClientsMayNotBeChangedAlert(parent.target);
|
||||
}
|
||||
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
|
||||
final res = await AndroidPermissionManager.request(kRecordAudio);
|
||||
if (!res) {
|
||||
// TODO handle fail
|
||||
showToast(translate('Failed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -243,10 +251,15 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
toggleFile() async {
|
||||
if (!_fileOk && !await PermissionManager.check("file")) {
|
||||
final res = await PermissionManager.request("file");
|
||||
if (clients.isNotEmpty) {
|
||||
await showClientsMayNotBeChangedAlert(parent.target);
|
||||
}
|
||||
if (!_fileOk &&
|
||||
!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
final res =
|
||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||
if (!res) {
|
||||
// TODO handle fail
|
||||
showToast(translate('Failed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -256,11 +269,17 @@ class ServerModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
toggleInput() {
|
||||
toggleInput() async {
|
||||
if (clients.isNotEmpty) {
|
||||
await showClientsMayNotBeChangedAlert(parent.target);
|
||||
}
|
||||
if (_inputOk) {
|
||||
parent.target?.invokeMethod("stop_input");
|
||||
bind.mainSetOption(key: "enable-keyboard", value: 'N');
|
||||
} else {
|
||||
if (parent.target != null) {
|
||||
/// the result of toggle-on depends on user actions in the settings page.
|
||||
/// handle result, see [ServerModel.changeStatue]
|
||||
showInputWarnAlert(parent.target!);
|
||||
}
|
||||
}
|
||||
@ -282,7 +301,7 @@ class ServerModel with ChangeNotifier {
|
||||
content: Text(translate("android_stop_service_tip")),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
@ -344,10 +363,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));
|
||||
@ -456,7 +471,8 @@ class ServerModel with ChangeNotifier {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (!hideCm) window_on_top(null);
|
||||
});
|
||||
if (client.authorized) {
|
||||
// Only do the hidden task when on Desktop.
|
||||
if (client.authorized && isDesktop) {
|
||||
cmHiddenTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (!hideCm) windowManager.minimize();
|
||||
cmHiddenTimer = null;
|
||||
@ -561,7 +577,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 +701,7 @@ String getLoginDialogTag(int id) {
|
||||
showInputWarnAlert(FFI ffi) {
|
||||
ffi.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
ffi.serverModel.initInput();
|
||||
AndroidPermissionManager.startAction(kActionAccessibilitySettings);
|
||||
close();
|
||||
}
|
||||
|
||||
@ -707,3 +724,22 @@ showInputWarnAlert(FFI ffi) {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> showClientsMayNotBeChangedAlert(FFI? ffi) async {
|
||||
await ffi?.dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Permissions")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("android_permission_may_not_change_tip")),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("OK", onPressed: close),
|
||||
],
|
||||
onSubmit: close,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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,18 +20,31 @@ 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 && !_fullscreen) {
|
||||
_maximize = v;
|
||||
_resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
|
||||
}
|
||||
}
|
||||
setFullscreen(bool v) {
|
||||
if (_fullscreen != v) {
|
||||
_fullscreen = v;
|
||||
_showTabBar.value = !_fullscreen;
|
||||
_resizeEdgeSize.value =
|
||||
fullscreen ? kFullScreenEdgeSize : kWindowEdgeSize;
|
||||
fullscreen
|
||||
? kFullScreenEdgeSize
|
||||
: _maximize
|
||||
? kMaximizeEdgeSize
|
||||
: kWindowEdgeSize;
|
||||
print(
|
||||
"fullscreen: ${fullscreen}, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||
_windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth;
|
||||
WindowController.fromWindowId(windowId)
|
||||
.setFullscreen(_fullscreen)
|
||||
|
@ -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;
|
||||
|
@ -325,8 +325,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
|
||||
resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
|
||||
ref: "980ae3b45837a4bc55d82231801ffcbb0806b0c1"
|
||||
resolved-ref: "980ae3b45837a4bc55d82231801ffcbb0806b0c1"
|
||||
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@ -386,6 +386,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.2"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dropdown_button2
|
||||
sha256: "4458d81bfd24207f3d58f66f78097064e02f810f94cf1bc80bf20fe7685ebc80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
event_bus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1228,10 +1236,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: texture_rgba_renderer
|
||||
sha256: fbb09b2c6b4ce71261927f9e7e4ea339af3e2f3f2b175f6fb921de1c66ec848d
|
||||
sha256: "52bc9f217b7b07a760ee837d5a17329ad1f78ae8ed1e3fa612c6f1bed3c77f79"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.8"
|
||||
version: "0.0.13"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1508,11 +1516,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "32b24c66151b72bba033ef8b954486aa9351d97b"
|
||||
resolved-ref: "32b24c66151b72bba033ef8b954486aa9351d97b"
|
||||
ref: c140f9685bead805c433c9f5a4618a5115f01fa8
|
||||
resolved-ref: c140f9685bead805c433c9f5a4618a5115f01fa8
|
||||
url: "https://github.com/Kingtous/rustdesk_window_manager"
|
||||
source: git
|
||||
version: "0.2.7"
|
||||
version: "0.3.1"
|
||||
window_size:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -55,11 +55,11 @@ dependencies:
|
||||
window_manager:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_window_manager
|
||||
ref: 32b24c66151b72bba033ef8b954486aa9351d97b
|
||||
ref: c140f9685bead805c433c9f5a4618a5115f01fa8
|
||||
desktop_multi_window:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
|
||||
ref: 980ae3b45837a4bc55d82231801ffcbb0806b0c1
|
||||
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).
|
||||
@ -92,8 +92,9 @@ dependencies:
|
||||
password_strength: ^0.2.0
|
||||
flutter_launcher_icons: ^0.11.0
|
||||
flutter_keyboard_visibility: ^5.4.0
|
||||
texture_rgba_renderer: ^0.0.8
|
||||
texture_rgba_renderer: ^0.0.13
|
||||
percent_indicator: ^4.2.2
|
||||
dropdown_button2: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
|
@ -4,5 +4,5 @@ dart pub global activate ffigen --version 5.0.1
|
||||
flutter pub get
|
||||
# call `flutter clean` if cargo build fails
|
||||
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
||||
cargo build --features flutter
|
||||
cargo build --features "flutter,flutter_texture_render"
|
||||
flutter run $@
|
||||
|
@ -7,16 +7,17 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
flexi_logger = { version = "0.25", features = ["async"] }
|
||||
protobuf = { version = "3.1", features = ["with-bytes"] }
|
||||
tokio = { version = "1.20", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["full"] }
|
||||
futures = "0.3"
|
||||
bytes = { version = "1.2", features = ["serde"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
env_logger = "0.10"
|
||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||
zstd = "0.9"
|
||||
quinn = {version = "0.8", optional = true }
|
||||
quinn = {version = "0.9", optional = true }
|
||||
anyhow = "1.0"
|
||||
futures-util = "0.3"
|
||||
directories-next = "2.0"
|
||||
@ -33,11 +34,11 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
|
||||
chrono = "0.4"
|
||||
backtrace = "0.3"
|
||||
libc = "0.2"
|
||||
sysinfo = "0.24"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
machine-uid = "0.2"
|
||||
sysinfo = "0.28"
|
||||
|
||||
[features]
|
||||
quic = []
|
||||
@ -53,5 +54,5 @@ winapi = { version = "0.3", features = ["winuser"] }
|
||||
osascript = "0.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
toml = "0.5"
|
||||
toml = "0.7"
|
||||
serde_json = "1.0"
|
||||
|
5
libs/hbb_common/examples/config.rs
Normal file
5
libs/hbb_common/examples/config.rs
Normal file
@ -0,0 +1,5 @@
|
||||
extern crate hbb_common;
|
||||
|
||||
fn main() {
|
||||
println!("{:?}", hbb_common::config::PeerConfig::load("455058072"));
|
||||
}
|
@ -4,7 +4,7 @@ use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
time::SystemTime,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
@ -51,6 +51,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned()));
|
||||
static ref KEY_PAIR: Arc<Mutex<Option<KeyPair>>> = Default::default();
|
||||
static ref HW_CODEC_CONFIG: Arc<RwLock<HwCodecConfig>> = Arc::new(RwLock::new(HwCodecConfig::load()));
|
||||
static ref USER_DEFAULT_CONFIG: Arc<RwLock<(UserDefaultConfig, Instant)>> = Arc::new(RwLock::new((UserDefaultConfig::load(), Instant::now())));
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@ -110,10 +111,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 {
|
||||
@ -123,7 +124,7 @@ macro_rules! serde_field_bool {
|
||||
}
|
||||
impl $struct_name {
|
||||
pub fn $func() -> bool {
|
||||
UserDefaultConfig::load().get($field_name) == "Y"
|
||||
UserDefaultConfig::read().get($field_name) == "Y"
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -217,6 +218,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)]
|
||||
@ -978,21 +981,21 @@ impl PeerConfig {
|
||||
serde_field_string!(
|
||||
default_view_style,
|
||||
deserialize_view_style,
|
||||
UserDefaultConfig::load().get("view_style")
|
||||
UserDefaultConfig::read().get("view_style")
|
||||
);
|
||||
serde_field_string!(
|
||||
default_scroll_style,
|
||||
deserialize_scroll_style,
|
||||
UserDefaultConfig::load().get("scroll_style")
|
||||
UserDefaultConfig::read().get("scroll_style")
|
||||
);
|
||||
serde_field_string!(
|
||||
default_image_quality,
|
||||
deserialize_image_quality,
|
||||
UserDefaultConfig::load().get("image_quality")
|
||||
UserDefaultConfig::read().get("image_quality")
|
||||
);
|
||||
|
||||
fn default_custom_image_quality() -> Vec<i32> {
|
||||
let f: f64 = UserDefaultConfig::load()
|
||||
let f: f64 = UserDefaultConfig::read()
|
||||
.get("custom_image_quality")
|
||||
.parse()
|
||||
.unwrap_or(50.0);
|
||||
@ -1018,15 +1021,15 @@ impl PeerConfig {
|
||||
let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?;
|
||||
let mut key = "codec-preference";
|
||||
if !mp.contains_key(key) {
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::load().get(key));
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
|
||||
}
|
||||
key = "custom-fps";
|
||||
if !mp.contains_key(key) {
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::load().get(key));
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
|
||||
}
|
||||
key = "zoom-cursor";
|
||||
if !mp.contains_key(key) {
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::load().get(key));
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
|
||||
}
|
||||
Ok(mp)
|
||||
}
|
||||
@ -1035,30 +1038,52 @@ 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,
|
||||
"DisableAudio::default_disable_audio"
|
||||
);
|
||||
serde_field_bool!(DisableAudio, "disable_audio", 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,
|
||||
"PrivacyMode::default_privacy_mode"
|
||||
);
|
||||
|
||||
serde_field_bool!(
|
||||
AllowSwapKey,
|
||||
"allow_swap_key",
|
||||
default_allow_swap_key,
|
||||
"AllowSwapKey::default_allow_swap_key"
|
||||
);
|
||||
serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode);
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct LocalConfig {
|
||||
@ -1273,6 +1298,14 @@ pub struct UserDefaultConfig {
|
||||
}
|
||||
|
||||
impl UserDefaultConfig {
|
||||
pub fn read() -> UserDefaultConfig {
|
||||
let mut cfg = USER_DEFAULT_CONFIG.write().unwrap();
|
||||
if cfg.1.elapsed() > Duration::from_secs(1) {
|
||||
*cfg = (Self::load(), Instant::now());
|
||||
}
|
||||
cfg.0.clone()
|
||||
}
|
||||
|
||||
pub fn load() -> UserDefaultConfig {
|
||||
Config::load_::<UserDefaultConfig>("_default")
|
||||
}
|
||||
|
@ -39,9 +39,10 @@ pub use tokio_socks::IntoTargetAddr;
|
||||
pub use tokio_socks::TargetAddr;
|
||||
pub mod password_security;
|
||||
pub use chrono;
|
||||
pub use libc;
|
||||
pub use directories_next;
|
||||
pub use libc;
|
||||
pub mod keyboard;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use sysinfo;
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
@ -312,6 +313,44 @@ pub fn is_domain_port_str(id: &str) -> bool {
|
||||
.is_match(id)
|
||||
}
|
||||
|
||||
pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHandle> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use env_logger::*;
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
None
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write
|
||||
// though async logger more efficient, but it also causes more problems, disable it for now
|
||||
let mut logger_holder: Option<flexi_logger::LoggerHandle> = None;
|
||||
let mut path = config::Config::log_path();
|
||||
if !_name.is_empty() {
|
||||
path.push(_name);
|
||||
}
|
||||
use flexi_logger::*;
|
||||
if let Ok(x) = Logger::try_with_env_or_str("debug") {
|
||||
logger_holder = x
|
||||
.log_to_file(FileSpec::default().directory(path))
|
||||
.write_mode(if _is_async {
|
||||
WriteMode::Async
|
||||
} else {
|
||||
WriteMode::Direct
|
||||
})
|
||||
.format(opt_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(6),
|
||||
)
|
||||
.start()
|
||||
.ok();
|
||||
}
|
||||
logger_holder
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -81,8 +81,10 @@ fn get_display_server_of_session(session: &str) -> String {
|
||||
display_server = sestype;
|
||||
}
|
||||
}
|
||||
// If the session is not a tty, then just return the type as usual
|
||||
display_server
|
||||
if display_server == "" {
|
||||
display_server = "x11".to_owned();
|
||||
}
|
||||
display_server.to_lowercase()
|
||||
}
|
||||
|
||||
pub fn get_values_of_seat0(indices: Vec<usize>) -> Vec<String> {
|
||||
|
@ -42,7 +42,7 @@ quest = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
target_build_utils = "0.3"
|
||||
bindgen = "0.59"
|
||||
bindgen = "0.64"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
dbus = { version = "0.9", optional = true }
|
||||
|
292
libs/scrap/examples/benchmark.rs
Normal file
292
libs/scrap/examples/benchmark.rs
Normal file
@ -0,0 +1,292 @@
|
||||
use docopt::Docopt;
|
||||
use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV};
|
||||
use scrap::{
|
||||
codec::{EncoderApi, EncoderCfg},
|
||||
Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig,
|
||||
VpxVideoCodecId, STRIDE_ALIGN,
|
||||
};
|
||||
use std::{io::Write, time::Instant};
|
||||
|
||||
// cargo run --package scrap --example benchmark --release --features hwcodec
|
||||
|
||||
const USAGE: &'static str = "
|
||||
Codec benchmark.
|
||||
|
||||
Usage:
|
||||
benchmark [--count=COUNT] [--bitrate=KBS] [--hw-pixfmt=PIXFMT]
|
||||
benchmark (-h | --help)
|
||||
|
||||
Options:
|
||||
-h --help Show this screen.
|
||||
--count=COUNT Capture frame count [default: 100].
|
||||
--bitrate=KBS Video bitrate in kilobits per second [default: 5000].
|
||||
--hw-pixfmt=PIXFMT Hardware codec pixfmt. [default: i420]
|
||||
Valid values: i420, nv12.
|
||||
";
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct Args {
|
||||
flag_count: usize,
|
||||
flag_bitrate: usize,
|
||||
flag_hw_pixfmt: Pixfmt,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
enum Pixfmt {
|
||||
I420,
|
||||
NV12,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
let args: Args = Docopt::new(USAGE)
|
||||
.and_then(|d| d.deserialize())
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
let bitrate_k = args.flag_bitrate;
|
||||
let yuv_count = args.flag_count;
|
||||
let (yuvs, width, height) = capture_yuv(yuv_count);
|
||||
println!(
|
||||
"benchmark {}x{} bitrate:{}k hw_pixfmt:{:?}",
|
||||
width, height, bitrate_k, args.flag_hw_pixfmt
|
||||
);
|
||||
test_vp9(&yuvs, width, height, bitrate_k, yuv_count);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
use hwcodec::AVPixelFormat;
|
||||
let hw_pixfmt = match args.flag_hw_pixfmt {
|
||||
Pixfmt::I420 => AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
};
|
||||
let yuvs = hw::vp9_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
|
||||
hw::test(&yuvs, width, height, bitrate_k, yuv_count, hw_pixfmt);
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_yuv(yuv_count: usize) -> (Vec<Vec<u8>>, usize, usize) {
|
||||
let mut index = 0;
|
||||
let mut displays = Display::all().unwrap();
|
||||
for i in 0..displays.len() {
|
||||
if displays[i].is_primary() {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let d = displays.remove(index);
|
||||
let mut c = Capturer::new(d, true).unwrap();
|
||||
let mut v = vec![];
|
||||
loop {
|
||||
if let Ok(frame) = c.frame(std::time::Duration::from_millis(30)) {
|
||||
v.push(frame.0.to_vec());
|
||||
print!("\rcapture {}/{}", v.len(), yuv_count);
|
||||
std::io::stdout().flush().ok();
|
||||
if v.len() == yuv_count {
|
||||
println!();
|
||||
return (v, c.width(), c.height());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_vp9(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize, yuv_count: usize) {
|
||||
let config = EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
timebase: [1, 1000],
|
||||
bitrate: bitrate_k as _,
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
});
|
||||
let mut encoder = VpxEncoder::new(config).unwrap();
|
||||
let start = Instant::now();
|
||||
for yuv in yuvs {
|
||||
let _ = encoder
|
||||
.encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN)
|
||||
.unwrap();
|
||||
let _ = encoder.flush().unwrap();
|
||||
}
|
||||
println!("vp9 encode: {:?}", start.elapsed() / yuv_count as _);
|
||||
|
||||
// prepare data separately
|
||||
let mut vp9s = vec![];
|
||||
let start = Instant::now();
|
||||
for yuv in yuvs {
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
vp9s.push(frame.data.to_vec());
|
||||
}
|
||||
for ref frame in encoder.flush().unwrap() {
|
||||
vp9s.push(frame.data.to_vec());
|
||||
}
|
||||
}
|
||||
assert_eq!(vp9s.len(), yuv_count);
|
||||
|
||||
let mut decoder = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
for vp9 in vp9s {
|
||||
let _ = decoder.decode(&vp9);
|
||||
let _ = decoder.flush();
|
||||
}
|
||||
println!("vp9 decode: {:?}", start.elapsed() / yuv_count as _);
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
mod hw {
|
||||
use super::*;
|
||||
use hwcodec::{
|
||||
decode::{DecodeContext, Decoder},
|
||||
encode::{EncodeContext, Encoder},
|
||||
ffmpeg::{ffmpeg_linesize_offset_length, CodecInfo, CodecInfos},
|
||||
AVPixelFormat,
|
||||
Quality::*,
|
||||
RateControl::*,
|
||||
};
|
||||
use scrap::{
|
||||
convert::{
|
||||
hw::{hw_bgra_to_i420, hw_bgra_to_nv12},
|
||||
i420_to_bgra,
|
||||
},
|
||||
HW_STRIDE_ALIGN,
|
||||
};
|
||||
|
||||
pub fn test(
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
bitrate_k: usize,
|
||||
yuv_count: usize,
|
||||
pixfmt: AVPixelFormat,
|
||||
) {
|
||||
let ctx = EncodeContext {
|
||||
name: String::from(""),
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
pixfmt,
|
||||
align: 0,
|
||||
bitrate: (bitrate_k * 1000) as _,
|
||||
timebase: [1, 30],
|
||||
gop: 60,
|
||||
quality: Quality_Default,
|
||||
rc: RC_DEFAULT,
|
||||
};
|
||||
|
||||
let encoders = Encoder::available_encoders(ctx.clone());
|
||||
println!("hw encoders: {}", encoders.len());
|
||||
let best = CodecInfo::score(encoders.clone());
|
||||
for info in encoders {
|
||||
test_encoder(info.clone(), ctx.clone(), yuvs, is_best(&best, &info));
|
||||
}
|
||||
|
||||
let (h264s, h265s) = prepare_h26x(best, ctx.clone(), yuvs);
|
||||
assert!(h264s.is_empty() || h264s.len() == yuv_count);
|
||||
assert!(h265s.is_empty() || h265s.len() == yuv_count);
|
||||
let decoders = Decoder::available_decoders();
|
||||
println!("hw decoders: {}", decoders.len());
|
||||
let best = CodecInfo::score(decoders.clone());
|
||||
for info in decoders {
|
||||
let h26xs = if info.name.contains("h264") {
|
||||
&h264s
|
||||
} else {
|
||||
&h265s
|
||||
};
|
||||
if h26xs.len() == yuvs.len() {
|
||||
test_decoder(info.clone(), h26xs, is_best(&best, &info));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_encoder(info: CodecInfo, ctx: EncodeContext, yuvs: &Vec<Vec<u8>>, best: bool) {
|
||||
let mut ctx = ctx;
|
||||
ctx.name = info.name;
|
||||
let mut encoder = Encoder::new(ctx.clone()).unwrap();
|
||||
let start = Instant::now();
|
||||
for yuv in yuvs {
|
||||
let _ = encoder.encode(yuv).unwrap();
|
||||
}
|
||||
println!(
|
||||
"{}{}: {:?}",
|
||||
if best { "*" } else { "" },
|
||||
ctx.name,
|
||||
start.elapsed() / yuvs.len() as _
|
||||
);
|
||||
}
|
||||
|
||||
fn test_decoder(info: CodecInfo, h26xs: &Vec<Vec<u8>>, best: bool) {
|
||||
let ctx = DecodeContext {
|
||||
name: info.name,
|
||||
device_type: info.hwdevice,
|
||||
};
|
||||
|
||||
let mut decoder = Decoder::new(ctx.clone()).unwrap();
|
||||
let start = Instant::now();
|
||||
let mut cnt = 0;
|
||||
for h26x in h26xs {
|
||||
let _ = decoder.decode(h26x).unwrap();
|
||||
cnt += 1;
|
||||
}
|
||||
let device = format!("{:?}", ctx.device_type).to_lowercase();
|
||||
let device = device.split("_").last().unwrap();
|
||||
println!(
|
||||
"{}{} {}: {:?}",
|
||||
if best { "*" } else { "" },
|
||||
ctx.name,
|
||||
device,
|
||||
start.elapsed() / cnt
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare_h26x(
|
||||
best: CodecInfos,
|
||||
ctx: EncodeContext,
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
) -> (Vec<Vec<u8>>, Vec<Vec<u8>>) {
|
||||
let f = |info: Option<CodecInfo>| {
|
||||
let mut h26xs = vec![];
|
||||
if let Some(info) = info {
|
||||
let mut ctx = ctx.clone();
|
||||
ctx.name = info.name;
|
||||
let mut encoder = Encoder::new(ctx).unwrap();
|
||||
for yuv in yuvs {
|
||||
let h26x = encoder.encode(yuv).unwrap();
|
||||
for frame in h26x {
|
||||
h26xs.push(frame.data.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
h26xs
|
||||
};
|
||||
(f(best.h264), f(best.h265))
|
||||
}
|
||||
|
||||
fn is_best(best: &CodecInfos, info: &CodecInfo) -> bool {
|
||||
Some(info.clone()) == best.h264 || Some(info.clone()) == best.h265
|
||||
}
|
||||
|
||||
pub fn vp9_yuv_to_hw_yuv(
|
||||
yuvs: Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
pixfmt: AVPixelFormat,
|
||||
) -> Vec<Vec<u8>> {
|
||||
let yuvs = yuvs;
|
||||
let mut bgra = vec![];
|
||||
let mut v = vec![];
|
||||
let (linesize, offset, length) =
|
||||
ffmpeg_linesize_offset_length(pixfmt, width, height, HW_STRIDE_ALIGN).unwrap();
|
||||
for mut yuv in yuvs {
|
||||
i420_to_bgra(width, height, &yuv, &mut bgra);
|
||||
if pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P {
|
||||
hw_bgra_to_i420(width, height, &linesize, &offset, length, &bgra, &mut yuv);
|
||||
} else {
|
||||
hw_bgra_to_nv12(width, height, &linesize, &offset, length, &bgra, &mut yuv);
|
||||
}
|
||||
v.push(yuv);
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
@ -306,7 +306,7 @@ impl Decoder {
|
||||
pub fn handle_video_frame(
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
fmt: ImageFormat,
|
||||
fmt: (ImageFormat, usize),
|
||||
rgb: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
match frame {
|
||||
@ -352,7 +352,7 @@ impl Decoder {
|
||||
fn handle_vp9s_video_frame(
|
||||
decoder: &mut VpxDecoder,
|
||||
vp9s: &EncodedVideoFrames,
|
||||
fmt: ImageFormat,
|
||||
fmt: (ImageFormat, usize),
|
||||
rgb: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
let mut last_frame = Image::new();
|
||||
@ -369,7 +369,7 @@ impl Decoder {
|
||||
if last_frame.is_null() {
|
||||
Ok(false)
|
||||
} else {
|
||||
last_frame.to(fmt, 1, rgb);
|
||||
last_frame.to(fmt.0, fmt.1, rgb);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@ -378,7 +378,7 @@ impl Decoder {
|
||||
fn handle_hw_video_frame(
|
||||
decoder: &mut HwDecoder,
|
||||
frames: &EncodedVideoFrames,
|
||||
fmt: ImageFormat,
|
||||
fmt: (ImageFormat, usize),
|
||||
raw: &mut Vec<u8>,
|
||||
i420: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
@ -398,7 +398,7 @@ impl Decoder {
|
||||
fn handle_mediacodec_video_frame(
|
||||
decoder: &mut MediaCodecDecoder,
|
||||
frames: &EncodedVideoFrames,
|
||||
fmt: ImageFormat,
|
||||
fmt: (ImageFormat, usize),
|
||||
raw: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
let mut ret = false;
|
||||
|
@ -190,6 +190,29 @@ pub fn i420_to_rgb(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn i420_to_bgra(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
let (_, _, src_stride_y, src_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
let src_y = src.as_ptr();
|
||||
let src_u = src[u..].as_ptr();
|
||||
let src_v = src[v..].as_ptr();
|
||||
dst.resize(width * height * 4, 0);
|
||||
unsafe {
|
||||
super::I420ToARGB(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_uv as _,
|
||||
src_v,
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 3) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
let (_, h, dst_stride_y, dst_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
@ -269,8 +292,8 @@ pub unsafe fn nv12_to_i420(
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
pub mod hw {
|
||||
use hbb_common::{anyhow::anyhow, ResultType};
|
||||
use crate::ImageFormat;
|
||||
use hbb_common::{anyhow::anyhow, ResultType};
|
||||
#[cfg(target_os = "windows")]
|
||||
use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat};
|
||||
|
||||
@ -466,9 +489,7 @@ pub mod hw {
|
||||
_ => Err(anyhow!("NV12ToABGR failed")),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
Err(anyhow!("unsupported image format"))
|
||||
}
|
||||
_ => Err(anyhow!("unsupported image format")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -519,8 +540,7 @@ pub mod hw {
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -236,7 +236,13 @@ pub struct HwDecoderImage<'a> {
|
||||
}
|
||||
|
||||
impl HwDecoderImage<'_> {
|
||||
pub fn to_fmt(&self, fmt: ImageFormat, fmt_data: &mut Vec<u8>, i420: &mut Vec<u8>) -> ResultType<()> {
|
||||
// take dst_stride into account when you convert
|
||||
pub fn to_fmt(
|
||||
&self,
|
||||
(fmt, dst_stride): (ImageFormat, usize),
|
||||
fmt_data: &mut Vec<u8>,
|
||||
i420: &mut Vec<u8>,
|
||||
) -> ResultType<()> {
|
||||
let frame = self.frame;
|
||||
match frame.pixfmt {
|
||||
AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to(
|
||||
@ -270,11 +276,11 @@ impl HwDecoderImage<'_> {
|
||||
}
|
||||
|
||||
pub fn bgra(&self, bgra: &mut Vec<u8>, i420: &mut Vec<u8>) -> ResultType<()> {
|
||||
self.to_fmt(ImageFormat::ARGB, bgra, i420)
|
||||
self.to_fmt((ImageFormat::ARGB, 1), bgra, i420)
|
||||
}
|
||||
|
||||
pub fn rgba(&self, rgba: &mut Vec<u8>, i420: &mut Vec<u8>) -> ResultType<()> {
|
||||
self.to_fmt(ImageFormat::ABGR, rgba, i420)
|
||||
self.to_fmt((ImageFormat::ABGR, 1), rgba, i420)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use hbb_common::{log, anyhow::Error, bail, ResultType};
|
||||
use hbb_common::{anyhow::Error, bail, log, ResultType};
|
||||
use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat};
|
||||
use std::ops::Deref;
|
||||
use std::{
|
||||
@ -50,7 +50,13 @@ impl MediaCodecDecoder {
|
||||
MediaCodecDecoders { h264, h265 }
|
||||
}
|
||||
|
||||
pub fn decode(&mut self, data: &[u8], fmt: ImageFormat, raw: &mut Vec<u8>) -> ResultType<bool> {
|
||||
// take dst_stride into account please
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
(fmt, dst_stride): (ImageFormat, usize),
|
||||
raw: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
match self.dequeue_input_buffer(Duration::from_millis(10))? {
|
||||
Some(mut input_buffer) => {
|
||||
let mut buf = input_buffer.buffer_mut();
|
||||
|
@ -30,7 +30,7 @@ cfg_if! {
|
||||
}
|
||||
|
||||
pub mod codec;
|
||||
mod convert;
|
||||
pub mod convert;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
pub mod hwcodec;
|
||||
#[cfg(feature = "mediacodec")]
|
||||
|
@ -83,7 +83,7 @@ impl crate::TraitCapturer for Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Frame<'a>(quartz::Frame, PhantomData<&'a [u8]>);
|
||||
pub struct Frame<'a>(pub quartz::Frame, PhantomData<&'a [u8]>);
|
||||
|
||||
impl<'a> ops::Deref for Frame<'a> {
|
||||
type Target = [u8];
|
||||
|
@ -539,15 +539,17 @@ impl Image {
|
||||
self.inner().stride[iplane]
|
||||
}
|
||||
|
||||
pub fn to(&self, fmt: ImageFormat, stride_align: usize, dst: &mut Vec<u8>) {
|
||||
pub fn to(&self, fmt: ImageFormat, stride: usize, dst: &mut Vec<u8>) {
|
||||
let h = self.height();
|
||||
let mut w = self.width();
|
||||
let bps = match fmt {
|
||||
let w = self.width();
|
||||
let bytes_per_pixel = match fmt {
|
||||
ImageFormat::Raw => 3,
|
||||
ImageFormat::ARGB | ImageFormat::ABGR => 4,
|
||||
};
|
||||
w = (w + stride_align - 1) & !(stride_align - 1);
|
||||
dst.resize(h * w * bps, 0);
|
||||
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L128
|
||||
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L129
|
||||
let bytes_per_row = (w * bytes_per_pixel + stride - 1) & !(stride - 1);
|
||||
dst.resize(h * bytes_per_row, 0);
|
||||
let img = self.inner();
|
||||
unsafe {
|
||||
match fmt {
|
||||
@ -560,7 +562,7 @@ impl Image {
|
||||
img.planes[2],
|
||||
img.stride[2],
|
||||
dst.as_mut_ptr(),
|
||||
(w * bps) as _,
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
@ -574,7 +576,7 @@ impl Image {
|
||||
img.planes[2],
|
||||
img.stride[2],
|
||||
dst.as_mut_ptr(),
|
||||
(w * bps) as _,
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
@ -588,7 +590,7 @@ impl Image {
|
||||
img.planes[2],
|
||||
img.stride[2],
|
||||
dst.as_mut_ptr(),
|
||||
(w * bps) as _,
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
|
@ -29,7 +29,7 @@ impl TraitCapturer for Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Frame<'a>(pub(crate) &'a [u8]);
|
||||
pub struct Frame<'a>(pub &'a [u8]);
|
||||
|
||||
impl<'a> ops::Deref for Frame<'a> {
|
||||
type Target = [u8];
|
||||
|
@ -127,15 +127,6 @@ impl MagInterface {
|
||||
};
|
||||
s.init_succeeded = false;
|
||||
unsafe {
|
||||
if GetSystemMetrics(SM_CMONITORS) != 1 {
|
||||
// Do not try to use the magnifier in multi-screen setup (where the API
|
||||
// crashes sometimes).
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Magnifier capturer cannot work on multi-screen system.",
|
||||
));
|
||||
}
|
||||
|
||||
// load lib
|
||||
let lib_file_name = "Magnification.dll";
|
||||
let lib_file_name_c = CString::new(lib_file_name).unwrap();
|
||||
@ -282,10 +273,10 @@ impl CapturerMag {
|
||||
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
if !(origin.0 == x as i32
|
||||
&& origin.1 == y as i32
|
||||
&& width == w as usize
|
||||
&& height == h as usize)
|
||||
if !(origin.0 >= x as i32
|
||||
&& origin.1 >= y as i32
|
||||
&& width <= w as usize
|
||||
&& height <= h as usize)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
@ -518,10 +509,10 @@ impl CapturerMag {
|
||||
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
if !(self.rect.left == x as i32
|
||||
&& self.rect.top == y as i32
|
||||
&& self.rect.right == (x + w) as i32
|
||||
&& self.rect.bottom == (y + h) as i32)
|
||||
if !(self.rect.left >= x as i32
|
||||
&& self.rect.top >= y as i32
|
||||
&& self.rect.right <= (x + w) as i32
|
||||
&& self.rect.bottom <= (y + h) as i32)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
@ -545,8 +536,8 @@ impl CapturerMag {
|
||||
HWND_TOP,
|
||||
self.rect.left,
|
||||
self.rect.top,
|
||||
self.rect.right,
|
||||
self.rect.bottom,
|
||||
self.rect.right - self.rect.left,
|
||||
self.rect.bottom - self.rect.top,
|
||||
0,
|
||||
)
|
||||
{
|
||||
@ -556,8 +547,8 @@ impl CapturerMag {
|
||||
"Failed SetWindowPos (x, y, w , h) - ({}, {}, {}, {}), error {}",
|
||||
self.rect.left,
|
||||
self.rect.top,
|
||||
self.rect.right,
|
||||
self.rect.bottom,
|
||||
self.rect.right - self.rect.left,
|
||||
self.rect.bottom - self.rect.top,
|
||||
GetLastError()
|
||||
),
|
||||
));
|
||||
|
@ -11,6 +11,7 @@
|
||||
<message>Authentication is required to change RustDesk options</message>
|
||||
<message xml:lang="zh_CN">要更改RustDesk选项, 需要您先通过身份验证</message>
|
||||
<message xml:lang="zh_TW">要變更RustDesk選項, 需要您先通過身份驗證</message>
|
||||
<message xml:lang="de">Authentifizierung zum Ändern der RustDesk-Optionen</message>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/share/rustdesk/files/polkit</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
|
||||
<defaults>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user