1430 Commits

368 changed files with 39507 additions and 14873 deletions

View File

@@ -1,20 +0,0 @@
name: 📝 Task
description: Create a task for the team to work on, used internally only. We will delete tasks created by non-team members.
title: "[Task]: "
labels: [Task]
body:
- type: checkboxes
attributes:
label: Are you our team member?
description: If you are not our team member, please go to discussions.
options:
- label: Yes, I am?
required: true
- type: textarea
attributes:
label: SubTasks
placeholder: |
- Sub Task 1
- Sub Task 2
validations:
required: false

View File

@@ -1,13 +1,13 @@
# This yaml shares the build bridge steps with ci and nightly. # This yaml shares the build bridge steps with ci and nightly.
name: Build flutter-rust-bridge name: Build flutter-rust-bridge
# 2023-04-19 15:48:00+00:00 # 2023-11-23 18:00:00+00:00
on: on:
workflow_call: workflow_call:
env: env:
FLUTTER_VERSION: "3.10.6" FLUTTER_VERSION: "3.16.9"
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
jobs: jobs:
generate_bridge: generate_bridge:
@@ -23,21 +23,35 @@ jobs:
} }
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install prerequisites - name: Install prerequisites
run: | run: |
sudo apt install ca-certificates -y sudo apt-get install ca-certificates -y
sudo apt update -y sudo apt-get update -y
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config sudo apt-get install -y \
clang \
cmake \
curl \
gcc \
git \
g++ \
libclang-10-dev \
libclang-dev \
libgtk-3-dev \
llvm-10-dev \
llvm-dev \
nasm \
ninja-build \
pkg-config \
wget
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: stable toolchain: stable
target: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
override: true components: ''
profile: minimal # minimal component installation (ie, no documentation)
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:

View File

@@ -1,8 +1,12 @@
name: CI name: CI
# env: env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0" # MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
on: on:
workflow_dispatch: workflow_dispatch:
@@ -76,36 +80,63 @@ jobs:
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps: steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install prerequisites - name: Install prerequisites
shell: bash shell: bash
run: | run: |
case ${{ matrix.job.target }} in case ${{ matrix.job.target }} in
x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; x86_64-unknown-linux-gnu)
sudo apt-get -y update
sudo apt-get install -y \
clang \
cmake \
curl \
gcc \
git \
g++ \
libasound2-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpulse-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
nasm \
wget
;;
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac esac
- name: Restore from cache and install vcpkg - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v7 uses: lukka/run-vcpkg@v11
with: with:
setupOnly: true vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: '501db0f17ef6df184fcdbfbe0f87cde2313b6ab1' #2023.04.15 vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
run: | run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
shell: bash shell: bash
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: stable toolchain: stable
target: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
override: true components: ''
profile: minimal # minimal component installation (ie, no documentation)
- name: Show version information (Rust, cargo, GCC) - name: Show version information (Rust, cargo, GCC)
shell: bash shell: bash
@@ -117,7 +148,7 @@ jobs:
cargo -V cargo -V
rustc -V rustc -V
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v2
- name: Build - name: Build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -183,7 +214,9 @@ jobs:
;; ;;
esac; esac;
echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} #deprecated echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS}
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_ENV
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
- name: Run tests - name: Run tests
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1

File diff suppressed because it is too large Load Diff

View File

@@ -19,4 +19,3 @@ jobs:
uses: ./.github/workflows/flutter-build.yml uses: ./.github/workflows/flutter-build.yml
with: with:
upload-artifact: false upload-artifact: false

View File

@@ -15,4 +15,24 @@ jobs:
secrets: inherit secrets: inherit
with: with:
upload-artifact: true upload-artifact: true
upload-tag: "1.2.2" upload-tag: ${{ env.GITHUB_REF_NAME }}
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater
runs-on: ubuntu-latest
steps:
- name: Generate RustDesk version file
run: |
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"
files: |
./rustdesk-version.txt

View File

@@ -4,13 +4,11 @@ on: [workflow_dispatch]
env: env:
LLVM_VERSION: "10.0" LLVM_VERSION: "10.0"
FLUTTER_VERSION: "3.10.6" FLUTTER_VERSION: "3.16.9"
TAG_NAME: "tmp" TAG_NAME: "tmp"
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# vcpkg version: 2022.05.10 VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# for multiarch gcc compatibility VERSION: "1.2.4"
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2"
jobs: jobs:
build-for-history-windows: build-for-history-windows:
@@ -23,7 +21,7 @@ jobs:
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 } - { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ matrix.job.ref }} ref: ${{ matrix.job.ref }}
@@ -54,14 +52,18 @@ jobs:
Push-Location flutter ; flutter pub get ; Pop-Location Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
run: | run: |
cd C:\ $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
git clone https://github.com/Kingtous/rustdesk_thirdpary_lib --depth=1 shell: bash
- name: Build rustdesk - name: Build rustdesk
env:
VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
- name: Build self-extracted executable - name: Build self-extracted executable

View File

@@ -1,88 +0,0 @@
name: Build vcpkg dependencies for linux clients
on:
workflow_call:
jobs:
build-vcpkg-deps-linux:
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: true
matrix:
job:
- { arch: armv7, os: ubuntu-20.04 }
- { arch: x86_64, os: ubuntu-20.04 }
- { arch: aarch64, os: ubuntu-20.04 }
steps:
- name: Create vcpkg artifacts folder
run: mkdir -p /opt/artifacts
- name: Cache Vcpkg
id: cache-vcpkg
uses: actions/cache@v3
with:
path: /opt/artifacts
key: vcpkg-${{ matrix.job.arch }}
- uses: Kingtous/run-on-arch-action@amd64-support
name: Run vcpkg install on ${{ matrix.job.arch }}
id: vcpkg
with:
arch: ${{ matrix.job.arch }}
distro: ubuntu18.04
githubToken: ${{ github.token }}
setup: |
ls -l "/opt/artifacts"
dockerRunArgs: |
--volume "/opt/artifacts:/artifacts"
shell: /bin/bash
install: |
apt update -y
case "${{ matrix.job.arch }}" in
x86_64)
# CMake 3.15+
apt install -y gpg wget ca-certificates
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
apt update -y
apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev
cmake --version
gcc -v
;;
aarch64|armv7)
apt install -y curl zip unzip git
esac
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
case "${{ matrix.job.arch }}" in
x86_64)
export VCPKG_FORCE_SYSTEM_BINARIES=1
pushd /artifacts
git clone https://github.com/microsoft/vcpkg.git || true
pushd vcpkg
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
./bootstrap-vcpkg.sh
./vcpkg install libvpx libyuv opus aom
;;
aarch64)
pushd /artifacts
rm -rf rustdesk_thirdparty_lib
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
mkdir -p /artifacts/vcpkg/installed
mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm64-linux /artifacts/vcpkg/installed/arm64-linux
;;
armv7)
pushd /artifacts
rm -rf rustdesk_thirdparty_lib
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
mkdir -p /artifacts/vcpkg/installed
mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm-linux /artifacts/vcpkg/installed/arm-linux
;;
esac
- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: vcpkg-artifact-${{ matrix.job.arch }}
path: |
/opt/artifacts/vcpkg/installed

2
.gitignore vendored
View File

@@ -50,3 +50,5 @@ lib/generated_bridge.dart
.devcontainer/.* .devcontainer/.*
# build cache in examples # build cache in examples
examples/**/target/ examples/**/target/
# ===
vcpkg_installed

3520
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.2" version = "1.2.4"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
description = "A remote control software." description = "A remote control software."
default-run = "rustdesk" default-run = "rustdesk"
rust-version = "1.75"
[lib] [lib]
name = "librustdesk" name = "librustdesk"
@@ -27,11 +28,19 @@ use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"] flutter = ["flutter_rust_bridge"]
default = ["use_dasp"] default = ["use_dasp"]
hwcodec = ["scrap/hwcodec"] hwcodec = ["scrap/hwcodec"]
gpucodec = ["scrap/gpucodec"]
mediacodec = ["scrap/mediacodec"] mediacodec = ["scrap/mediacodec"]
linux_headless = ["pam" ] linux_headless = ["pam" ]
virtual_display_driver = ["virtual_display"] virtual_display_driver = ["virtual_display"]
plugin_framework = [] plugin_framework = []
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"] linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
unix-file-copy-paste = [
"dep:x11-clipboard",
"dep:x11rb",
"dep:percent-encoding",
"dep:once_cell",
"clipboard/unix-file-copy-paste",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -47,13 +56,12 @@ cfg-if = "1.0"
lazy_static = "1.4" lazy_static = "1.4"
sha2 = "0.10" sha2 = "0.10"
repng = "0.2" repng = "0.2"
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" }
runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13 runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" } magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true } rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true } samplerate = { version = "0.2", optional = true }
async-trait = "0.1"
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
clap = "4.2" clap = "4.2"
rpassword = "7.2" rpassword = "7.2"
@@ -62,18 +70,19 @@ num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }
default-net = "0.14" default-net = "0.14"
wol-rs = "1.0" wol-rs = "1.0"
flutter_rust_bridge = { version = "1.75", features = ["uuid"], optional = true} flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
errno = "0.3" errno = "0.3"
rdev = { git = "https://github.com/fufesou/rdev" } rdev = { git = "https://github.com/fufesou/rdev" }
url = { version = "2.3", features = ["serde"] } url = { version = "2.3", features = ["serde"] }
crossbeam-queue = "0.3" crossbeam-queue = "0.3"
hex = "0.4" hex = "0.4"
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls"], default-features=false }
chrono = "0.4" chrono = "0.4"
cidr-utils = "0.5" cidr-utils = "0.5"
libloading = "0.8" libloading = "0.8"
fon = "0.6" fon = "0.6"
zip = "0.6" zip = "0.6"
shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
cpal = "0.15" cpal = "0.15"
@@ -86,17 +95,18 @@ sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] } enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" } clipboard = { path = "libs/clipboard" }
ctrlc = "3.2" ctrlc = "3.2"
arboard = "3.2" arboard = { version = "3.2", features = ["wayland-data-control"] }
system_shutdown = "4.0" system_shutdown = "4.0"
qrcode-generator = "4.1"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi"] } winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
winreg = "0.11" winreg = "0.11"
windows-service = "0.6" windows-service = "0.6"
virtual_display = { path = "libs/virtual_display", optional = true } virtual_display = { path = "libs/virtual_display", optional = true }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" } impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
shutdown_hooks = "0.1" tauri-winrt-notification = "0.1.2"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2" objc = "0.2"
@@ -105,18 +115,27 @@ dispatch = "0.2"
core-foundation = "0.9" core-foundation = "0.9"
core-graphics = "0.22" core-graphics = "0.22"
include_dir = "0.7" include_dir = "0.7"
dark-light = "1.0"
fruitbasket = "0.10" fruitbasket = "0.10"
objc_id = "0.1" objc_id = "0.1"
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
tray-icon = { git = "https://github.com/rustdesk-org/tray-icon" } tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" } tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
image = "0.24" image = "0.24"
[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" } psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" }
@@ -128,10 +147,15 @@ dbus = "0.9"
dbus-crossroads = "0.5" dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam", optional = true } pam = { git="https://github.com/fufesou/pam", optional = true }
users = { version = "0.11" } users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", optional = true}
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"
jni = "0.21" jni = "0.21"
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
[workspace] [workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
@@ -144,12 +168,11 @@ FileDescription = "RustDesk"
[target.'cfg(target_os="windows")'.build-dependencies] [target.'cfg(target_os="windows")'.build-dependencies]
winres = "0.1" winres = "0.1"
winapi = { version = "0.3", features = [ "winnt" ] } winapi = { version = "0.3", features = [ "winnt", "pdh", "synchapi" ] }
[build-dependencies] [build-dependencies]
cc = "1.0" cc = "1.0"
hbb_common = { path = "libs/hbb_common" } hbb_common = { path = "libs/hbb_common" }
flutter_rust_bridge_codegen = "1.75"
os-version = "0.2" os-version = "0.2"
[dev-dependencies] [dev-dependencies]
@@ -169,3 +192,7 @@ panic = 'abort'
strip = true strip = true
#opt-level = 'z' # only have smaller size after strip #opt-level = 'z' # only have smaller size after strip
rpath = true rpath = true
[profile.dev]
split-debuginfo = '...' # Platform-specific.
#strip = "debuginfo"

View File

@@ -1,21 +1,54 @@
FROM debian FROM debian:bullseye-slim
WORKDIR / WORKDIR /
RUN apt update -y && apt install -y g++ gcc git curl nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev cmake ninja-build && rm -rf /var/lib/apt/lists/* ARG DEBIAN_FRONTEND=noninteractive
RUN apt update -y && \
apt install --yes --no-install-recommends \
g++ \
gcc \
git \
curl \
nasm \
yasm \
libgtk-3-dev \
clang \
libxcb-randr0-dev \
libxdo-dev \
libxfixes-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libasound2-dev \
libpulse-dev \
make \
cmake \
unzip \
zip \
sudo \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
ca-certificates \
ninja-build && \
rm -rf /var/lib/apt/lists/*
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics /vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
RUN groupadd -r user && \
useradd -r -g user user --home /home/user && \
mkdir -p /home/user/rustdesk && \
chown -R user: /home/user && \
echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user
RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user
WORKDIR /home/user WORKDIR /home/user
RUN curl -LO https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so RUN curl -LO https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
USER user USER user
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh && \
RUN chmod +x rustup.sh chmod +x rustup.sh && \
RUN ./rustup.sh -y ./rustup.sh -y
USER root USER root
ENV HOME=/home/user ENV HOME=/home/user
COPY ./entrypoint / COPY ./entrypoint.sh /
ENTRYPOINT ["/entrypoint"] ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br> <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>] | [<a href="docs/README-GR.md">Ελληνικά</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>] | [<a href="docs/README-TR.md">Türkçe</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">RustDesk Doc</a> to your native language</b> <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">RustDesk Doc</a> to your native language</b>
</p> </p>
@@ -13,6 +13,8 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -47,7 +49,7 @@ Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
## Dependencies ## Dependencies
Desktop versions use [Sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version.
Please download Sciter dynamic library yourself. Please download Sciter dynamic library yourself.
@@ -83,6 +85,7 @@ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxc
```sh ```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 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) ### Fedora 28 (CentOS 8)
```sh ```sh
@@ -133,34 +136,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Change Wayland to X11 (Xorg)
RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session.
## Wayland support
Wayland does not seem to provide any API for sending keypresses to other windows. Therefore, the RustDesk uses an API from a lower level, namely the `/dev/uinput` device (Linux kernel level).
When Wayland is the controlled side, you have to start in the following way:
```bash
# Start uinput service
$ sudo rustdesk --service
$ rustdesk
```
**Notice**: Wayland screen recording uses different interfaces. RustDesk currently only supports 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
```
## How to build with Docker ## How to build with Docker
Begin by cloning the repository and building the Docker container: Begin by cloning the repository and building the Docker container:
@@ -196,12 +171,13 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[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/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/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 - **[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 - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: file copy and paste implementation for Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: obsolete Sciter UI (deprecated)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections - **[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/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/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 - **[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](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
## Snapshots ## Snapshots

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb - bsdtar -zxvf ../rustdesk-1.2.4.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.2 version: 1.2.4
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb - bsdtar -zxvf ../rustdesk-1.2.4.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.2 version: 1.2.4
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -16,7 +16,7 @@ osx = platform.platform().startswith(
hbb_name = 'rustdesk' + ('.exe' if windows else '') hbb_name = 'rustdesk' + ('.exe' if windows else '')
exe_path = 'target/release/' + hbb_name exe_path = 'target/release/' + hbb_name
if windows: if windows:
flutter_build_dir = 'build/windows/runner/Release/' flutter_build_dir = 'build/windows/x64/runner/Release/'
elif osx: elif osx:
flutter_build_dir = 'build/macos/Build/Products/Release/' flutter_build_dir = 'build/macos/Build/Products/Release/'
else: else:
@@ -24,18 +24,21 @@ else:
flutter_build_dir_2 = f'flutter/{flutter_build_dir}' flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
skip_cargo = False skip_cargo = False
def get_arch() -> str: def get_arch() -> str:
custom_arch = os.environ.get("ARCH") custom_arch = os.environ.get("ARCH")
if custom_arch is None: if custom_arch is None:
return "amd64" return "amd64"
return custom_arch return custom_arch
def system2(cmd): def system2(cmd):
err = os.system(cmd) err = os.system(cmd)
if err != 0: if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.") print(f"Error occurred when executing: {cmd}. Exiting.")
sys.exit(-1) sys.exit(-1)
def get_version(): def get_version():
with open("Cargo.toml", encoding="utf-8") as fh: with open("Cargo.toml", encoding="utf-8") as fh:
for line in fh: for line in fh:
@@ -46,17 +49,11 @@ def get_version():
def parse_rc_features(feature): def parse_rc_features(feature):
available_features = { available_features = {
'IddDriver': {
'platform': ['windows'],
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/RustDeskIddDriver_x64.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/checksum_md5',
'exclude': ['README.md', 'certmgr.exe', 'install_cert_runas_admin.bat', 'RustDeskIddApp.exe'],
},
'PrivacyMode': { 'PrivacyMode': {
'platform': ['windows'], 'platform': ['windows'],
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
'/TempTopMostWindow_x64_pic_en.zip', '/TempTopMostWindow_x64.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
'include': ['WindowInjection.dll'], 'include': ['WindowInjection.dll'],
} }
} }
@@ -109,7 +106,7 @@ def make_parser():
nargs='+', nargs='+',
default='', default='',
help='Integrate features, windows only.' help='Integrate features, windows only.'
'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.') 'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
parser.add_argument('--flutter', action='store_true', parser.add_argument('--flutter', action='store_true',
help='Build flutter package', default=False) help='Build flutter package', default=False)
parser.add_argument( parser.add_argument(
@@ -118,11 +115,21 @@ def make_parser():
help='Enable feature hwcodec' + ( help='Enable feature hwcodec' + (
'' if windows or osx else ', need libva-dev, libvdpau-dev.') '' if windows or osx else ', need libva-dev, libvdpau-dev.')
) )
parser.add_argument(
'--gpucodec',
action='store_true',
help='Enable feature gpucodec, only available on windows now.'
)
parser.add_argument( parser.add_argument(
'--portable', '--portable',
action='store_true', action='store_true',
help='Build windows portable' help='Build windows portable'
) )
parser.add_argument(
'--unix-file-copy-paste',
action='store_true',
help='Build with unix file copy paste feature'
)
parser.add_argument( parser.add_argument(
'--flatpak', '--flatpak',
action='store_true', action='store_true',
@@ -172,8 +179,8 @@ def generate_build_script_for_docker():
export VCPKG_ROOT=`pwd`/vcpkg export VCPKG_ROOT=`pwd`/vcpkg
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
vcpkg/bootstrap-vcpkg.sh vcpkg/bootstrap-vcpkg.sh
vcpkg/vcpkg install libvpx libyuv opus
popd popd
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
# build rustdesk # build rustdesk
./build.py --flutter --hwcodec ./build.py --flutter --hwcodec
''') ''')
@@ -185,6 +192,7 @@ def download_extract_features(features, res_dir):
import re import re
proxy = '' proxy = ''
def req(url): def req(url):
if not proxy: if not proxy:
return url return url
@@ -196,9 +204,9 @@ def download_extract_features(features, res_dir):
for (feat, feat_info) in features.items(): for (feat, feat_info) in features.items():
includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else [] includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else []
includes = [ re.compile(p) for p in includes ] includes = [re.compile(p) for p in includes]
excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else [] excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else []
excludes = [ re.compile(p) for p in excludes ] excludes = [re.compile(p) for p in excludes]
print(f'{feat} download begin') print(f'{feat} download begin')
download_filename = feat_info['zip_url'].split('/')[-1] download_filename = feat_info['zip_url'].split('/')[-1]
@@ -261,10 +269,10 @@ def external_resources(flutter, args, res_dir):
def get_features(args): def get_features(args):
features = ['inline'] if not args.flutter else [] features = ['inline'] if not args.flutter else []
if windows:
features.append('virtual_display_driver')
if args.hwcodec: if args.hwcodec:
features.append('hwcodec') features.append('hwcodec')
if args.gpucodec:
features.append('gpucodec')
if args.flutter: if args.flutter:
features.append('flutter') features.append('flutter')
features.append('flutter_texture_render') features.append('flutter_texture_render')
@@ -272,6 +280,8 @@ def get_features(args):
features.append('flatpak') features.append('flatpak')
if args.appimage: if args.appimage:
features.append('appimage') features.append('appimage')
if args.unix_file_copy_paste:
features.append('unix-file-copy-paste')
print("features:", features) print("features:", features)
return features return features
@@ -350,6 +360,7 @@ def build_flutter_deb(version, features):
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..") os.chdir("..")
def build_deb_from_folder(version, binary_folder): def build_deb_from_folder(version, binary_folder):
os.chdir('flutter') os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/') system2('mkdir -p tmpdeb/usr/bin/')
@@ -388,10 +399,12 @@ def build_deb_from_folder(version, binary_folder):
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..") os.chdir("..")
def build_flutter_dmg(version, features): def build_flutter_dmg(version, features):
if not skip_cargo: if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') system2(
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
# copy dylib # copy dylib
system2( system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
@@ -481,6 +494,7 @@ def main():
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe') system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P') pa = os.environ.get('P')
if pa: if pa:
# https://certera.com/kb/tutorial-guide-for-safenet-authentication-client-for-code-signing/
system2( system2(
f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com '
'target\\release\\rustdesk.exe') 'target\\release\\rustdesk.exe')
@@ -545,13 +559,6 @@ def main():
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# https://github.com/sindresorhus/create-dmg # https://github.com/sindresorhus/create-dmg
system2('/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:
fh.write(txt.replace("</dict>", """
<key>LSUIElement</key>
<string>1</string>
</dict>"""))
pa = os.environ.get('P') pa = os.environ.get('P')
if pa: if pa:
system2(''' system2('''
@@ -564,7 +571,8 @@ def main():
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
'''.format(pa)) '''.format(pa))
system2('create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version) system2(
'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
os.rename('RustDesk %s.dmg' % os.rename('RustDesk %s.dmg' %
version, 'rustdesk-%s.dmg' % version) version, 'rustdesk-%s.dmg' % version)
if pa: if pa:
@@ -584,7 +592,7 @@ def main():
else: else:
print('Not signed') print('Not signed')
else: else:
# buid deb package # build deb package
system2( system2(
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
system2('dpkg-deb -R rustdesk.deb tmpdeb') system2('dpkg-deb -R rustdesk.deb tmpdeb')

View File

@@ -41,7 +41,7 @@ fn build_manifest() {
} }
} }
fn install_oboe() { fn install_android_deps() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os != "android" { if target_os != "android" {
return; return;
@@ -49,6 +49,8 @@ fn install_oboe() {
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "x86_64" { if target_arch == "x86_64" {
target_arch = "x64".to_owned(); target_arch = "x64".to_owned();
} else if target_arch == "x86" {
target_arch = "x86".to_owned();
} else if target_arch == "aarch64" { } else if target_arch == "aarch64" {
target_arch = "arm64".to_owned(); target_arch = "arm64".to_owned();
} else { } else {
@@ -66,62 +68,16 @@ fn install_oboe() {
path.join("lib").to_str().unwrap() path.join("lib").to_str().unwrap()
) )
); );
println!("cargo:rustc-link-lib=ndk_compat");
println!("cargo:rustc-link-lib=oboe"); println!("cargo:rustc-link-lib=oboe");
println!("cargo:rustc-link-lib=oboe_wrapper");
println!("cargo:rustc-link-lib=c++"); println!("cargo:rustc-link-lib=c++");
println!("cargo:rustc-link-lib=OpenSLES"); println!("cargo:rustc-link-lib=OpenSLES");
// I always got some strange link error with oboe, so as workaround, put oboe.cc into oboe src: src/common/AudioStreamBuilder.cpp
// also to avoid libc++_shared not found issue, cp ndk's libc++_shared.so to jniLibs, e.g.
// ./flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/libc++_shared.so
// let include = path.join("include");
//cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper");
}
#[cfg(feature = "flutter")]
fn gen_flutter_rust_bridge() {
if !std::env::var("RUN_FFIGEN").is_ok() {
return;
}
use lib_flutter_rust_bridge_codegen::{
config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts,
};
let llvm_path = match std::env::var("LLVM_HOME") {
Ok(path) => Some(vec![path]),
Err(_) => None,
};
// Tell Cargo that if the given file changes, to rerun this build script.
println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
// Options for frb_codegen
let raw_opts = RawOpts {
// Path of input Rust code
rust_input: vec!["src/flutter_ffi.rs".to_string()],
// Path of output generated Dart code
dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()],
// Path of output generated C header
c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
/// Path to the installed LLVM
llvm_path,
// for other options use defaults
..Default::default()
};
// get opts from raw opts
let configs = config_parse(raw_opts);
// generation of rust api for ffi
let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap();
for config in configs.iter() {
frb_codegen(config, &all_symbols).unwrap();
}
} }
fn main() { fn main() {
hbb_common::gen_version(); hbb_common::gen_version();
install_oboe(); install_android_deps();
// there is problem with cfg(target_os) in build.rs, so use our workaround
// let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
// if target_os == "android" || target_os == "ios" {
#[cfg(feature = "flutter")]
gen_flutter_rust_bridge();
// return;
// }
#[cfg(all(windows, feature = "inline"))] #[cfg(all(windows, feature = "inline"))]
build_manifest(); build_manifest();
#[cfg(windows)] #[cfg(windows)]

101
docs/CODE_OF_CONDUCT-JP.md Normal file
View File

@@ -0,0 +1,101 @@
# コントリビューター規約 行動規範
## 私たちの誓い
私たちは、メンバー、貢献者、リーダーとして、年齢、体格、目に見える・見えない障害、
民族性、性の特徴、性自認と表現、経験のレベル、教育、社会経済的地位、国籍、個人の外見、
人種、宗教、性的自認と指向に関係なく、誰もがハラスメントのないコミュニティに参加できるようにすることを誓います。
私たちは、開かれた、歓迎された、多様で、包容力のある、健全な地域社会に貢献するように行動し、交流することを誓います。
## 私たちの基準
地域社会にとって好ましい環境にコントリビュートする行動の例には、以下のようなものがある:
* 他者への共感と優しさ
* 異なる意見、視点、経験を尊重すること
* 建設的なフィードバックを与え、潔く受け入れること
* 私たちの過ちによって影響を受けた人々に責任を受け入れ、謝罪し、経験から学ぶこと
* 私たち個人にとってだけでなく、地域社会全体にとって何が最善であるかに焦点を合わせること
許されない行為の例:
* 性的な言葉やイメージの使用、性的な注目や誘いかけ
* 荒らし、侮辱的または軽蔑的なコメント、個人的または政治的な攻撃
* 公的または私的な嫌がらせ
* 明示的な許可なく、他人の住所や電子メールアドレスなどの個人情報を公開すること
* 職業上不適切と見なされるその他の行為
## 執行責任
コミュニティリーダーは、許容される行動の基準を明確にし、実施する責任があり、
不適切、脅迫的、攻撃的、または有害と判断される行動に対しては、適切かつ公正な是正措置をとります
コミュニティリーダーは、本行動規範に沿わないコメント、コミット、コード、ウィキ編集、
課題、その他の貢献を削除、編集、拒否する権利と責任を有し、適切な場合にはモデレーション決定の理由を伝えます。
## スコープ
この行動規範は、すべてのコミュニティスペースで適用され、また個人が公的なスペースでコミュニティを公式に代表している場合にも適用されます。
当コミュニティを代表する例としては、公式 E メールアドレスの使用、公式ソーシャルメディアアカウントによる投稿、
オンラインまたはオフラインのイベントでの任命された代表としての行動などが挙げられます。
## 施行
虐待、ハラスメント、その他容認できない行為があった場合は、[info@rustdesk.com](mailto:info@rustdesk.com) の
執行担当コミュニティリーダーに報告することができる。
すべての苦情は、迅速かつ公正に検討・調査されます。
すべての地域社会の指導者は、いかなる事件の報告者のプライバシーと安全を尊重する義務がある。
## 執行ガイドライン
コミュニティリーダーは、本行動規範に違反すると判断した行為に対する結果を決定する際、
以下の「コミュニティへの影響に関するガイドライン」に従います:
### 1. 修正
**コミュニティへの影響**: 不適切な言葉の使用、またはプロフェッショナルでない、あるいは地域社会で歓迎されないとみなされるその他の行動。
**結果**: コミュニティリーダーからの私的な書面による警告。違反の性質と、
なぜその行為が不適切であったのかについての説明を明確にする。公的な謝罪が要求される場合もある。
### 2. 警告
**コミュニティへの影響**: 単一の出来事または一連の行動による違反。
**結果**: 行動を続けた場合の結果を伴う警告。一定期間、行動規範の実施者との勝手な交流を含め、
関係者と交流しないこと。これには、ソーシャルメディアなどの外部チャンネルだけでなく、
コミュニティスペースでの交流を避けることも含まれます。これらの条件に違反した場合、一時的または恒久的に追放される可能性があります。
### 3. 一時的な禁止
**コミュニティへの影響**: 継続的な不適切な行動を含む、コミュニティ基準に対する重大な違反。
**結果**: 一定期間、地域社会とのあらゆる交流や公的なコミュニケーションを一時的に禁止すること。
この期間中は、行動規範を執行する人々との未承諾の交流を含め、関係者との公私にわたる交流は許されない。
これらの条件に違反した場合、永久禁止となる可能性があります。
### 4. 永久禁止
**コミュニティへの影響**: 継続的な不適切な行動、個人に対する嫌がらせ、
または個人クラスに対する攻撃や中傷など、地域社会の基準に対する違反のパターンを示すこと。
**結果**: コミュニティ内でのあらゆる公的交流の永久禁止。
## 帰属
この行動規範は、[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] に掲載されている
[コントリビューター規約][ホームページ]、バージョン 2.0 から引用したものです。
コミュニティインパクトガイドラインは、[Mozilla's code of conduct enforcement ladder][Mozilla CoC] に触発されました。
この行動規範に関するよくある質問については、[https://www.contributor-covenant.org/faq][FAQ] の FAQ をご覧ください。
翻訳は [https://www.contributor-covenant.org/translations][翻訳] にあります。
[ホームページ]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[翻訳]: https://www.contributor-covenant.org/translations

View File

@@ -0,0 +1,89 @@
# Katkıda Bulunanların Davranış Kuralları
## Taahhüdümüz
Biz üyeler, katkıda bulunanlar ve liderler olarak, yaş, beden büyüklüğü, görünür veya görünmez engellilik, etnik köken, cinsiyet özellikleri, cinsiyet kimliği ve ifadesi, deneyim seviyesi, eğitim, sosyo-ekonomik durum, milliyet, kişisel görünüm, ırk, din veya cinsel kimlik ve yönelim ayrımı gözetmeksizin herkes için topluluğumuzdaki katılımı taciz içermeyen bir deneyim haline getirmeyi taahhüt ederiz.
ık, hoşgörülü, çeşitli, kapsayıcı ve sağlıklı bir topluluğa katkıda bulunacak şekillerde hareket etmeyi ve etkileşimde bulunmayı taahhüt ederiz.
## Standartlarımız
Topluluğumuz için olumlu bir ortam yaratmaya katkıda bulunan davranış örnekleri şunlardır:
* Diğer insanlara empati ve nezaket göstermek
* Farklı görüşlere, bakış açılarına ve deneyimlere saygılı olmak
* Yapıcı eleştiriyi vermek ve zarifçe kabul etmek
* Hatalarımızdan etkilenenlere sorumluluk kabul etmek, özür dilemek ve deneyimden öğrenmek
* Sadece bireyler olarak değil, aynı zamanda genel topluluk için en iyisi üzerine odaklanmak
Kabul edilemez davranış örnekleri şunları içerir:
* Cinselleştirilmiş dil veya imgelerin kullanımı ve cinsel ilgi veya herhangi bir türdeki yaklaşımlar
* Trollük, aşağılayıcı veya hakaret içeren yorumlar ve kişisel veya siyasi saldırılar
* Kamuoyu veya özel taciz
* Başkalarının fiziksel veya e-posta adresi gibi özel bilgilerini, açık izinleri olmadan yayınlamak
* Profesyonel bir ortamda makul bir şekilde uygunsuz kabul edilebilecek diğer davranışlar
## Uygulama Sorumlulukları
Topluluk liderleri, kabul edilebilir davranış standartlarımızııklığa kavuşturmak ve uygulamakla sorumludur ve uygunsuz, tehditkar, saldırgan veya zarar verici herhangi bir davranışa yanıt olarak uygun ve adil düzeltici önlemler alacaklardır.
Topluluk liderleri, bu Davranış Kurallarına uyumlu olmayan yorumları, taahhütlerini veya kodu, wiki düzenlemelerini, sorunları ve diğer katkıları kaldırma, düzenleme veya reddetme hakkına sahiptir. Denetim kararlarının nedenlerini uygun olduğunda ileteceklerdir.
## Kapsam
Bu Davranış Kuralları, tüm topluluk alanlarında geçerlidir ve aynı zamanda birey resmi olarak topluluğu halka açık alanlarda temsil ettiğinde de geçerlidir. Topluluğumuzu temsil etme örnekleri, resmi bir e-posta adresi kullanmak, resmi bir sosyal medya hesabı üzerinden gönderi yapmak veya çevrimiçi veya çevrimdışı bir etkinlikte atanmış bir temsilci olarak hareket etmeyi içerir.
## Uygulama
Taciz edici, rahatsız edici veya başka türlü kabul edilemez davranış örnekleri, [info@rustdesk.com](mailto:info@rustdesk.com) adresindeki uygulama sorumlularına bildirilebilir. Tüm şikayetler hızlı ve adil bir şekilde incelenecek ve araştırılacaktır.
Tüm topluluk liderleri, olayın raporlayıcısının gizliliğine ve güvenliğine saygı gösterme yükümlülüğündedir.
## Uygulama Kılavuzları
Topluluk liderleri, bu Davranış Kurallarını ihlal olarak değerlendirdikleri herhangi bir eylem için bu Topluluk Etkisi Kılavuzlarını izleyeceklerdir:
### 1. Düzeltme
**Topluluk Etkisi**: Topluluk içinde profesyonel veya hoşgörülü olmayan uygun olmayan dil veya diğer davranışların kullanımı.
**Sonuç**: Topluluk liderlerinden özel ve yazılı bir uyarı almak, ihlalin niteliği ve davranışın nedeninin açıklığa kavuşturulması. Bir kamu özrü istenebilir.
### 2. Uyarı
**Topluluk Etkisi**: Tek bir olay veya dizi aracılığıyla bir ihlal.
**Sonuç**: Devam eden davranış için sonuçları olan bir uyarı. Topluluk liderleri de dahil olmak üzere ihlalle ilgili kişilerle etkileşim, belirli bir süre boyunca önerilmez. Bu, topluluk alanlarında ve sosyal medya gibi harici kanallarda etkileşimleri içerir. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
### 3. Geçici Yasak
**Topluluk Etkisi**: Sürekli uygunsuz davranış da dahil olmak üzere topluluk standartlarının ciddi bir ihlali.
**Sonuç**: Belirli bir süre için toplulukla herhangi bir türdeki etkileşim veya halka açık iletişimden geçici bir yasak. Bu dönem boyunca, toplul
ukla veya uygulama kurallarını uygulayanlarla her türlü kamuoyu veya özel etkileşim izin verilmez. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
### 4. Kalıcı Yasak
**Topluluk Etkisi**: Topluluk standartlarının ihlalinde sürekli bir desen sergilemek, bireye sürekli olarak uygun olmayan davranışlarda bulunmak, bir bireye tacizde bulunmak veya birey sınıflarına karşı saldırganlık veya aşağılama yapmak.
**Sonuç**: Topluluk içinde her türlü halka açık etkileşimden kalıcı bir yasak.
## Atıf
Bu Davranış Kuralları, [Contributor Covenant][anasayfa], 2.0 sürümünden uyarlanmıştır ve
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] adresinde bulunmaktadır.
Topluluk Etkisi Kılavuzları,
[Mozilla'nın davranış kuralları uygulama merdiveni][Mozilla DK] tarafından ilham alınarak oluşturulmuştur.
Bu davranış kuralları hakkında yaygın soruların cevapları için, SSS'ye göz atın:
[https://www.contributor-covenant.org/faq][SSS]. Çeviriler,
[https://www.contributor-covenant.org/translations][çeviriler] adresinde bulunabilir.
[anasayfa]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla DK]: https://github.com/mozilla/diversity
[SSS]: https://www.contributor-covenant.org/faq
[çeviriler]: https://www.contributor-covenant.org/translations

31
docs/CONTRIBUTING-ID.md Normal file
View File

@@ -0,0 +1,31 @@
# Berkontribusi dalam pengembangan RustDesk
RustDesk mengajak semua orang untuk ikut berkontribusi. Berikut ini adalah panduan jika kamu sedang mempertimbangkan untuk memberikan bantuan kepada kami:
## Kontirbusi
Untuk melakukan kontribusi pada RustDesk atau dependensinya, sebaiknya dilakukan dalam bentuk pull request di GitHub. Setiap permintaan pull request akan ditinjau oleh kontributor utama atau seseorang yang memiliki wewenang untuk menggabungkan perubahan kode, baik yang sudah dimasukkan ke dalam struktur utama ataupun memberikan umpan balik untuk perubahan yang akan diperlukan. Setiap kontribusi harus sesuai dengan format ini, juga termasuk yang berasal dari kontributor utama.
Apabila kamu ingin mengatasi sebuah masalah yang sudah ada di daftar issue, harap klaim terlebih dahulu dengan memberikan komentar pada GitHub issue yang ingin kamu kerjakan. Hal ini dilakukan untuk mencegah terjadinya duplikasi dari kontributor pada daftar issue yang sama.
## Pemeriksaan Pull Request
- Branch yang menjadi acuan adalah branch master dari repositori utama dan, jika diperlukan, lakukan rebase ke branch master yang terbaru sebelum kamu mengirim pull request. Apabila terdapat masalah kita melakukan proses merge ke branch master kemungkinan kamu akan diminta untuk melakukan rebase pada perubahan yang sudah dibuat.
- Sebaiknya buatlah commit seminimal mungkin, sambil memastikan bahwa setiap commit yang dibuat sudah benar (contohnya, setiap commit harus bisa di kompilasi dan berhasil melewati tahap test).
- Setiap commit harus disertai dengan tanda tangan Sertifikat Asal Pengembang (Developer Certificate of Origin) (<http://developercertificate.org>), yang mengindikasikan bahwa kamu (and your employer if applicable) bersedia untuk patuh terhadap persyaratan dari [lisensi projek](../LICENCE). Di git bash, ini adalah opsi parameter `-s` pada `git commit`
- Jika perubahan yang kamu buat tidak mendapat tinjauan atau kamu membutuhkan orang tertentu untuk meninjaunya, kamu bisa @-reply seorang reviewer meminta peninjauan dalam permintaan pull request atau komentar, atau kamu bisa meminta tinjauan melalui [email](mailto:info@rustdesk.com).
- Sertakan test yang relevan terhadap bug atau fitur baru yang sudah dikerjakan.
Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Tindakan
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
## Komunikasi
Kontributor RustDesk sering berkunjung ke [Discord](https://discord.gg/nDceKgxnkV).

41
docs/CONTRIBUTING-JP.md Normal file
View File

@@ -0,0 +1,41 @@
# RustDesk へのコントリビュート
RustDesk は皆さんからのコントリビュートを歓迎します。ご協力いただける方のガイドラインは
以下の通りです:
## コントリビューション
RustDesk またはその依存関係へのコントリビュートは、GitHub のプルリクエストの形で行ってください。
それぞれのプルリクエストは、コアコントリビューター(パッチの適用を許可されている人)によってレビューされ、
メインツリーに適用されるか、必要な変更についてのフィードバックが与えられます。
コアコントリビューターからのものであっても、すべてのコントリビューターはこのフォーマットに従うべきです。
ある issue に取り組みたい場合は、GitHub の issue にコメントすることで、まずその対応を主張してください。
これは、同じ issue に対するコントリビューターの重複作業を防ぐためです。
## プルリクエストのチェックリスト
- master ブランチからブランチし、必要であればプルリクエストを提出する前に現在の master ブランチにリベースしてください。
master と正しくマージできない場合、変更をリベースするよう求められる可能性があります。
- コミットは、各コミットが独立して正しい(すなわち、各コミットがコンパイルされ、テストに合格する)ことを保証しながら、
可能な限り小さくすべきです。
- コミットには、Developer Certificate of Origin (http://developercertificate.org) の sign-off を添えてください。
これは、あなた(および該当する場合はあなたの雇用主)が [プロジェクトのライセンス](../LICENCE) の条項に拘束されることに
同意していることを示すものです。git では、これは `git commit``-s` オプションを使います。
- もしあなたのパッチがレビューされなかったり、特定の人にレビューしてもらう必要がある場合、
プルリクエストやコメントでレビューを依頼するレビュアーに@返信したり、[email](mailto:info@rustdesk.com) でレビューを依頼することができます。
- 修正したバグや新機能に関連するテストを追加する。
具体的なgitの手順については、[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)を参照してください。
## 行動規範
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## コミュニケーション
RustDesk のコントリビューターは、[Discord](https://discord.gg/nDceKgxnkV) を良く使っています。

31
docs/CONTRIBUTING-TR.md Normal file
View File

@@ -0,0 +1,31 @@
# RustDesk'a Katkı Sağlamak
RustDesk, herkesten katkıyı memnuniyetle karşılar. Eğer bize yardımcı olmayı düşünüyorsanız, işte rehberlik eden kurallar:
## Katkılar
RustDesk veya bağımlılıklarına yapılan katkılar, GitHub pull istekleri şeklinde yapılmalıdır. Her bir pull isteği, çekirdek katkıcı tarafından gözden geçirilecek (yamaları kabul etme izni olan biri) ve ana ağaca kabul edilecek veya gerekli değişiklikler için geri bildirim verilecektir. Tüm katkılar bu formata uymalıdır, çekirdek katkıcılardan gelenler bile.
Eğer bir konu üzerinde çalışmak isterseniz, önce üzerinde çalışmak istediğinizi belirten bir yorum yaparak konuyu talep ediniz. Bu, katkı sağlayanların aynı konuda çift çalışmasını engellemek içindir.
## Pull İstek Kontrol Listesi
- Master dalından dallandırın ve gerekiyorsa pull isteğinizi göndermeden önce mevcut master dalına rebase yapın. Eğer master ile temiz bir şekilde birleşmezse, değişikliklerinizi rebase yapmanız istenebilir.
- Her bir commit mümkün olduğunca küçük olmalıdır, ancak her commit'in bağımsız olarak doğru olduğundan emin olun (örneğin, her commit derlenebilir ve testleri geçmelidir).
- Commit'ler, bir Geliştirici Sertifikası ile desteklenmelidir (http://developercertificate.org). Bu, [proje lisansının](../LICENCE) koşullarına uymayı kabul ettiğinizi gösteren bir onaydır. Git'te bunu `git commit` seçeneği olarak `-s` seçeneği ile yapabilirsiniz.
- Yamalarınız gözden geçirilmiyorsa veya belirli bir kişinin gözden geçirmesine ihtiyacınız varsa, çekme isteği veya yorum içinde bir gözden geçirmeyi istemek için bir inceleyiciyi @etiketleyebilir veya inceleme için [e-posta](mailto:info@rustdesk.com) ile talep edebilirsiniz.
- Düzelttiğiniz hatanın veya eklediğiniz yeni özelliğin ilgili testlerini ekleyin.
Daha spesifik git talimatları için, [GitHub iş akışı 101](https://github.com/servo/servo/wiki/GitHub-workflow)'e bakınız.
## Davranış
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-TR.md
## İletişim
RustDesk katkı sağlayıcıları, [Discord](https://discord.gg/nDceKgxnkV) kanalını sık sık ziyaret ederler.

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

@@ -0,0 +1,14 @@
docker コンテナで devcontainer を起動すると、デバッグモードの linux バイナリが作成されます。
現在 devcontainer では、Linux と android のビルドをデバッグモードとリリースモードの両方で提供しています。
以下は、特定のビルドを作成するためにプロジェクトのルートから実行するコマンドの表になります。
コマンド|ビルド タイプ|モード
-|-|-|
`.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

12
docs/DEVCONTAINER-TR.md Normal file
View File

@@ -0,0 +1,12 @@
Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur.
Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır.
Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır.
Komut | Derleme Türü | Mod
-|-|-
`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama
`.devcontainer/build.sh --release linux` | Linux | sürüm
`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama
`.devcontainer/build.sh --release android` | Android-arm64 | sürüm

View File

@@ -118,10 +118,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### X11 (Xorg) إلى Wayland تغيير
افتراضية GNOME session ك Xorg إتبع [هذه](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) الخطوات لإعداد Wayland لا تدعم RustDesk
## Docker طريقة البناء باستخدام ## Docker طريقة البناء باستخدام
ابدأ باستنساخ المستودع وبناء الكونتاينر: ابدأ باستنساخ المستودع وبناء الكونتاينر:

View File

@@ -6,10 +6,10 @@
<a href="#file-structure">Struktura</a> • <a href="#file-structure">Struktura</a> •
<a href="#snapshot">Ukázky</a><br> <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>] | [<a href="README-GR.md">Ελληνικά</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> <b>Potřebujeme Vaši pomoc s překladem tohoto README, <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> </p>
Dopisujte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
@@ -44,7 +44,7 @@ Varianta pro mobilní platformy používá aplikační rámec (framework) Flutte
- Připravte si vývojové prostředí pro jazyky Rust a C++ - Připravte si vývojové prostředí pro jazyky Rust a C++
- Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a nastavte správně proměnnou prostsředí `VCPKG_ROOT` - Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a správně nastavte proměnnou prostředí `VCPKG_ROOT`
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom - Linux/MacOS: vcpkg install libvpx libyuv opus aom
@@ -111,10 +111,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Změna z Wayland na X11 (Xorg)
RustDesk (zatím) nepodporuje zobrazovací server Wayland. Jak nastavit Xorg jako výchozí pro relace v prostředí GNOME naleznete [zde](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/).
## Jak sestavit prostřednictvím Docker kontejnerizace ## Jak sestavit prostřednictvím Docker kontejnerizace
Začněte tím, že si naklonujete tento repozitář a sestavíte docker kontejner: Začněte tím, že si naklonujete tento repozitář a sestavíte docker kontejner:
@@ -131,7 +127,7 @@ Poté pokaždé, když bude třeba aplikaci sestavit, spusťte následující p
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 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
``` ```
Všimněte si, že prvotní sestavení může trvat déle (než se do mezipaměti uloží veškeré softwarové součásti, které jsou potřeba) následná opakování už budou rychlejší. Dále, pokud potřebujete příkazu pro sestavení zadat nějaké argumenty, je možné je zapsat na konec příkazu na pozici `<OPTIONAL-ARGS>`. Například, pokud byste chtěli sestavit optimalizovaně pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vašem systému a bude ho možné spustit pomocí: Všimněte si, že prvotní sestavení může trvat déle (než se do mezipaměti uloží veškeré softwarové součásti, které jsou potřeba) následná opakování už budou rychlejší. Pokud navíc potřebujete zadat různé argumenty příkazu pro sestavení, můžete tak učinit na konci příkazu v pozici `<OPTIONAL-ARGS>`. Například, pokud byste chtěli sestavit optimalizovanou verzi pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vašem systému a bude ho možné spustit pomocí:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
@@ -143,7 +139,7 @@ Nebo, pokud spouštíte variantu pro vydání:
target/release/rustdesk target/release/rustdesk
``` ```
Zajistětě, abyste tyto příkazy spouštěli z kořene repozitáře s RustDesk, jinak aplikace nemusí být schopná nalézt potřebné prostředky (resources). Také si všimněte, že ostatní dílčí príkazy nástroje cargo, jako třeba `install` nebo `run` zatím nejsou prostřednictvím této metody podporovány, protože by vedly k instalaci či spuštění program uvnitř kontejneru namísto přímo v systému. Ujistěte se, že tyto příkazy spouštíte z kořenového adresáře RustDesk, jinak aplikace nemusí být schopná nalézt potřebné prostředky (resources). Také si všimněte, že ostatní dílčí príkazy nástroje cargo, jako třeba `install` nebo `run` zatím nejsou prostřednictvím této metody podporovány, protože by vedly k instalaci či spuštění program uvnitř kontejneru namísto přímo v systému.
## Struktura souborů ## Struktura souborů

View File

@@ -108,33 +108,6 @@ mv libsciter-gtk.so target/debug
cargo run cargo run
``` ```
### Skift Wayland til X11 (Xorg)
RustDesk understøtter ikke Wayland. Tjek [dette](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) for at konfigurere Xorg som standard GNOME-session.
## Wayland-support
Wayland ser ikke ud til at levere nogen API til at sende tastetryk til andre vinduer. Derfor bruger rustdesk et API fra et lavere niveau, nemlig `/dev/uinput`-enheden (Linux-kerneniveau).
Når wayland er den kontrollerede side, skal du starte på følgende måde:
```bash
# Start uinput service
$ sudo rustdesk --service
$ rustdesk
```
**Bemærk**: Wayland-skærmoptagelse bruger forskellige grænseflader. RustDesk understøtter i øjeblikket kun 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
```
## Sådan bygger du med Docker ## Sådan bygger du med Docker
```sh ```sh

View File

@@ -133,34 +133,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Wayland zu X11 (Xorg) ändern
RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen.
## Wayland-Unterstützung
Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene).
Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen:
```bash
# Dienst uinput starten
$ sudo rustdesk --service
$ rustdesk
```
**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur 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
# Keine Unterstützung
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
# Unterstützung
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
variant uint32 4
```
## Auf Docker kompilieren ## Auf Docker kompilieren
Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen: Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen:

View File

@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Ŝanĝi Wayland por X11 (Xorg)
RustDesk ne subtenas Wayland. Kontrolu [tion](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) por agordi Xorg kiel defaŭlta sesio GNOME.
## Kiel kompili kun Docker ## Kiel kompili kun Docker
Komencu klonante la deponejon kaj kompilu la konteneron Docker: Komencu klonante la deponejon kaj kompilu la konteneron Docker:

View File

@@ -113,34 +113,6 @@ mv libsciter-gtk.so target/debug
cargo run cargo run
``` ```
### Cambia Wayland a X11 (Xorg)
RustDesk no soporta Wayland. Lee [esto](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME.
## Soporte para Wayland
Wayland no parece proporcionar ninguna API para enviar pulsaciones de teclas a otras ventanas. Por lo tanto, rustdesk usa una API de nivel bajo, a saber, el dispositivo `/dev/uinput` (a nivel del kernel de Linux).
Cuando wayland esta del lado controlado, hay que iniciar de la siguiente manera:
```bash
# Empezar el servicio uinput
$ sudo rustdesk --service
$ rustdesk
```
**Aviso**: La grabación de pantalla de Wayland utiliza diferentes interfaces. RustDesk actualmente sólo soporta 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
# No soportado
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
# Soportado
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
variant uint32 4
```
## Como compilar con Docker ## Como compilar con Docker
Empieza clonando el repositorio y compilando el contenedor de docker: Empieza clonando el repositorio y compilando el contenedor de docker:

View File

@@ -112,10 +112,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### تغییر Wayland به (X11 (Xorg
راست‌دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید.
## نحوه ساخت با داکر ## نحوه ساخت با داکر
این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید

View File

@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Vaihda Wayland-ympäristö X11 (Xorg)-ympäristöön
RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamalla Xorg oletus GNOME-istuntoon.
## Kuinka rakennetaan Dockerin kanssa ## Kuinka rakennetaan Dockerin kanssa
Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö: Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö:

View File

@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
Exécution du cargo Exécution du cargo
``` ```
### Changer Wayland en X11 (Xorg)
RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) pour configurer Xorg comme la session GNOME par défaut.
## Comment construire avec Docker ## Comment construire avec Docker
Commencez par cloner le dépôt et construire le conteneur Docker : Commencez par cloner le dépôt et construire le conteneur Docker :

View File

@@ -133,34 +133,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run 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
Ξεκινήστε κλωνοποιώντας το αποθετήριο και κάνοντας build το docker container: Ξεκινήστε κλωνοποιώντας το αποθετήριο και κάνοντας build το docker container:
@@ -189,7 +161,7 @@ target/debug/rustdesk
target/release/rustdesk target/release/rustdesk
``` ```
Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αρχική διαδρομή του αποθετηρίου του Rustdesk, διαφορετικά η εφαρμογή ενδέχεται να μην είναι σε θέση να βρεί τους απαιτούμενους πόρους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηρίζονται επί του παρόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το πρόγραμμα εντός του container αντί του κεντρικού υπολογιστή. Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αρχική διαδρομή του αποθετηρίου του RustDesk, διαφορετικά η εφαρμογή ενδέχεται να μην είναι σε θέση να βρεί τους απαιτούμενους πόρους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηρίζονται επί του παρόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το πρόγραμμα εντός του container αντί του κεντρικού υπολογιστή.
## Δομή φακέλων ## Δομή φακέλων

View File

@@ -116,10 +116,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Válts Wayland-ról X11-re (Xorg)
A RustDesk nem támogatja a Waylendet. [Itt](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) található egy tutorial amelynek segítségével beállíthatod a Xorg-ot mint alap GNOME session.
## Hogyan építs Dockerrel ## Hogyan építs Dockerrel
Kezdjünk a repo clónozásával, majd pedig a Docker container megépítésével: Kezdjünk a repo clónozásával, majd pedig a Docker container megépítésével:

View File

@@ -13,15 +13,27 @@ Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Merupakan perangkat lunak Remote Desktop yang baru, dibangun dengan Rust. kamu bisa langsung menggunakannya tanpa perlu konfigurasi tambahan. Serta ,emiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo). [![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) untuk melihat panduan. Merupakan perangkat lunak Remote Desktop yang baru, dan dibangun dengan Rust. Bahkan kamu bisa langsung menggunakannya tanpa perlu melakukan konfigurasi tambahan. Serta memiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING-ID.md`](CONTRIBUTING-ID.md) untuk melihat panduan.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**UNDUH BINARY**](https://github.com/rustdesk/rustdesk/releases) [**UNDUH BINARY**](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)
## Server Publik Gratis ## Server Publik Gratis
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring waktu kemungkinan akan terjadi perubahan spesifikasi pada setiap server. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote. Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
| Lokasi | Penyedia | Spesifikasi | | Lokasi | Penyedia | Spesifikasi |
| --------- | ------------- | ------------------ | | --------- | ------------- | ------------------ |
| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM | | Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
@@ -31,11 +43,11 @@ Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring waktu kemun
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Apabila kamu sudah menginstall VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya. Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
## Dependensi ## Dependensi
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter, tutorial ini hanya berlaku untuk Sciter Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter
Kamu bisa mengunduh Sciter dynamic library disini. Kamu bisa mengunduh Sciter dynamic library disini.
@@ -116,37 +128,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Mengubah Wayland ke X11 (Xorg)
RustDesk tidak mendukung Wayland. Cek [ini](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) untuk mengonfigurasi Xorg sebagai sesi standar di GNOME.
## Kompatibilitas dengan Wayland
Sepertinya Wayland tidak memiliki API untuk mengirimkan ketukan tombol ke jendela lain. Maka dari itu, RustDesk menggunakan API dari level yang lebih rendah, lebih tepatnya perangkat `/dev/uinput` (linux kernel level)
Saat Wayland menjadi sisi yang dikendalikan atau sisi yang sedang diremote, kamu harus memulai dengan cara ini
```bash
# Start uinput service
$ sudo rustdesk --service
$ rustdesk
```
**Harap Diperhatikan**: Saat Perekaman layar menggunakan Wayland antarmuka (UI) yang ditampilkan akan berbeda. Untuk saat ini RustDesk hanya mendukung 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
```
## Cara Build dengan Docker ## Cara Build dengan Docker
Mulailah dengan melakukan kloning (clone) repositori dan build dengan docker container: Mulailah dengan melakukan kloning (clone) repositori dan build dengan docker container:

View File

@@ -109,11 +109,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Cambiare Wayland in X11 (Xorg)
RustDesk non supporta Wayland.
Controlla [qui](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) per configurare Xorg come sessione predefinita di GNOME.
## Come compilare con Docker ## Come compilare con Docker
Clona il repository e compila i container docker: Clona il repository e compila i container docker:

View File

@@ -114,11 +114,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Wayland の場合、X11Xorgに変更します
RustDeskはWaylandをサポートしていません。
[こちら](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) を確認して、XorgをデフォルトのGNOMEセッションとして構成します。
## Dockerでビルドする方法 ## Dockerでビルドする方法
リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。 リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。

View File

@@ -112,10 +112,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run 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 세션을 구성합니다.
## Docker에 빌드하는 방법 ## Docker에 빌드하는 방법
레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. 레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.

View File

@@ -103,10 +103,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### വേലാൻഡ് X11 (Xorg) ആയി മാറ്റുക
RustDesk Wayland-നെ പിന്തുണയ്ക്കുന്നില്ല. സ്ഥിരസ്ഥിതി ഗ്നോം സെഷനായി Xorg കോൺഫിഗർ ചെയ്യുന്നതിന് [ഇത്](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) പരിശോധിക്കുക.
## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം ## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം
റെപ്പോസിറ്റോറി ക്ലോണുചെയ്‌ത് ഡോക്കർ കണ്ടെയ്‌നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക: റെപ്പോസിറ്റോറി ക്ലോണുചെയ്‌ത് ഡോക്കർ കണ്ടെയ്‌നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക:

View File

@@ -130,34 +130,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Wissel van Wayland naar X11 (Xorg)
RustDesk ondersteunt Wayland niet. Lees [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) hoe je Xorg als standaardsessie kunt instellen voor GNOME.
## Wayland support
Wayland lijkt geen API te bieden voor het verzenden van toetsaanslagen naar andere vensters. Daarom gebruikt de rustdesk een API van een lager niveau, namelijk het `/dev/uinput` apparaat (Linux kernel niveau).
Als wayland de gecontroleerde kant is, moet je op de volgende manier beginnen:
```bash
# Start uinput service
$ sudo rustdesk --service
$ rustdesk
```
**Let op**: Wayland schermopname gebruikt verschillende interfaces. RustDesk ondersteunt momenteel alleen 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
```
## Bouwen met Docker ## Bouwen met Docker
Begin met het klonen van de repository en het bouwen van de docker container: Begin met het klonen van de repository en het bouwen van de docker container:

View File

@@ -128,34 +128,6 @@ mv libsciter-gtk.so target/debug
cargo run cargo run
``` ```
### Zmień Wayland na X11 (Xorg)
RustDesk nie obsługuje Waylanda. Sprawdź [tutaj](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), jak skonfigurować Xorg jako domyślną sesję GNOME.
## Wspracie Wayland
Wygląda na to, że Wayland nie wspiera żadnego API do wysyłania naciśnięć klawiszy do innych okien. Dlatego rustdesk używa API z niższego poziomu, urządzenia o nazwie `/dev/uinput` (poziom jądra Linux).
Gdy po stronie kontrolowanej pracuje Wayland, musisz uruchomić program w następujący sposób:
```bash
# Start uinput service
$ sudo rustdesk --service
$ rustdesk
```
**Uwaga**: Nagrywanie ekranu Wayland wykorzystuje różne interfejsy. RustDesk obecnie obsługuje tylko 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
```
## Jak kompilować za pomocą Dockera ## Jak kompilować za pomocą Dockera
Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker: Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker:

View File

@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Mude Wayland para X11 (Xorg)
RustDesk não suporta Wayland. Veja [esse link](https://docs.fedoraproject.org/pt_BR/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar o Xorg como a sessão padrão do GNOME.
## Como compilar com Docker ## Como compilar com Docker
Comece clonando o repositório e montando o container docker: Comece clonando o repositório e montando o container docker:

View File

@@ -114,10 +114,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run 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 по умолчанию.
## Как собрать с помощью Docker ## Как собрать с помощью Docker
Начните с клонирования репозитория и создания docker-контейнера: Начните с клонирования репозитория и создания docker-контейнера:

195
docs/README-TR.md Normal file
View File

@@ -0,0 +1,195 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Uzak masaüstü uygulamanız"><br>
<a href="#free-public-servers">Sunucular</a> •
<a href="#raw-steps-to-build">Derleme</a> •
<a href="#how-to-build-with-docker">Docker ile Derleme</a> •
<a href="#file-structure">Dosya Yapısı</a> •
<a href="#snapshot">Ekran Görüntüleri</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>README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ve <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Belge</a>'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
</p>
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kullanıma hazır, hiçbir yapılandırma gerektirmez. Verilerinizin tam kontrolünü elinizde tutarsınız ve güvenlikle ilgili endişeleriniz olmaz. Kendi buluş/iletme sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi buluş/iletme sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BİNARİ İNDİR**](https://github.com/rustdesk/rustdesk/releases)
[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="F-Droid'de Alın"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Ücretsiz Genel Sunucular
Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
| Konum | Sağlayıcı | Özellikler |
| --------- | ------------- | ------------------ |
| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Geliştirici Konteyneri
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
## Bağımlılıklar
Masaüstü sürümleri GUI için
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
[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)
## Temel Derleme Adımları
- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın.
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` çevresel değişkenini doğru bir şekilde ayarlayın.
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/macOS: vcpkg install libvpx libyuv opus aom
- `cargo run` komutunu çalıştırın.
## [Derleme](https://rustdesk.com/docs/en/dev/build/)
## Linux Üzerinde Derleme Nasıl Yapılır
### 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'yi Yükleyin
```sh
git clone https://github.com/microsoft/vcpkg
cd vcpkg
git checkout 2023.04.15
cd ..
vcpkg/bootstrap-vcpkg.sh
export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### libvpx'i Düzeltin (Fedora için)
```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
```
### Derleme
```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
```
## Docker ile Derleme Nasıl Yapılır
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
docker build -t "rustdesk-builder" .
```
Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
```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
```
İlk derleme, bağımlılıklar önbelleğe alınmadan önce daha uzun sürebilir, sonraki derlemeler daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu
komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
```sh
target/debug/rustdesk
```
Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
```sh
target/release/rustdesk
```
Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır ve ana makinede değil.
## Dosya Yapısı
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodlayıcı, yapılandırma, tcp/udp sarmalayıcı, protobuf, dosya transferi için fs işlevleri ve diğer bazı yardımcı işlevler
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
## Ekran Görüntüleri
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
```

View File

@@ -1,29 +1,31 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Ваш віддалений робочий стіл"><br> <img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> • <a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
<a href="#первинні-кроки-для-складання">Складання</a> • <a href="#кроки-для-збірки">Збирання</a> •
<a href="#як-зібрати-за-допомогою-docker">Docker</a> • <a href="#як-зібрати-за-допомогою-docker">Docker</a> •
<a href="#структура-файлів">Структура</a> • <a href="#структура-файлів">Структура</a> •
<a href="#знімки">Знімки</a><br> <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> [<a href="../README.md">English</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>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
<b>Нам потрібна ваша допомога для перекладу цього README і <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на вашу рідну мову</B> <b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B>
</p> </p>
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Ще одне програмне забезпечення для віддаленого робочого столу, написане на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo). [![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) для допомоги на початку роботи. RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/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) [**НІЧНІ ЗБІРКИ**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid" alt="Get it on F-Droid"
@@ -34,38 +36,40 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно. Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
| Місцезнаходження | Постачальник | Технічні характеристики | | Місцезнаходження | Постачальник | Технічні характеристики |
| --------- | ------------- | ------------------ | | --------- | ------------- | ------------------ |
| Німеччина | Hetzner | 2 VCPU / 4GB RAM | | Німеччина | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | | Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container ## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Якщо у вас уже встановлено VS Code і Docker, ви можете натиснути значок вище, щоб почати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує виcхідний код у том контейнера та розгорне контейнер dev для використання. Якщо у вас уже встановлено VS Code та Docker, ви можете натиснути значок вище, щоб розпочати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує вихідний код у том контейнера та розгорне контейнер dev для використання.
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інфо. Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інформації
## Залежності ## Залежності
Настільні версії використовують [sciter](https://sciter.com/) для графічного інтерфейсу, завантажте динамічну бібліотеку sciter самостійно. Стільничні версії використовують Flutter чи Sciter (застаріле) для графічного інтерфейсу. Ця інструкція лише для Sciter, оскільки він є більш простим та дружнім для початківців. Перегляньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для збірки версії на Flutter.
Будь ласка, завантажте динамічну бібліотеку Sciter самостійно.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
Мобільні версії використовують Flutter. У майбутньому ми перенесемо настільну версію зі Sciter на Flutter. ## Кроки для збірки
## Первинні кроки для складання - Підготуйте середовище розробки Rust і середовище збирання C++.
- Підготуйте середовище розробки Rust і середовище збірки C++.
- Встановіть [vcpkg](https://github.com/microsoft/vcpkg), і правильно встановіть змінну `VCPKG_ROOT`. - Встановіть [vcpkg](https://github.com/microsoft/vcpkg), і правильно встановіть змінну `VCPKG_ROOT`.
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom - Linux/macOS: vcpkg install libvpx libyuv opus aom
- Запустіть `cargo run` - Запустіть `cargo run`
## [Збирання](https://rustdesk.com/docs/en/dev/build/)
## Як зібрати на Linux ## Як зібрати на Linux
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
@@ -81,6 +85,7 @@ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxc
```sh ```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 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) ### Fedora 28 (CentOS 8)
```sh ```sh
@@ -99,7 +104,7 @@ sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-c
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
cd vcpkg cd vcpkg
git checkout 2023.04.15 git checkout 2023.04.15
cd ... cd ..
vcpkg/bootstrap-vcpkg.sh vcpkg/bootstrap-vcpkg.sh
export VCPKG_ROOT=$HOME/vcpkg export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
@@ -118,7 +123,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd cd
``` ```
### Збірка ### Збирання
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -131,10 +136,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run 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 за замовчуванням.
## Як зібрати за допомогою Docker ## Як зібрати за допомогою Docker
Почніть з клонування сховища та створення docker-контейнера: Почніть з клонування сховища та створення docker-контейнера:
@@ -145,7 +146,7 @@ cd rustdesk
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
Потім кожного разу, коли вам потрібно зібрати додаток, запускайте таку команду: Надалі щоразу, коли вам буде потрібно зібрати застосунок, запускайте таку команду:
```sh ```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 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
@@ -170,6 +171,7 @@ target/release/rustdesk
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: відеокодек, конфіг, обгортка tcp/udp, protobuf, функції fs для передавання файлів і деякі інші службові функції - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: відеокодек, конфіг, обгортка tcp/udp, protobuf, функції fs для передавання файлів і деякі інші службові функції
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захоплення екрана - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захоплення екрана
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: специфічне для платформи керування клавіатурою/мишею - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: специфічне для платформи керування клавіатурою/мишею
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень - **[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/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання

View File

@@ -116,10 +116,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
### Chuyển từ Wayland sang X11 (Xorg)
RustDesk hiện không hỗ trợ Wayland. Hãy xem [đường linh ở đây](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) cách để cài đặt Xorg làm session mặc định của GNOME.
## Cách để build sử dụng Docker ## Cách để build sử dụng Docker
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer: Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:

View File

@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](docs/CONTRIBUTING.md). RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -134,39 +134,6 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run 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 session.
## Wayland 支持
Wayland 似乎没有提供任何将按键发送到其他窗口的 API. 因此, RustDesk 使用较低级别的 API, 即 `/dev/uinput` devices (Linux kernal level).
当 Wayland 是受控方时,您必须以下列方式开始操作:
```bash
# Start uinput service
$ sudo rustdesk --service
$ rustdesk
```
**Notice**: 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
```
## 使用 Docker 编译 ## 使用 Docker 编译
克隆版本库并构建 Docker 容器: 克隆版本库并构建 Docker 容器:

9
docs/SECURITY-JP.md Normal file
View File

@@ -0,0 +1,9 @@
# セキュリティポリシー
## 脆弱性の報告
私たちはプロジェクトのセキュリティを非常に重視しています。私たちは、すべてのユーザーが脆弱性を発見した場合、私たちに報告することを奨励しています。
RustDesk プロジェクトにセキュリティの脆弱性を発見した場合は、info@rustdesk.com までメールで責任を持って報告してください。
現時点では、バグ報奨金制度はありません。私たちは大きな問題を解決しようとしている小さなチームです。コミュニティ全体のために安全なアプリケーションを作り続けることができるよう、
責任を持って脆弱性を報告してください。

9
docs/SECURITY-TR.md Normal file
View File

@@ -0,0 +1,9 @@
# Güvenlik Politikası
## Bir Güvenlik Açığı Bildirme
Projemiz için güvenliği çok önemsiyoruz. Kullanıcıların keşfettikleri herhangi bir güvenlik açığını bize bildirmelerini teşvik ediyoruz.
Eğer RustDesk projesinde bir güvenlik açığı bulursanız, lütfen info@rustdesk.com adresine sorumlu bir şekilde bildirin.
Şu an için bir hata ödül programımız bulunmamaktadır. Büyük bir sorunu çözmeye çalışan küçük bir ekibiz. Herhangi bir güvenlik açığını sorumlu bir şekilde bildirmenizi rica ederiz,
böylece tüm topluluk için güvenli bir uygulama oluşturmaya devam edebiliriz.

View File

@@ -1,7 +1,7 @@
{ {
"id": "com.rustdesk.RustDesk", "id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform", "runtime": "org.freedesktop.Platform",
"runtime-version": "21.08", "runtime-version": "23.08",
"sdk": "org.freedesktop.Sdk", "sdk": "org.freedesktop.Sdk",
"command": "rustdesk", "command": "rustdesk",
"icon": "share/icons/hicolor/scalable/apps/rustdesk.svg", "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
@@ -12,7 +12,7 @@
"name": "rustdesk", "name": "rustdesk",
"buildsystem": "simple", "buildsystem": "simple",
"build-commands": [ "build-commands": [
"bsdtar -zxvf rustdesk-1.2.2.deb", "bsdtar -zxvf rustdesk-1.2.4.deb",
"tar -xvf ./data.tar.xz", "tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/", "cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
@@ -26,7 +26,7 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"path": "../rustdesk-1.2.2.deb" "path": "../rustdesk-1.2.4.deb"
}, },
{ {
"type": "file", "type": "file",

View File

@@ -13,4 +13,4 @@ A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials, [online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. samples and guidance on mobile development, and a full API reference.

View File

@@ -1,3 +1,8 @@
import com.google.protobuf.gradle.*
plugins {
id "com.google.protobuf" version "0.9.4"
}
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
@@ -31,10 +36,33 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.20.1'
}
generateProtoTasks {
all().configureEach { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
android { android {
compileSdkVersion 33 compileSdkVersion 33
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
main.proto.srcDirs += '../../../libs/hbb_common/protos'
main.proto.includes += "message.proto"
} }
compileOptions { compileOptions {
@@ -46,7 +74,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb" applicationId "com.carriez.flutter_hbb"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@@ -65,6 +93,7 @@ android {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules'
} }
} }
} }
@@ -75,7 +104,7 @@ flutter {
dependencies { dependencies {
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:16.2' implementation 'com.github.getActivity:XXPermissions:18.5'
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
} }

View File

@@ -0,0 +1,4 @@
# Keep class members from protobuf generated code.
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
<fields>;
}

View File

@@ -61,6 +61,14 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Intent for deep linking-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="rustdesk" />
</intent-filter>
</activity> </activity>
<activity <activity

View File

@@ -10,12 +10,27 @@ import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription
import android.graphics.Path import android.graphics.Path
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.EditText
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.ViewGroup.LayoutParams
import android.view.accessibility.AccessibilityNodeInfo
import android.graphics.Rect
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
import android.view.inputmethod.EditorInfo
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import java.util.* import java.util.*
import java.lang.Character
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import hbb.MessageOuterClass.KeyEvent
import hbb.MessageOuterClass.KeyboardMode
import hbb.KeyEventConverter
const val LIFT_DOWN = 9 const val LIFT_DOWN = 9
const val LIFT_MOVE = 8 const val LIFT_MOVE = 8
@@ -26,6 +41,13 @@ const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331 const val WHEEL_DOWN = 523331
const val WHEEL_UP = 963 const val WHEEL_UP = 963
const val TOUCH_SCALE_START = 1
const val TOUCH_SCALE = 2
const val TOUCH_SCALE_END = 3
const val TOUCH_PAN_START = 4
const val TOUCH_PAN_UPDATE = 5
const val TOUCH_PAN_END = 6
const val WHEEL_STEP = 120 const val WHEEL_STEP = 120
const val WHEEL_DURATION = 50L const val WHEEL_DURATION = 50L
const val LONG_TAP_DELAY = 200L const val LONG_TAP_DELAY = 200L
@@ -51,6 +73,8 @@ class InputService : AccessibilityService() {
private var isWheelActionsPolling = false private var isWheelActionsPolling = false
private var isWaitingLongPress = false private var isWaitingLongPress = false
private var fakeEditTextForTextStateCalculation: EditText? = null
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) { fun onMouseInput(mask: Int, _x: Int, _y: Int) {
val x = max(0, _x) val x = max(0, _x)
@@ -167,6 +191,30 @@ class InputService : AccessibilityService() {
} }
} }
@RequiresApi(Build.VERSION_CODES.N)
fun onTouchInput(mask: Int, _x: Int, _y: Int) {
when (mask) {
TOUCH_PAN_UPDATE -> {
mouseX -= _x * SCREEN_INFO.scale
mouseY -= _y * SCREEN_INFO.scale
mouseX = max(0, mouseX);
mouseY = max(0, mouseY);
continueGesture(mouseX, mouseY)
}
TOUCH_PAN_START -> {
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
startGesture(mouseX, mouseY)
}
TOUCH_PAN_END -> {
endGesture(mouseX, mouseY)
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
}
else -> {}
}
}
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun consumeWheelActions() { private fun consumeWheelActions() {
if (isWheelActionsPolling) { if (isWheelActionsPolling) {
@@ -221,9 +269,296 @@ class InputService : AccessibilityService() {
} }
} }
@RequiresApi(Build.VERSION_CODES.N)
fun onKeyEvent(data: ByteArray) {
val keyEvent = KeyEvent.parseFrom(data)
val keyboardMode = keyEvent.getMode()
var textToCommit: String? = null
if (keyboardMode == KeyboardMode.Legacy) {
if (keyEvent.hasChr() && keyEvent.getDown()) {
val chr = keyEvent.getChr()
if (chr != null) {
textToCommit = String(Character.toChars(chr))
}
}
} else if (keyboardMode == KeyboardMode.Translate) {
if (keyEvent.hasSeq() && keyEvent.getDown()) {
val seq = keyEvent.getSeq()
if (seq != null) {
textToCommit = seq
}
}
}
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
if (Build.VERSION.SDK_INT >= 33) {
getInputMethod()?.let { inputMethod ->
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
if (textToCommit != null) {
textToCommit?.let { text ->
inputConnection.commitText(text, 1, null)
}
} else {
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
inputConnection.sendKeyEvent(event)
}
}
}
}
} else {
val handler = Handler(Looper.getMainLooper())
handler.post {
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
val possibleNodes = possibleAccessibiltyNodes()
Log.d(logTag, "possibleNodes:$possibleNodes")
for (item in possibleNodes) {
val success = trySendKeyEvent(event, item, textToCommit)
if (success) {
break
}
}
}
}
}
}
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
if (node == null) {
return
}
if (list.contains(node)) {
return
}
list.add(node)
}
private fun findChildNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
if (node == null) {
return null
}
if (node.isEditable() && node.isFocusable()) {
return node
}
val childCount = node.getChildCount()
for (i in 0 until childCount) {
val child = node.getChild(i)
if (child != null) {
if (child.isEditable() && child.isFocusable()) {
return child
}
if (Build.VERSION.SDK_INT < 33) {
child.recycle()
}
}
}
for (i in 0 until childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findChildNode(child)
if (Build.VERSION.SDK_INT < 33) {
if (child != result) {
child.recycle()
}
}
if (result != null) {
return result
}
}
}
return null
}
private fun possibleAccessibiltyNodes(): LinkedList<AccessibilityNodeInfo> {
val linkedList = LinkedList<AccessibilityNodeInfo>()
val latestList = LinkedList<AccessibilityNodeInfo>()
val focusInput = findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
var focusAccessibilityInput = findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
val rootInActiveWindow = getRootInActiveWindow()
Log.d(logTag, "focusInput:$focusInput focusAccessibilityInput:$focusAccessibilityInput rootInActiveWindow:$rootInActiveWindow")
if (focusInput != null) {
if (focusInput.isFocusable() && focusInput.isEditable()) {
insertAccessibilityNode(linkedList, focusInput)
} else {
insertAccessibilityNode(latestList, focusInput)
}
}
if (focusAccessibilityInput != null) {
if (focusAccessibilityInput.isFocusable() && focusAccessibilityInput.isEditable()) {
insertAccessibilityNode(linkedList, focusAccessibilityInput)
} else {
insertAccessibilityNode(latestList, focusAccessibilityInput)
}
}
val childFromFocusInput = findChildNode(focusInput)
Log.d(logTag, "childFromFocusInput:$childFromFocusInput")
if (childFromFocusInput != null) {
insertAccessibilityNode(linkedList, childFromFocusInput)
}
val childFromFocusAccessibilityInput = findChildNode(focusAccessibilityInput)
if (childFromFocusAccessibilityInput != null) {
insertAccessibilityNode(linkedList, childFromFocusAccessibilityInput)
}
Log.d(logTag, "childFromFocusAccessibilityInput:$childFromFocusAccessibilityInput")
if (rootInActiveWindow != null) {
insertAccessibilityNode(linkedList, rootInActiveWindow)
}
for (item in latestList) {
insertAccessibilityNode(linkedList, item)
}
return linkedList
}
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
node.refresh()
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
this.fakeEditTextForTextStateCalculation?.setText(null)
val text = node.getText()
var isShowingHint = false
if (Build.VERSION.SDK_INT >= 26) {
isShowingHint = node.isShowingHintText()
}
var textSelectionStart = node.textSelectionStart
var textSelectionEnd = node.textSelectionEnd
if (text != null) {
if (textSelectionStart > text.length) {
textSelectionStart = text.length
}
if (textSelectionEnd > text.length) {
textSelectionEnd = text.length
}
if (textSelectionStart > textSelectionEnd) {
textSelectionStart = textSelectionEnd
}
}
var success = false
Log.d(logTag, "existing text:$text textToCommit:$textToCommit textSelectionStart:$textSelectionStart textSelectionEnd:$textSelectionEnd")
if (textToCommit != null) {
if ((textSelectionStart == -1) || (textSelectionEnd == -1)) {
val newText = textToCommit
this.fakeEditTextForTextStateCalculation?.setText(newText)
success = updateTextForAccessibilityNode(node)
} else if (text != null) {
this.fakeEditTextForTextStateCalculation?.setText(text)
this.fakeEditTextForTextStateCalculation?.setSelection(
textSelectionStart,
textSelectionEnd
)
this.fakeEditTextForTextStateCalculation?.text?.insert(textSelectionStart, textToCommit)
success = updateTextAndSelectionForAccessibiltyNode(node)
}
} else {
if (isShowingHint) {
this.fakeEditTextForTextStateCalculation?.setText(null)
} else {
this.fakeEditTextForTextStateCalculation?.setText(text)
}
if (textSelectionStart != -1 && textSelectionEnd != -1) {
Log.d(logTag, "setting selection $textSelectionStart $textSelectionEnd")
this.fakeEditTextForTextStateCalculation?.setSelection(
textSelectionStart,
textSelectionEnd
)
}
this.fakeEditTextForTextStateCalculation?.let {
// This is essiential to make sure layout object is created. OnKeyDown may not work if layout is not created.
val rect = Rect()
node.getBoundsInScreen(rect)
it.layout(rect.left, rect.top, rect.right, rect.bottom)
it.onPreDraw()
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
val succ = it.onKeyDown(event.getKeyCode(), event)
Log.d(logTag, "onKeyDown $succ")
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
val success = it.onKeyUp(event.getKeyCode(), event)
Log.d(logTag, "keyup $success")
} else {}
}
success = updateTextAndSelectionForAccessibiltyNode(node)
}
return success
}
fun updateTextForAccessibilityNode(node: AccessibilityNodeInfo): Boolean {
var success = false
this.fakeEditTextForTextStateCalculation?.text?.let {
val arguments = Bundle()
arguments.putCharSequence(
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
it.toString()
)
success = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
}
return success
}
fun updateTextAndSelectionForAccessibiltyNode(node: AccessibilityNodeInfo): Boolean {
var success = updateTextForAccessibilityNode(node)
if (success) {
val selectionStart = this.fakeEditTextForTextStateCalculation?.selectionStart
val selectionEnd = this.fakeEditTextForTextStateCalculation?.selectionEnd
if (selectionStart != null && selectionEnd != null) {
val arguments = Bundle()
arguments.putInt(
AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT,
selectionStart
)
arguments.putInt(
AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT,
selectionEnd
)
success = node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments)
Log.d(logTag, "Update selection to $selectionStart $selectionEnd success:$success")
}
}
return success
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
}
override fun onServiceConnected() { override fun onServiceConnected() {
super.onServiceConnected() super.onServiceConnected()
ctx = this ctx = this
val info = AccessibilityServiceInfo()
if (Build.VERSION.SDK_INT >= 33) {
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
} else {
info.flags = FLAG_RETRIEVE_INTERACTIVE_WINDOWS
}
setServiceInfo(info)
fakeEditTextForTextStateCalculation = EditText(this)
// Size here doesn't matter, we won't show this view.
fakeEditTextForTextStateCalculation?.layoutParams = LayoutParams(100, 100)
fakeEditTextForTextStateCalculation?.onPreDraw()
val layout = fakeEditTextForTextStateCalculation?.getLayout()
Log.d(logTag, "fakeEditTextForTextStateCalculation layout:$layout")
Log.d(logTag, "onServiceConnected!") Log.d(logTag, "onServiceConnected!")
} }
@@ -232,7 +567,5 @@ class InputService : AccessibilityService() {
super.onDestroy() super.onDestroy()
} }
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
override fun onInterrupt() {} override fun onInterrupt() {}
} }

View File

@@ -0,0 +1,118 @@
package hbb;
import android.view.KeyEvent
import android.view.KeyCharacterMap
import hbb.MessageOuterClass.KeyboardMode
import hbb.MessageOuterClass.ControlKey
object KeyEventConverter {
fun toAndroidKeyEvent(keyEventProto: hbb.MessageOuterClass.KeyEvent): KeyEvent {
var chrValue = 0
var modifiers = 0
val keyboardMode = keyEventProto.getMode()
if (keyEventProto.hasChr()) {
if (keyboardMode == KeyboardMode.Map || keyboardMode == KeyboardMode.Translate) {
chrValue = keyEventProto.getChr()
} else {
chrValue = convertUnicodeToKeyCode(keyEventProto.getChr() as Int)
}
} else if (keyEventProto.hasControlKey()) {
chrValue = convertControlKeyToKeyCode(keyEventProto.getControlKey())
}
var modifiersList = keyEventProto.getModifiersList()
if (modifiersList != null) {
for (modifier in keyEventProto.getModifiersList()) {
val modifierValue = convertModifier(modifier)
modifiers = modifiers or modifierValue
}
}
var action = 0
if (keyEventProto.getDown()) {
action = KeyEvent.ACTION_DOWN
} else {
action = KeyEvent.ACTION_UP
}
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
}
private fun convertModifier(controlKey: hbb.MessageOuterClass.ControlKey): Int {
// Add logic to map ControlKey values to Android KeyEvent key codes.
// You'll need to provide the mapping for each key.
return when (controlKey) {
ControlKey.Alt -> KeyEvent.META_ALT_ON
ControlKey.Control -> KeyEvent.META_CTRL_ON
ControlKey.CapsLock -> KeyEvent.META_CAPS_LOCK_ON
ControlKey.Meta -> KeyEvent.META_META_ON
ControlKey.NumLock -> KeyEvent.META_NUM_LOCK_ON
ControlKey.RShift -> KeyEvent.META_SHIFT_RIGHT_ON
ControlKey.Shift -> KeyEvent.META_SHIFT_ON
ControlKey.RAlt -> KeyEvent.META_ALT_RIGHT_ON
ControlKey.RControl -> KeyEvent.META_CTRL_RIGHT_ON
else -> 0 // Default to unknown.
}
}
private val tag = "KeyEventConverter"
private fun convertUnicodeToKeyCode(unicode: Int): Int {
val charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD)
android.util.Log.d(tag, "unicode: $unicode")
val events = charMap.getEvents(charArrayOf(unicode.toChar()))
if (events != null && events.size > 0) {
android.util.Log.d(tag, "keycode ${events[0].keyCode}")
return events[0].keyCode
}
return 0
}
private fun convertControlKeyToKeyCode(controlKey: hbb.MessageOuterClass.ControlKey): Int {
// Add logic to map ControlKey values to Android KeyEvent key codes.
// You'll need to provide the mapping for each key.
return when (controlKey) {
ControlKey.Alt -> KeyEvent.KEYCODE_ALT_LEFT
ControlKey.Backspace -> KeyEvent.KEYCODE_DEL
ControlKey.Control -> KeyEvent.KEYCODE_CTRL_LEFT
ControlKey.CapsLock -> KeyEvent.KEYCODE_CAPS_LOCK
ControlKey.Meta -> KeyEvent.KEYCODE_META_LEFT
ControlKey.NumLock -> KeyEvent.KEYCODE_NUM_LOCK
ControlKey.RShift -> KeyEvent.KEYCODE_SHIFT_RIGHT
ControlKey.Shift -> KeyEvent.KEYCODE_SHIFT_LEFT
ControlKey.RAlt -> KeyEvent.KEYCODE_ALT_RIGHT
ControlKey.RControl -> KeyEvent.KEYCODE_CTRL_RIGHT
ControlKey.DownArrow -> KeyEvent.KEYCODE_DPAD_DOWN
ControlKey.LeftArrow -> KeyEvent.KEYCODE_DPAD_LEFT
ControlKey.RightArrow -> KeyEvent.KEYCODE_DPAD_RIGHT
ControlKey.UpArrow -> KeyEvent.KEYCODE_DPAD_UP
ControlKey.End -> KeyEvent.KEYCODE_MOVE_END
ControlKey.Home -> KeyEvent.KEYCODE_MOVE_HOME
ControlKey.PageUp -> KeyEvent.KEYCODE_PAGE_UP
ControlKey.PageDown -> KeyEvent.KEYCODE_PAGE_DOWN
ControlKey.Insert -> KeyEvent.KEYCODE_INSERT
ControlKey.Escape -> KeyEvent.KEYCODE_ESCAPE
ControlKey.F1 -> KeyEvent.KEYCODE_F1
ControlKey.F2 -> KeyEvent.KEYCODE_F2
ControlKey.F3 -> KeyEvent.KEYCODE_F3
ControlKey.F4 -> KeyEvent.KEYCODE_F4
ControlKey.F5 -> KeyEvent.KEYCODE_F5
ControlKey.F6 -> KeyEvent.KEYCODE_F6
ControlKey.F7 -> KeyEvent.KEYCODE_F7
ControlKey.F8 -> KeyEvent.KEYCODE_F8
ControlKey.F9 -> KeyEvent.KEYCODE_F9
ControlKey.F10 -> KeyEvent.KEYCODE_F10
ControlKey.F11 -> KeyEvent.KEYCODE_F11
ControlKey.F12 -> KeyEvent.KEYCODE_F12
ControlKey.Space -> KeyEvent.KEYCODE_SPACE
ControlKey.Tab -> KeyEvent.KEYCODE_TAB
ControlKey.Return -> KeyEvent.KEYCODE_ENTER
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
else -> 0 // Default to unknown.
}
}
}

View File

@@ -44,7 +44,6 @@ import java.nio.ByteBuffer
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
const val DEFAULT_NOTIFY_TITLE = "RustDesk" const val DEFAULT_NOTIFY_TITLE = "RustDesk"
const val DEFAULT_NOTIFY_TEXT = "Service is running" const val DEFAULT_NOTIFY_TEXT = "Service is running"
const val DEFAULT_NOTIFY_ID = 1 const val DEFAULT_NOTIFY_ID = 1
@@ -71,20 +70,35 @@ class MainService : Service() {
@Keep @Keep
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun rustMouseInput(mask: Int, x: Int, y: Int) { fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off // turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && mask == LIFT_DOWN) { if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
Log.d(logTag,"Turn on Screen, WakeLock release") Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release() wakeLock.release()
} }
Log.d(logTag,"Turn on Screen") Log.d(logTag,"Turn on Screen")
wakeLock.acquire(5000) wakeLock.acquire(5000)
} else { } else {
InputService.ctx?.onMouseInput(mask,x,y) when (kind) {
"touch" -> {
InputService.ctx?.onTouchInput(mask, x, y)
}
"mouse" -> {
InputService.ctx?.onMouseInput(mask, x, y)
}
else -> {
}
}
} }
} }
@Keep
@RequiresApi(Build.VERSION_CODES.N)
fun rustKeyEventInput(input: ByteArray) {
InputService.ctx?.onKeyEvent(input)
}
@Keep @Keep
fun rustGetByName(name: String): String { fun rustGetByName(name: String): String {
return when (name) { return when (name) {
@@ -197,6 +211,7 @@ class MainService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(logTag,"MainService onCreate") Log.d(logTag,"MainService onCreate")
init(this)
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start() start()
serviceLooper = looper serviceLooper = looper
@@ -301,7 +316,6 @@ class MainService : Service() {
mediaProjection = mediaProjection =
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
checkMediaPermission() checkMediaPermission()
init(this)
_isReady = true _isReady = true
} ?: let { } ?: let {
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection") Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")

View File

@@ -1,5 +1,6 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowsChanged" android:accessibilityEventTypes="typeWindowsChanged"
android:canRetrieveWindowContent="true"
android:accessibilityFlags="flagDefault" android:accessibilityFlags="flagDefault"
android:notificationTimeout="50" android:notificationTimeout="50"
android:description="@string/accessibility_service_description" android:description="@string/accessibility_service_description"

View File

@@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.9.10'
repositories { repositories {
google() google()
jcenter() jcenter()

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="236" preserveAspectRatio="xMidYMid"><path fill="#e24329" d="m128.075 236.075 47.104-144.97H80.97z"/><path fill="#fc6d26" d="M128.075 236.074 80.97 91.104H14.956z"/><path fill="#fca326" d="M14.956 91.104.642 135.16a9.752 9.752 0 0 0 3.542 10.903l123.891 90.012z"/><path fill="#e24329" d="M14.956 91.105H80.97L52.601 3.79c-1.46-4.493-7.816-4.492-9.275 0z"/><path fill="#fc6d26" d="m128.075 236.074 47.104-144.97h66.015z"/><path fill="#fca326" d="m241.194 91.104 14.314 44.056a9.752 9.752 0 0 1-3.543 10.903l-123.89 90.012z"/><path fill="#e24329" d="M241.194 91.105h-66.015l28.37-87.315c1.46-4.493 7.816-4.492 9.275 0z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696255389449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1922" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M435.2 704c-9 0-17.8-3.8-23.8-10.6l-115.2-128c-11.8-13.2-10.8-33.4 2.4-45.2 13.2-11.8 33.4-10.8 45.2 2.4l90.6 100.6 245.2-291.8c11.4-13.6 31.6-15.2 45-4 13.6 11.4 15.2 31.6 4 45l-268.8 320c-6 7-14.6 11.2-24 11.4-0.2 0.2-0.4 0.2-0.6 0.2z" p-id="1923"></path><path d="M800 928H224c-70.6 0-128-57.4-128-128V224c0-70.6 57.4-128 128-128h576c70.6 0 128 57.4 128 128v576c0 70.6-57.4 128-128 128zM224 160c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64H224z" p-id="1924"></path></svg>

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696245886035" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4133" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 132.717714c-9.435429 0-18.852571 3.84-29.147429 12.434286L194.011429 379.574857c-7.277714 6.418286-11.556571 15.433143-11.556572 28.288 0 22.272 16.713143 39.003429 39.424 39.003429 8.996571 0 18.432-3.437714 28.288-11.154286L512 222.281143l261.851429 213.430857c9.874286 7.716571 19.291429 11.154286 28.708571 11.154286 22.308571 0 39.003429-16.731429 39.003429-39.003429 0-12.854857-4.278857-21.869714-11.556572-28.288L541.147429 144.713143c-10.294857-8.137143-19.291429-11.995429-29.147429-11.995429z m0 758.564572c9.856 0 18.852571-3.84 29.147429-11.995429L829.988571 644.425143c7.277714-6.418286 11.556571-15.433143 11.556572-28.288 0-22.272-16.713143-39.424-38.985143-39.424-9.435429 0-18.870857 3.858286-28.708571 11.574857L512 801.718857 250.148571 588.288c-9.874286-7.716571-19.291429-11.574857-28.288-11.574857-22.710857 0-39.424 17.152-39.424 39.424 0 12.854857 4.278857 21.869714 11.556572 28.288l288.859428 234.422857c10.294857 8.594286 19.712 12.434286 29.147429 12.434286z" p-id="4134"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694049173782" class="icon" viewBox="0 0 1024 1024" width="24" height="24" fill="#fff" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="992" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M891.64 184.73H620.41c-27.41 0-54.41-7.77-77.32-22.5L428.13 87.36C402.77 71 372.91 62 342.64 62H131.95C93.5 62 62 93.5 62 132.36v759.68C62 930.91 93.5 962 131.95 962h759.68c38.86 0 70.36-31.09 70.36-69.96V255.09c0.01-38.86-31.49-70.36-70.35-70.36zM480.5 753.77c0 16.77-13.5 30.68-30.68 30.68-16.77 0-30.68-13.91-30.68-30.68V523.04l-31.91 55.64c-8.59 14.32-27.41 19.64-42.14 11.04-14.32-8.59-19.64-27.41-11.05-41.73l89.18-154.64c6.96-12.27 21.27-18 34.77-14.32 13.09 3.27 22.5 15.55 22.5 29.45v345.29z m209.04-139.5l-89.18 154.64c-5.32 9.82-15.55 15.55-26.59 15.55-2.46 0-5.32-0.41-7.77-1.23-13.5-3.68-22.91-15.55-22.91-29.46V408.5c0-16.77 13.91-30.68 30.68-30.68 17.18 0 30.68 13.91 30.68 30.68v230.73l31.91-55.64c8.59-14.73 27.41-19.64 42.14-11.05 14.73 8.6 19.64 27.01 11.04 41.73z" p-id="993"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
flutter/assets/scam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

View File

@@ -1,13 +1,17 @@
#!/bin/bash #!/bin/bash
# Build libyuv / opus / libvpx / oboe for Android set -e -o pipefail
ANDROID_ABI=$1
# Build RustDesk dependencies for Android using vcpkg.json
# Required: # Required:
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
# 2. vcpkg initialized # 2. vcpkg initialized
# 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`) # 3. ndk, version: r25c or newer
if [ -z "$ANDROID_NDK" ]; then if [ -z "$ANDROID_NDK_HOME" ]; then
echo "Failed! Please set ANDROID_NDK" echo "Failed! Please set ANDROID_NDK_HOME"
exit 1 exit 1
fi fi
@@ -18,107 +22,66 @@ fi
API_LEVEL="21" API_LEVEL="21"
# Get directory of this script
SCRIPTDIR="$(readlink -f "$0")"
SCRIPTDIR="$(dirname "$SCRIPTDIR")"
# Check if vcpkg.json is one level up - in root directory of RD
if [ ! -f "$SCRIPTDIR/../vcpkg.json" ]; then
echo "Failed! Please check where vcpkg.json is!"
exit 1
fi
# NDK llvm toolchain # NDK llvm toolchain
HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/` HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/`
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
function build { function build {
ANDROID_ABI=$1 ANDROID_ABI=$1
VCPKG_TARGET=$2
NDK_LLVM_TARGET=$3
LIBVPX_TARGET=$4
PREFIX=$VCPKG_ROOT/installed/$VCPKG_TARGET/ case "$ANDROID_ABI" in
arm64-v8a)
ABI=aarch64-linux-android$API_LEVEL
VCPKG_TARGET=arm64-android
;;
armeabi-v7a)
ABI=armv7a-linux-androideabi$API_LEVEL
VCPKG_TARGET=arm-neon-android
;;
x86_64)
ABI=x86_64-linux-android$API_LEVEL
VCPKG_TARGET=x64-android
;;
x86)
ABI=i686-linux-android$API_LEVEL
VCPKG_TARGET=x86-android
;;
*)
echo "ERROR: ANDROID_ABI must be one of: arm64-v8a, armeabi-v7a, x86_64, x86" >&2
return 1
esac
# 1 echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies"
echo "*** [$ANDROID_ABI][Start] Build opus / libyuv from vcpkg" pushd "$SCRIPTDIR/.."
export ANDROID_NDK_HOME=$ANDROID_NDK $VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
pushd $VCPKG_ROOT
$VCPKG_ROOT/vcpkg install opus --triplet $VCPKG_TARGET
$VCPKG_ROOT/vcpkg install libyuv --triplet $VCPKG_TARGET
popd popd
echo "*** [$ANDROID_ABI][Finished] Build opus / libyuv from vcpkg" echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
# 2 if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
echo "*** [$ANDROID_ABI][Start] Build libvpx" echo "*** [Start] Move arm-neon-android to arm-android"
pushd build/libvpx
export AR=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ar
export AS=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-as
export LD=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ld.gold # if ndk < 22, use aarch64-linux-android-ld
export RANLIB=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ranlib
export STRIP=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-strip
if [ $NDK_LLVM_TARGET == "arm-linux-androideabi" ] mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android"
then
export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi${API_LEVEL}-clang
export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi${API_LEVEL}-clang++
else
export CC=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}${API_LEVEL}-clang
export CXX=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}${API_LEVEL}-clang++
fi
make clean
./configure --target=$LIBVPX_TARGET \
--enable-pic
--disable-webm-io \
--disable-unit-tests \
--disable-examples \
--disable-libyuv \
--disable-postproc \
--disable-tools \
--disable-docs \
--prefix=$PREFIX
make -j5
make install
popd echo "*** [Finished] Move arm-neon-android to arm-android"
echo "*** [$ANDROID_ABI][Finished] Build libvpx" fi
# 3
echo "*** [$ANDROID_ABI][Start] Build oboe"
pushd build/oboe
make clean
cmake -DBUILD_SHARED_LIBS=true \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DANDROID_TOOLCHAIN=clang \
-DANDROID_STL=c++_shared \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DANDROID_ABI=$ANDROID_ABI \
-DANDROID_PLATFORM=android-$API_LEVEL
make -j5
make install
mv $PREFIX/lib/$ANDROID_ABI/liboboe.a $PREFIX/lib/
popd
echo "*** [$ANDROID_ABI][Finished] Build oboe"
echo "*** [$ANDROID_ABI][All Finished]"
} }
git clone -b v1.11.0 --depth=1 https://github.com/webmproject/libvpx.git build/libvpx if [ ! -z "$ANDROID_ABI" ]; then
git clone -b 1.6.1 --depth=1 https://github.com/google/oboe build/oboe build "$ANDROID_ABI"
patch -N -d build/oboe -p1 < ../src/oboe.patch else
echo "Usage: build-android-deps.sh <ANDROID-ABI>" >&2
# VCPKG_TARGET ANDROID_ABI exit 1
# arm64-android arm64-v8a fi
# arm-android armeabi-v7a
# x64-android x86_64
# x86-android x86
# NDK_LLVM_TARGET
# aarch64-linux-android
# arm-linux-androideabi
# x86_64-linux-android
# i686-linux-android
# LIBVPX_TARGET :
# arm64-android-gcc
# armv7-android-gcc
# x86_64-android-gcc
# x86-android-gcc
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
# rm -rf build/libvpx
# rm -rf build/oboe

View File

@@ -1,2 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info # https://docs.flutter.dev/deployment/ios
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# no obfuscate, because no easy to check errors
flutter build ipa --release

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>11.0</string> <string>12.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,10 +1,10 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '11.0' # platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
platform :ios, '11.0' platform :ios, '12.0'
project 'Runner', { project 'Runner', {
'Debug' => :debug, 'Debug' => :debug,

View File

@@ -38,9 +38,6 @@ PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_keyboard_visibility (0.0.1): - flutter_keyboard_visibility (0.0.1):
- Flutter - Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- MTBBarcodeScanner (5.0.11) - MTBBarcodeScanner (5.0.11)
@@ -52,12 +49,12 @@ PODS:
- qr_code_scanner (0.2.0): - qr_code_scanner (0.2.0):
- Flutter - Flutter
- MTBBarcodeScanner - MTBBarcodeScanner
- SDWebImage (5.15.5): - SDWebImage (5.18.11):
- SDWebImage/Core (= 5.15.5) - SDWebImage/Core (= 5.18.11)
- SDWebImage/Core (5.15.5) - SDWebImage/Core (5.18.11)
- sqflite (0.0.2): - sqflite (0.0.3):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FlutterMacOS
- SwiftyGif (5.4.4) - SwiftyGif (5.4.4)
- uni_links (0.0.1): - uni_links (0.0.1):
- Flutter - Flutter
@@ -65,7 +62,8 @@ PODS:
- Flutter - Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- wakelock (0.0.1): - FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
@@ -75,19 +73,18 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`)
- uni_links (from `.symlinks/plugins/uni_links/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock (from `.symlinks/plugins/wakelock/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- FMDB
- MTBBarcodeScanner - MTBBarcodeScanner
- SDWebImage - SDWebImage
- SwiftyGif - SwiftyGif
@@ -106,41 +103,40 @@ EXTERNAL SOURCES:
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios" :path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner: qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios" :path: ".symlinks/plugins/qr_code_scanner/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/darwin"
uni_links: uni_links:
:path: ".symlinks/plugins/uni_links/ios" :path: ".symlinks/plugins/uni_links/ios"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock: wakelock_plus:
:path: ".symlinks/plugins/wakelock/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: ce3938a0df3cc1ef404671531facef740d03f920 file_picker: ce3938a0df3cc1ef404671531facef740d03f920
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
COCOAPODS: 1.12.0 COCOAPODS: 1.12.1

View File

@@ -159,7 +159,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1300; LastUpgradeCheck = 1430;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@@ -208,6 +208,7 @@
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Thin Binary";
outputPaths = ( outputPaths = (
@@ -346,7 +347,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -393,8 +394,6 @@
"-framework", "-framework",
"\"DKPhotoGallery\"", "\"DKPhotoGallery\"",
"-framework", "-framework",
"\"FMDB\"",
"-framework",
"\"Foundation\"", "\"Foundation\"",
"-framework", "-framework",
"\"ImageIO\"", "\"ImageIO\"",
@@ -433,10 +432,11 @@
"-framework", "-framework",
"\"video_player_avfoundation\"", "\"video_player_avfoundation\"",
"-framework", "-framework",
"\"wakelock\"", "\"wakelock_plus\"",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -491,7 +491,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -541,7 +541,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -590,8 +590,6 @@
"-framework", "-framework",
"\"DKPhotoGallery\"", "\"DKPhotoGallery\"",
"-framework", "-framework",
"\"FMDB\"",
"-framework",
"\"Foundation\"", "\"Foundation\"",
"-framework", "-framework",
"\"ImageIO\"", "\"ImageIO\"",
@@ -630,10 +628,11 @@
"-framework", "-framework",
"\"video_player_avfoundation\"", "\"video_player_avfoundation\"",
"-framework", "-framework",
"\"wakelock\"", "\"wakelock_plus\"",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -679,8 +678,6 @@
"-framework", "-framework",
"\"DKPhotoGallery\"", "\"DKPhotoGallery\"",
"-framework", "-framework",
"\"FMDB\"",
"-framework",
"\"Foundation\"", "\"Foundation\"",
"-framework", "-framework",
"\"ImageIO\"", "\"ImageIO\"",
@@ -719,10 +716,11 @@
"-framework", "-framework",
"\"video_player_avfoundation\"", "\"video_player_avfoundation\"",
"-framework", "-framework",
"\"wakelock\"", "\"wakelock_plus\"",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1300" LastUpgradeVersion = "1430"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@@ -13,9 +13,7 @@ import Flutter
} }
public func dummyMethodToEnforceBundling() { public func dummyMethodToEnforceBundling() {
get_rgba(); dummy_method_to_enforce_bundling();
// free_rgba(nil); session_get_rgba(nil, 0);
// get_by_name("", "");
// set_by_name("", "");
} }
} }

View File

@@ -1,122 +1,122 @@
{ {
"images": [ "images" : [
{ {
"filename": "Icon-App-20x20@2x.png", "filename" : "Icon-App-20x20@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-20x20@3x.png", "filename" : "Icon-App-20x20@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29@1x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "1x", "scale" : "1x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-29x29@3x.png", "filename" : "Icon-App-29x29@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-40x40@2x.png", "filename" : "Icon-App-40x40@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-40x40@3x.png", "filename" : "Icon-App-40x40@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-60x60@2x.png", "filename" : "Icon-App-60x60@2x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "60x60" "size" : "60x60"
}, },
{ {
"filename": "Icon-App-60x60@3x.png", "filename" : "Icon-App-60x60@3x.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "60x60" "size" : "60x60"
}, },
{ {
"filename": "Icon-App-20x20@1x.png", "filename" : "Icon-App-20x20@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-20x20@2x.png", "filename" : "Icon-App-20x20@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "Icon-App-40x40@1x.png", "filename" : "Icon-App-40x40@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-40x40@2x.png", "filename" : "Icon-App-40x40@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "Icon-App-76x76@1x.png", "filename" : "Icon-App-76x76@1x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "1x",
"size": "76x76" "size" : "76x76"
}, },
{ {
"filename": "Icon-App-76x76@2x.png", "filename" : "Icon-App-76x76@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "76x76" "size" : "76x76"
}, },
{ {
"filename": "Icon-App-83.5x83.5@2x.png", "filename" : "Icon-App-83.5x83.5@2x.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "83.5x83.5" "size" : "83.5x83.5"
}, },
{ {
"filename": "Icon-App-1024x1024@1x.png", "filename" : "Icon-App-1024x1024@1x.png",
"idiom": "ios-marketing", "idiom" : "ios-marketing",
"scale": "1x", "scale" : "1x",
"size": "1024x1024" "size" : "1024x1024"
} }
], ],
"info": { "info" : {
"author": "icons_launcher", "author" : "xcode",
"version": 1 "version" : 1
} }
} }

View File

@@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="48" y="-2"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

View File

@@ -24,6 +24,21 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLIconFile</key>
<string></string>
<key>CFBundleURLName</key>
<string>com.carriez.rustdesk</string>
<key>CFBundleURLSchemes</key>
<array>
<string>rustdesk</string>
</array>
</dict>
</array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>

View File

@@ -1,3 +1,3 @@
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import "ffi.h" #import "bridge_generated.h"

View File

@@ -1,4 +0,0 @@
void* get_rgba();
void free_rgba(void*);
void set_by_name(const char*, const char*);
const char* get_by_name(const char*, const char*);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
@@ -10,9 +11,11 @@ class HttpType {
static const kAuthReqTypeMobile = "mobile"; static const kAuthReqTypeMobile = "mobile";
static const kAuthReqTypeSMSCode = "sms_code"; static const kAuthReqTypeSMSCode = "sms_code";
static const kAuthReqTypeEmailCode = "email_code"; static const kAuthReqTypeEmailCode = "email_code";
static const kAuthReqTypeTfaCode = "tfa_code";
static const kAuthResTypeToken = "access_token"; static const kAuthResTypeToken = "access_token";
static const kAuthResTypeEmailCheck = "email_check"; static const kAuthResTypeEmailCheck = "email_check";
static const kAuthResTypeTfaCheck = "tfa_check";
} }
enum UserStatus { kDisabled, kNormal, kUnverified } enum UserStatus { kDisabled, kNormal, kUnverified }
@@ -48,11 +51,18 @@ class UserPayload {
}; };
return map; return map;
} }
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
};
return map;
}
} }
class PeerPayload { class PeerPayload {
String id = ''; String id = '';
String info = ''; Map<String, dynamic> info = {};
int? status; int? status;
String user = ''; String user = '';
String user_name = ''; String user_name = '';
@@ -60,14 +70,45 @@ class PeerPayload {
PeerPayload.fromJson(Map<String, dynamic> json) PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '', : id = json['id'] ?? '',
info = json['info'] ?? '', info = (json['info'] is Map<String, dynamic>) ? json['info'] : {},
status = json['status'], status = json['status'],
user = json['user'] ?? '', user = json['user'] ?? '',
user_name = json['user_name'] ?? '', user_name = json['user_name'] ?? '',
note = json['note'] ?? ''; note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) { static Peer toPeer(PeerPayload p) {
return Peer.fromJson({"id": p.id, "username": p.user_name}); return Peer.fromJson({
"id": p.id,
'loginName': p.user_name,
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
});
}
static String? _platform(dynamic field) {
if (field == null) {
return null;
}
final fieldStr = field.toString();
List<String> list = fieldStr.split(' / ');
if (list.isEmpty) return null;
final os = list[0];
switch (os.toLowerCase()) {
case 'windows':
return kPeerPlatformWindows;
case 'linux':
return kPeerPlatformLinux;
case 'macos':
return kPeerPlatformMacOS;
case 'android':
return kPeerPlatformAndroid;
default:
if (fieldStr.toLowerCase().contains('linux')) {
return kPeerPlatformLinux;
}
return null;
}
} }
} }
@@ -79,6 +120,8 @@ class LoginRequest {
bool? autoLogin; bool? autoLogin;
String? type; String? type;
String? verificationCode; String? verificationCode;
String? tfaCode;
String? secret;
LoginRequest( LoginRequest(
{this.username, {this.username,
@@ -87,7 +130,9 @@ class LoginRequest {
this.uuid, this.uuid,
this.autoLogin, this.autoLogin,
this.type, this.type,
this.verificationCode}); this.verificationCode,
this.tfaCode,
this.secret});
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
@@ -100,6 +145,8 @@ class LoginRequest {
if (verificationCode != null) { if (verificationCode != null) {
data['verificationCode'] = verificationCode; data['verificationCode'] = verificationCode;
} }
if (tfaCode != null) data['tfaCode'] = tfaCode;
if (secret != null) data['secret'] = secret;
Map<String, dynamic> deviceInfo = {}; Map<String, dynamic> deviceInfo = {};
try { try {
@@ -115,13 +162,18 @@ class LoginRequest {
class LoginResponse { class LoginResponse {
String? access_token; String? access_token;
String? type; String? type;
String? tfa_type;
String? secret;
UserPayload? user; UserPayload? user;
LoginResponse({this.access_token, this.type, this.user}); LoginResponse(
{this.access_token, this.type, this.tfa_type, this.secret, this.user});
LoginResponse.fromJson(Map<String, dynamic> json) { LoginResponse.fromJson(Map<String, dynamic> json) {
access_token = json['access_token']; access_token = json['access_token'];
type = json['type']; type = json['type'];
tfa_type = json['tfa_type'];
secret = json['secret'];
user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; user = json['user'] != null ? UserPayload.fromJson(json['user']) : null;
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:flutter_hbb/common.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../consts.dart'; import '../consts.dart';
@@ -10,7 +11,7 @@ class PrivacyModeState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered(tag: key)) {
final RxBool state = false.obs; final RxString state = ''.obs;
Get.put(state, tag: key); Get.put(state, tag: key);
} }
} }
@@ -20,11 +21,11 @@ class PrivacyModeState {
if (Get.isRegistered(tag: key)) { if (Get.isRegistered(tag: key)) {
Get.delete(tag: key); Get.delete(tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxString>(tag: key).value = '';
} }
} }
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id)); static RxString find(String id) => Get.find<RxString>(tag: tag(id));
} }
class BlockInputState { class BlockInputState {
@@ -318,6 +319,7 @@ initSharedStates(String id) {
FingerprintState.init(id); FingerprintState.init(id);
PeerBoolOption.init(id, 'zoom-cursor', () => false); PeerBoolOption.init(id, 'zoom-cursor', () => false);
UnreadChatCountState.init(id); UnreadChatCountState.init(id);
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
} }
removeSharedStates(String id) { removeSharedStates(String id) {
@@ -330,4 +332,5 @@ removeSharedStates(String id) {
FingerprintState.delete(id); FingerprintState.delete(id);
PeerBoolOption.delete(id, 'zoom-cursor'); PeerBoolOption.delete(id, 'zoom-cursor');
UnreadChatCountState.delete(id); UnreadChatCountState.delete(id);
if (isMobile) ConnectionTypeState.delete(id);
} }

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
@@ -7,6 +10,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import '../../common.dart'; import '../../common.dart';
import 'dialog.dart'; import 'dialog.dart';
@@ -34,7 +38,7 @@ class _AddressBookState extends State<AddressBook> {
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
if (gFFI.userModel.userName.value.isEmpty) { if (!gFFI.userModel.isLogin) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
@@ -48,11 +52,13 @@ class _AddressBookState extends State<AddressBook> {
children: [ children: [
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
if (gFFI.abModel.retrying.value) LinearProgressIndicator(), if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
_buildErrorBanner( buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pullError, err: gFFI.abModel.pullError,
retry: null, retry: null,
close: () => gFFI.abModel.pullError.value = ''), close: () => gFFI.abModel.pullError.value = ''),
_buildErrorBanner( buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pushError, err: gFFI.abModel.pushError,
retry: () => gFFI.abModel.pushAb(isRetry: true), retry: () => gFFI.abModel.pushAb(isRetry: true),
close: () => gFFI.abModel.pushError.value = ''), close: () => gFFI.abModel.pushError.value = ''),
@@ -65,61 +71,6 @@ class _AddressBookState extends State<AddressBook> {
} }
}); });
Widget _buildErrorBanner(
{required RxString err,
required Function? retry,
required Function close}) {
const double height = 25;
return Obx(() => Offstage(
offstage: !(!gFFI.abModel.abLoading.value && err.value.isNotEmpty),
child: Center(
child: Container(
height: height,
color: Color.fromARGB(255, 253, 238, 235),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FittedBox(
child: Icon(
Icons.info,
color: Color.fromARGB(255, 249, 81, 81),
),
).marginAll(4),
Flexible(
child: Align(
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
if (retry != null)
InkWell(
onTap: () {
retry.call();
},
child: Text(
translate("Retry"),
style: TextStyle(color: MyTheme.accent),
)).marginSymmetric(horizontal: 5),
FittedBox(
child: InkWell(
onTap: () {
close.call();
},
child: Icon(Icons.close).marginSymmetric(horizontal: 5),
),
).marginAll(4)
],
),
)).marginOnly(bottom: 14),
));
}
Widget _buildAddressBookDesktop() { Widget _buildAddressBookDesktop() {
return Row( return Row(
children: [ children: [
@@ -208,20 +159,31 @@ class _AddressBookState extends State<AddressBook> {
} else { } else {
tags = gFFI.abModel.tags; tags = gFFI.abModel.tags;
} }
return Wrap( tagBuilder(String e) {
children: tags return AddressBookTag(
.map((e) => AddressBookTag( name: e,
name: e, tags: gFFI.abModel.selectedTags,
tags: gFFI.abModel.selectedTags, onTap: () {
onTap: () { if (gFFI.abModel.selectedTags.contains(e)) {
if (gFFI.abModel.selectedTags.contains(e)) { gFFI.abModel.selectedTags.remove(e);
gFFI.abModel.selectedTags.remove(e); } else {
} else { gFFI.abModel.selectedTags.add(e);
gFFI.abModel.selectedTags.add(e); }
} });
})) }
.toList(),
); final gridView = DynamicGridView.builder(
shrinkWrap: isMobile,
gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length,
itemBuilder: (BuildContext context, int index) {
final e = tags[index];
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView);
}); });
} }
@@ -229,11 +191,10 @@ class _AddressBookState extends State<AddressBook> {
return Expanded( return Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.abModel.peers,
initPeers: gFFI.abModel.peers.value, )),
))),
); );
} }
@@ -268,6 +229,22 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
@protected
MenuEntryBase<String> filterMenuItem() {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate('Filter by intersection'),
getter: () async {
return filterAbTagByIntersection();
},
setter: (bool v) async {
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
gFFI.abModel.filterByIntersection.value = v;
},
dismissOnClicked: true,
);
}
void _showMenu(RelativeRect pos) { void _showMenu(RelativeRect pos) {
final items = [ final items = [
getEntry(translate("Add ID"), abAddId), getEntry(translate("Add ID"), abAddId),
@@ -275,6 +252,7 @@ class _AddressBookState extends State<AddressBook> {
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(), sortMenuItem(),
syncMenuItem(), syncMenuItem(),
filterMenuItem(),
]; ];
mod_menu.showMenu( mod_menu.showMenu(
@@ -513,7 +491,7 @@ class AddressBookTag extends StatelessWidget {
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: tags.contains(name) color: tags.contains(name)
? str2color2(name, 0xFF) ? gFFI.abModel.getTagColor(name)
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(4)), borderRadius: BorderRadius.circular(4)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
@@ -528,7 +506,7 @@ class AddressBookTag extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: tags.contains(name) color: tags.contains(name)
? Colors.white ? Colors.white
: str2color2(name)), : gFFI.abModel.getTagColor(name)),
).marginOnly(right: radius / 2), ).marginOnly(right: radius / 2),
Expanded( Expanded(
child: Text(name, child: Text(name,
@@ -568,6 +546,30 @@ class AddressBookTag extends StatelessWidget {
Future.delayed(Duration.zero, () => Get.back()); Future.delayed(Duration.zero, () => Get.back());
}); });
}), }),
getEntry(translate(translate('Change Color')), () async {
final model = gFFI.abModel;
Color oldColor = model.getTagColor(name);
Color newColor = await showColorPickerDialog(
context,
oldColor,
pickersEnabled: {
ColorPickerType.accent: false,
ColorPickerType.wheel: true,
},
pickerTypeLabels: {
ColorPickerType.primary: translate("Primary Color"),
ColorPickerType.wheel: translate("HSV Color"),
},
actionButtons: ColorPickerActionButtons(
dialogOkButtonLabel: translate("OK"),
dialogCancelButtonLabel: translate("Cancel")),
showColorCode: true,
);
if (oldColor != newColor) {
model.setTagColor(name, newColor);
model.pushAb();
}
}),
getEntry(translate("Delete"), () { getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name); gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb(); gFFI.abModel.pushAb();

View File

@@ -0,0 +1,205 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import '../../../models/platform_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
Future<List<Peer>> getAllPeers() async {
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
Map<String, dynamic> abPeers = jsonDecode(bind.mainLoadAbSync());
Map<String, dynamic> groupPeers = jsonDecode(bind.mainLoadGroupSync());
Map<String, dynamic> combinedPeers = {};
void mergePeers(Map<String, dynamic> peers) {
if (peers.containsKey("peers")) {
dynamic peerData = peers["peers"];
if (peerData is String) {
try {
peerData = jsonDecode(peerData);
} catch (e) {
print("Error decoding peers: $e");
return;
}
}
if (peerData is List) {
for (var peer in peerData) {
if (peer is Map && peer.containsKey("id")) {
String id = peer["id"];
if (!combinedPeers.containsKey(id)) {
combinedPeers[id] = peer;
}
}
}
}
}
}
mergePeers(recentPeers);
mergePeers(lanPeers);
mergePeers(abPeers);
mergePeers(groupPeers);
List<Peer> parsedPeers = [];
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
}
return parsedPeers;
}
class AutocompletePeerTile extends StatefulWidget {
final VoidCallback onSelect;
final Peer peer;
const AutocompletePeerTile({
Key? key,
required this.onSelect,
required this.peer,
}) : super(key: key);
@override
AutocompletePeerTileState createState() => AutocompletePeerTileState();
}
class AutocompletePeerTileState extends State<AutocompletePeerTile> {
List _frontN<T>(List list, int n) {
if (list.length <= n) {
return list;
} else {
return list.sublist(0, n);
}
}
@override
Widget build(BuildContext context) {
final double tileRadius = 5;
final name =
'${widget.peer.username}${widget.peer.username.isNotEmpty && widget.peer.hostname.isNotEmpty ? '@' : ''}${widget.peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = GestureDetector(
onTap: () => widget.onSelect(),
child: Padding(
padding: EdgeInsets.only(left: 5, right: 5),
child: Container(
height: 42,
margin: EdgeInsets.only(bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color(
'${widget.peer.id}${widget.peer.platform}', 0x7f),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(tileRadius),
bottomLeft: Radius.circular(tileRadius),
),
),
alignment: Alignment.center,
width: 42,
height: null,
child: Padding(
padding: EdgeInsets.all(6),
child: getPlatformImage(widget.peer.platform,
size: 30))),
Expanded(
child: Container(
padding: EdgeInsets.only(left: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(tileRadius),
bottomRight: Radius.circular(tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Container(
margin: EdgeInsets.only(top: 2),
child: Container(
margin: EdgeInsets.only(top: 2),
child: Column(
children: [
Container(
margin:
EdgeInsets.only(top: 2),
child: Row(children: [
getOnline(
8, widget.peer.online),
Expanded(
child: Text(
widget.peer.alias.isEmpty
? formatID(
widget.peer.id)
: widget.peer.alias,
overflow:
TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall,
)),
widget.peer.alias.isNotEmpty
? Padding(
padding:
const EdgeInsets
.only(
left: 5,
right: 5),
child: Text(
"(${widget.peer.id})",
style: greyStyle,
overflow:
TextOverflow
.ellipsis,
))
: Container(),
])),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow:
TextOverflow.ellipsis,
),
),
],
)))),
],
)),
)
],
))));
final colors = _frontN(widget.peer.tags, 25)
.map((e) => gFFI.abModel.getTagColor(e))
.toList();
return Tooltip(
message: isMobile
? ''
: widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
if (colors.isNotEmpty)
Positioned(
top: 5,
right: 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
)
]),
);
}
}

View File

@@ -1,10 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
@@ -302,6 +306,53 @@ Future<String> changeDirectAccessPort(
return controller.text; return controller.text;
} }
Future<String> changeAutoDisconnectTimeout(String old) async {
final controller = TextEditingController(text: old);
await gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("Timeout in minutes")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8.0),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '10',
isCollapsed: true,
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
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])$')),
],
controller: controller,
autofocus: true),
),
],
),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async {
await bind.mainSetOption(
key: 'auto-disconnect-timeout', value: controller.text);
close();
}),
],
onCancel: close,
);
});
return controller.text;
}
class DialogTextField extends StatelessWidget { class DialogTextField extends StatelessWidget {
final String title; final String title;
final String? hintText; final String? hintText;
@@ -312,6 +363,8 @@ class DialogTextField extends StatelessWidget {
final Widget? suffixIcon; final Widget? suffixIcon;
final TextEditingController controller; final TextEditingController controller;
final FocusNode? focusNode; final FocusNode? focusNode;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
static const kUsernameTitle = 'Username'; static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined); static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -327,6 +380,8 @@ class DialogTextField extends StatelessWidget {
this.prefixIcon, this.prefixIcon,
this.suffixIcon, this.suffixIcon,
this.hintText, this.hintText,
this.keyboardType,
this.inputFormatters,
required this.title, required this.title,
required this.controller}) required this.controller})
: super(key: key); : super(key: key);
@@ -351,6 +406,8 @@ class DialogTextField extends StatelessWidget {
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
), ),
), ),
], ],
@@ -358,17 +415,260 @@ class DialogTextField extends StatelessWidget {
} }
} }
abstract class ValidationField extends StatelessWidget {
ValidationField({Key? key}) : super(key: key);
String? validate();
bool get isReady;
}
class Dialog2FaField extends ValidationField {
Dialog2FaField({
Key? key,
required this.controller,
this.autoFocus = true,
this.reRequestFocus = false,
this.title,
this.hintText,
this.errorText,
this.readyCallback,
this.onChanged,
}) : super(key: key);
final TextEditingController controller;
final bool autoFocus;
final bool reRequestFocus;
final String? title;
final String? hintText;
final String? errorText;
final VoidCallback? readyCallback;
final VoidCallback? onChanged;
final errMsg = translate('2FA code must be 6 digits.');
@override
Widget build(BuildContext context) {
return DialogVerificationCodeField(
title: title ?? translate('2FA code'),
controller: controller,
errorText: errorText,
autoFocus: autoFocus,
reRequestFocus: reRequestFocus,
hintText: hintText,
readyCallback: readyCallback,
onChanged: _onChanged,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
);
}
String get text => controller.text;
bool get isAllDigits => text.codeUnits.every((e) => e >= 48 && e <= 57);
@override
bool get isReady => text.length == 6 && isAllDigits;
@override
String? validate() => isReady ? null : errMsg;
_onChanged(StateSetter setState, SimpleWrapper<String?> errText) {
onChanged?.call();
if (text.length > 6) {
setState(() => errText.value = errMsg);
return;
}
if (!isAllDigits) {
setState(() => errText.value = errMsg);
return;
}
if (isReady) {
readyCallback?.call();
return;
}
if (errText.value != null) {
setState(() => errText.value = null);
}
}
}
class DialogEmailCodeField extends ValidationField {
DialogEmailCodeField({
Key? key,
required this.controller,
this.autoFocus = true,
this.reRequestFocus = false,
this.hintText,
this.errorText,
this.readyCallback,
this.onChanged,
}) : super(key: key);
final TextEditingController controller;
final bool autoFocus;
final bool reRequestFocus;
final String? hintText;
final String? errorText;
final VoidCallback? readyCallback;
final VoidCallback? onChanged;
final errMsg = translate('Email verification code must be 6 characters.');
@override
Widget build(BuildContext context) {
return DialogVerificationCodeField(
title: translate('Verification code'),
controller: controller,
errorText: errorText,
autoFocus: autoFocus,
reRequestFocus: reRequestFocus,
hintText: hintText,
readyCallback: readyCallback,
helperText: translate('verification_tip'),
onChanged: _onChanged,
keyboardType: TextInputType.visiblePassword,
);
}
String get text => controller.text;
@override
bool get isReady => text.length == 6;
@override
String? validate() => isReady ? null : errMsg;
_onChanged(StateSetter setState, SimpleWrapper<String?> errText) {
onChanged?.call();
if (text.length > 6) {
setState(() => errText.value = errMsg);
return;
}
if (isReady) {
readyCallback?.call();
return;
}
if (errText.value != null) {
setState(() => errText.value = null);
}
}
}
class DialogVerificationCodeField extends StatefulWidget {
DialogVerificationCodeField({
Key? key,
required this.controller,
required this.title,
this.autoFocus = true,
this.reRequestFocus = false,
this.helperText,
this.hintText,
this.errorText,
this.textLength,
this.readyCallback,
this.onChanged,
this.keyboardType,
this.inputFormatters,
}) : super(key: key);
final TextEditingController controller;
final bool autoFocus;
final bool reRequestFocus;
final String title;
final String? helperText;
final String? hintText;
final String? errorText;
final int? textLength;
final VoidCallback? readyCallback;
final Function(StateSetter setState, SimpleWrapper<String?> errText)?
onChanged;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
@override
State<DialogVerificationCodeField> createState() =>
_DialogVerificationCodeField();
}
class _DialogVerificationCodeField extends State<DialogVerificationCodeField> {
final _focusNode = FocusNode();
Timer? _timer;
Timer? _timerReRequestFocus;
SimpleWrapper<String?> errorText = SimpleWrapper(null);
String _preText = '';
@override
void initState() {
super.initState();
if (widget.autoFocus) {
_timer =
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
if (widget.onChanged != null) {
widget.controller.addListener(() {
final text = widget.controller.text.trim();
if (text == _preText) return;
widget.onChanged!(setState, errorText);
_preText = text;
});
}
}
// software secure keyboard will take the focus since flutter 3.13
// request focus again when android account password obtain focus
if (Platform.isAndroid && widget.reRequestFocus) {
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
_timerReRequestFocus?.cancel();
_timerReRequestFocus = Timer(
Duration(milliseconds: 100), () => _focusNode.requestFocus());
}
});
}
}
@override
void dispose() {
_timer?.cancel();
_timerReRequestFocus?.cancel();
_focusNode.unfocus();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DialogTextField(
title: widget.title,
controller: widget.controller,
errorText: widget.errorText ?? errorText.value,
focusNode: _focusNode,
helperText: widget.helperText,
keyboardType: widget.keyboardType,
inputFormatters: widget.inputFormatters,
);
}
}
class PasswordWidget extends StatefulWidget { class PasswordWidget extends StatefulWidget {
PasswordWidget({ PasswordWidget({
Key? key, Key? key,
required this.controller, required this.controller,
this.autoFocus = true, this.autoFocus = true,
this.reRequestFocus = false,
this.hintText, this.hintText,
this.errorText, this.errorText,
}) : super(key: key); }) : super(key: key);
final TextEditingController controller; final TextEditingController controller;
final bool autoFocus; final bool autoFocus;
final bool reRequestFocus;
final String? hintText; final String? hintText;
final String? errorText; final String? errorText;
@@ -380,6 +680,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
bool _passwordVisible = false; bool _passwordVisible = false;
final _focusNode = FocusNode(); final _focusNode = FocusNode();
Timer? _timer; Timer? _timer;
Timer? _timerReRequestFocus;
@override @override
void initState() { void initState() {
@@ -388,11 +689,23 @@ class _PasswordWidgetState extends State<PasswordWidget> {
_timer = _timer =
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
} }
// software secure keyboard will take the focus since flutter 3.13
// request focus again when android account password obtain focus
if (Platform.isAndroid && widget.reRequestFocus) {
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
_timerReRequestFocus?.cancel();
_timerReRequestFocus = Timer(
Duration(milliseconds: 100), () => _focusNode.requestFocus());
}
});
}
} }
@override @override
void dispose() { void dispose() {
_timer?.cancel(); _timer?.cancel();
_timerReRequestFocus?.cancel();
_focusNode.unfocus(); _focusNode.unfocus();
_focusNode.dispose(); _focusNode.dispose();
super.dispose(); super.dispose();
@@ -664,6 +977,13 @@ void showWaitUacDialog(
(setState, close, context) => CustomAlertDialog( (setState, close, context) => CustomAlertDialog(
title: null, title: null,
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
actions: [
dialogButton(
'OK',
icon: Icon(Icons.done_rounded),
onPressed: close,
),
],
)); ));
} }
@@ -812,6 +1132,8 @@ void showRequestElevationDialog(
} else { } else {
bind.sessionElevateDirect(sessionId: sessionId); bind.sessionElevateDirect(sessionId: sessionId);
} }
close();
showWaitUacDialog(sessionId, dialogManager, "wait-uac");
} }
return CustomAlertDialog( return CustomAlertDialog(
@@ -882,7 +1204,7 @@ void showElevationError(SessionID sessionId, String type, String title,
dialogButton('Cancel', onPressed: () { dialogButton('Cancel', onPressed: () {
close(); close();
}, isOutline: true), }, isOutline: true),
dialogButton('Retry', onPressed: submit), if (text != 'No permission') dialogButton('Retry', onPressed: submit),
], ],
onSubmit: submit, onSubmit: submit,
onCancel: close, onCancel: close,
@@ -916,7 +1238,7 @@ void showRestartRemoteDevice(PeerInfo pi, String id, SessionID sessionId,
title: Row(children: [ title: Row(children: [
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
Flexible( Flexible(
child: Text(translate("Restart Remote Device")) child: Text(translate("Restart remote device"))
.paddingOnly(left: 10)), .paddingOnly(left: 10)),
]), ]),
content: Text( content: Text(
@@ -1188,11 +1510,24 @@ void showConfirmSwitchSidesDialog(
} }
customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
double qualityInitValue = 50; double initQuality = kDefaultQuality;
double fpsInitValue = 30; double initFps = kDefaultFps;
bool qualitySet = false; bool qualitySet = false;
bool fpsSet = false; bool fpsSet = false;
bool? direct;
try {
direct =
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
} catch (_) {}
bool hideFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
bool hideMoreQuality =
(await bind.mainIsUsingPublicServer() && direct != true) ||
versionCmp(ffi.ffiModel.pi.version, '1.2.2') < 0;
setCustomValues({double? quality, double? fps}) async { setCustomValues({double? quality, double? fps}) async {
debugPrint("setCustomValues quality:$quality, fps:$fps");
if (quality != null) { if (quality != null) {
qualitySet = true; qualitySet = true;
await bind.sessionSetCustomImageQuality( await bind.sessionSetCustomImageQuality(
@@ -1205,12 +1540,12 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
if (!qualitySet) { if (!qualitySet) {
qualitySet = true; qualitySet = true;
await bind.sessionSetCustomImageQuality( await bind.sessionSetCustomImageQuality(
sessionId: sessionId, value: qualityInitValue.toInt()); sessionId: sessionId, value: initQuality.toInt());
} }
if (!fpsSet) { if (!hideFps && !fpsSet) {
fpsSet = true; fpsSet = true;
await bind.sessionSetCustomFps( await bind.sessionSetCustomFps(
sessionId: sessionId, fps: fpsInitValue.toInt()); sessionId: sessionId, fps: initFps.toInt());
} }
} }
@@ -1221,134 +1556,30 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
// quality // quality
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId); final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
qualityInitValue = initQuality = quality != null && quality.isNotEmpty
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; ? quality[0].toDouble()
const qualityMinValue = 10.0; : kDefaultQuality;
const qualityMoreThresholdValue = 100.0; if (initQuality < kMinQuality ||
const qualityMaxValue = 2000.0; initQuality > (!hideMoreQuality ? kMaxMoreQuality : kMaxQuality)) {
if (qualityInitValue < qualityMinValue) { initQuality = kDefaultQuality;
qualityInitValue = qualityMinValue;
} }
if (qualityInitValue > qualityMaxValue) {
qualityInitValue = qualityMaxValue;
}
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
final moreQualityInitValue = qualityInitValue > qualityMoreThresholdValue;
final RxBool moreQualityChecked = RxBool(moreQualityInitValue);
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(quality: v);
},
initialValue: qualityInitValue,
);
final qualitySlider = Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: qualitySliderValue.value,
min: qualityMinValue,
max: moreQualityChecked.value
? qualityMaxValue
: qualityMoreThresholdValue,
divisions: 18,
onChanged: (double value) {
qualitySliderValue.value = value;
debouncerQuality.value = value;
},
)),
Expanded(
flex: 1,
child: Text(
'${qualitySliderValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 1,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 1,
child: Row(
children: [
Checkbox(
value: moreQualityChecked.value,
onChanged: (bool? value) {
moreQualityChecked.value = value!;
if (!value &&
qualitySliderValue.value >
qualityMoreThresholdValue) {
qualitySliderValue.value = qualityMoreThresholdValue;
debouncerQuality.value = qualityMoreThresholdValue;
}
},
).marginOnly(right: 5),
Expanded(
child: Text(translate('More')),
)
],
)),
],
));
// fps // fps
final fpsOption = final fpsOption =
await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps'); await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; initFps = fpsOption == null
if (fpsInitValue < 5 || fpsInitValue > 120) { ? kDefaultFps
fpsInitValue = 30; : double.tryParse(fpsOption) ?? kDefaultFps;
if (initFps < kMinFps || initFps > kMaxFps) {
initFps = kDefaultFps;
} }
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(fps: v);
},
initialValue: qualityInitValue,
);
bool? direct;
try {
direct =
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
} catch (_) {}
final fpsSlider = Offstage(
offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
child: Row(
children: [
Expanded(
flex: 3,
child: Obx((() => Slider(
value: fpsSliderValue.value,
min: 5,
max: 120,
divisions: 23,
onChanged: (double value) {
fpsSliderValue.value = value;
debouncerFps.value = value;
},
)))),
Expanded(
flex: 1,
child: Obx(() => Text(
'${fpsSliderValue.value.round()}',
style: const TextStyle(fontSize: 15),
))),
Expanded(
flex: 2,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
),
);
final content = Column( final content = customImageQualityWidget(
children: [qualitySlider, fpsSlider], initQuality: initQuality,
); initFps: initFps,
setQuality: (v) => setCustomValues(quality: v),
setFps: (v) => setCustomValues(fps: v),
showFps: !hideFps,
showMoreQuality: !hideMoreQuality);
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
} }
@@ -1519,3 +1750,162 @@ void renameDialog(
); );
}); });
} }
void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: "");
callback?.call();
return;
}
var new2fa = (await bind.mainGenerate2Fa());
final secretRegex = RegExp(r'secret=([^&]+)');
final secret = secretRegex.firstMatch(new2fa)?.group(1);
String? errorText;
final controller = TextEditingController();
gFFI.dialogManager.show((setState, close, context) {
onVerify() async {
if (await bind.mainVerify2Fa(code: controller.text.trim())) {
callback?.call();
close();
} else {
errorText = translate('wrong-2fa-code');
}
}
final codeField = Dialog2FaField(
controller: controller,
errorText: errorText,
onChanged: () => setState(() => errorText = null),
title: translate('Verification code'),
readyCallback: () {
onVerify();
setState(() {});
},
);
getOnSubmit() => codeField.isReady ? onVerify : null;
return CustomAlertDialog(
title: Text(translate("enable-2fa-title")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(translate("enable-2fa-desc"),
style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
SizedBox(
width: 160,
height: 160,
child: QrImageView(
backgroundColor: Colors.white,
data: new2fa,
version: QrVersions.auto,
size: 160,
gapless: false,
)).marginOnly(bottom: 6),
SelectableText(secret ?? '', style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
Row(children: [Expanded(child: codeField)]),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: getOnSubmit()),
],
onCancel: close,
);
});
}
void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
final RxBool submitReady = false.obs;
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
cancel() {
close();
closeConnection();
}
submit() {
gFFI.send2FA(sessionId, controller.text.trim());
close();
dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection);
}
late Dialog2FaField codeField;
codeField = Dialog2FaField(
controller: controller,
title: translate('Verification code'),
onChanged: () => submitReady.value = codeField.isReady,
);
return CustomAlertDialog(
title: Text(translate('enter-2fa-title')),
content: codeField,
actions: [
dialogButton('Cancel',
onPressed: cancel,
isOutline: true,
style: TextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color)),
Obx(() => dialogButton(
'OK',
onPressed: submitReady.isTrue ? submit : null,
)),
],
onSubmit: submit,
onCancel: cancel);
});
}
// This dialog should not be dismissed, otherwise it will be black screen, have not reproduced this.
void showWindowsSessionsDialog(
String type,
String title,
String text,
OverlayDialogManager dialogManager,
SessionID sessionId,
String peerId,
String sessions) {
List<dynamic> sessionsList = [];
try {
sessionsList = json.decode(sessions);
} catch (e) {
print(e);
}
List<String> sids = [];
List<String> names = [];
for (var session in sessionsList) {
sids.add(session['sid']);
names.add(session['name']);
}
String selectedUserValue = sids.first;
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
submit() {
bind.sessionSendSelectedSessionId(
sessionId: sessionId, sid: selectedUserValue);
close();
}
return CustomAlertDialog(
title: null,
content: msgboxContent(type, title, text),
actions: [
ComboBox(
keys: sids,
values: names,
initialKey: selectedUserValue,
onChanged: (value) {
selectedUserValue = value;
}),
dialogButton('Connect', onPressed: submit, isOutline: false),
],
);
});
}

View File

@@ -14,6 +14,7 @@ import './dialog.dart';
const kOpSvgList = [ const kOpSvgList = [
'github', 'github',
'gitlab',
'google', 'google',
'apple', 'apple',
'okta', 'okta',
@@ -72,6 +73,11 @@ class ButtonOP extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final opLabel = {
'github': 'GitHub',
'gitlab': 'GitLab'
}[op.toLowerCase()] ??
toCapitalized(op);
return Row(children: [ return Row(children: [
Container( Container(
height: height, height: height,
@@ -97,8 +103,7 @@ class ButtonOP extends StatelessWidget {
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Center( child: Center(
child: Text( child: Text('${translate("Continue with")} $opLabel')),
'${translate("Continue with")} ${op.toLowerCase() == "github" ? "GitHub" : toCapitalized(op)}')),
), ),
), ),
], ],
@@ -352,6 +357,7 @@ class LoginWidgetUserPass extends StatelessWidget {
PasswordWidget( PasswordWidget(
controller: pass, controller: pass,
autoFocus: false, autoFocus: false,
reRequestFocus: true,
errorText: passMsg, errorText: passMsg,
), ),
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
@@ -384,8 +390,7 @@ class LoginWidgetUserPass extends StatelessWidget {
const kAuthReqTypeOidc = 'oidc/'; const kAuthReqTypeOidc = 'oidc/';
/// common login dialog for desktop // call this directly
/// call this directly
Future<bool?> loginDialog() async { Future<bool?> loginDialog() async {
var username = var username =
TextEditingController(text: UserModel.getLocalUserInfo()?['name'] ?? ''); TextEditingController(text: UserModel.getLocalUserInfo()?['name'] ?? '');
@@ -421,6 +426,55 @@ Future<bool?> loginDialog() async {
close(false); close(false);
} }
handleLoginResponse(LoginResponse resp, bool storeIfAccessToken,
void Function([dynamic])? close) async {
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
if (storeIfAccessToken) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
await bind.mainSetLocalOption(
key: 'user_info', value: jsonEncode(resp.user ?? {}));
}
if (close != null) {
close(true);
}
return;
}
break;
case HttpType.kAuthResTypeEmailCheck:
bool? isEmailVerification;
if (resp.tfa_type == null ||
resp.tfa_type == HttpType.kAuthResTypeEmailCheck) {
isEmailVerification = true;
} else if (resp.tfa_type == HttpType.kAuthResTypeTfaCheck) {
isEmailVerification = false;
} else {
passwordMsg = "Failed, bad tfa type from server";
}
if (isEmailVerification != null) {
if (isMobile) {
if (close != null) close(false);
verificationCodeDialog(
resp.user, resp.secret, isEmailVerification);
} else {
setState(() => isInProgress = false);
final res = await verificationCodeDialog(
resp.user, resp.secret, isEmailVerification);
if (res == true) {
if (close != null) close(false);
return;
}
}
}
break;
default:
passwordMsg = "Failed, bad response from server";
break;
}
}
onLogin() async { onLogin() async {
// validate // validate
if (username.text.isEmpty) { if (username.text.isEmpty) {
@@ -441,35 +495,7 @@ Future<bool?> loginDialog() async {
uuid: await bind.mainGetUuid(), uuid: await bind.mainGetUuid(),
autoLogin: true, autoLogin: true,
type: HttpType.kAuthReqTypeAccount)); type: HttpType.kAuthReqTypeAccount));
await handleLoginResponse(resp, true, close);
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
await bind.mainSetLocalOption(
key: 'user_info', value: jsonEncode(resp.user ?? {}));
close(true);
return;
}
break;
case HttpType.kAuthResTypeEmailCheck:
if (isMobile) {
close(true);
verificationCodeDialog(resp.user);
} else {
setState(() => isInProgress = false);
final res = await verificationCodeDialog(resp.user);
if (res == true) {
close(true);
return;
}
}
break;
default:
passwordMsg = "Failed, bad response from server";
break;
}
} on RequestException catch (err) { } on RequestException catch (err) {
passwordMsg = translate(err.cause); passwordMsg = translate(err.cause);
} catch (err) { } catch (err) {
@@ -500,15 +526,21 @@ Future<bool?> loginDialog() async {
.map((e) => ConfigOP(op: e['name'], icon: e['icon'])) .map((e) => ConfigOP(op: e['name'], icon: e['icon']))
.toList(), .toList(),
curOP: curOP, curOP: curOP,
cbLogin: (Map<String, dynamic> authBody) { cbLogin: (Map<String, dynamic> authBody) async {
LoginResponse? resp;
try { try {
// access_token is already stored in the rust side. // access_token is already stored in the rust side.
gFFI.userModel.getLoginResponseFromAuthBody(authBody); resp =
gFFI.userModel.getLoginResponseFromAuthBody(authBody);
} catch (e) { } catch (e) {
debugPrint( debugPrint(
'Failed to parse oidc login body: "$authBody"'); 'Failed to parse oidc login body: "$authBody"');
} }
close(true); close(true);
if (resp != null) {
handleLoginResponse(resp, false, null);
}
}, },
), ),
], ],
@@ -567,6 +599,7 @@ Future<bool?> loginDialog() async {
], ],
), ),
onCancel: onDialogCancel, onCancel: onDialogCancel,
onSubmit: onLogin,
); );
}); });
@@ -577,37 +610,23 @@ Future<bool?> loginDialog() async {
return res; return res;
} }
Future<bool?> verificationCodeDialog(UserPayload? user) async { Future<bool?> verificationCodeDialog(
UserPayload? user, String? secret, bool isEmailVerification) async {
var autoLogin = true; var autoLogin = true;
var isInProgress = false; var isInProgress = false;
String? errorText; String? errorText;
final code = TextEditingController(); final code = TextEditingController();
final focusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
final res = await gFFI.dialogManager.show<bool>((setState, close, context) { final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
bool validate() {
return code.text.length >= 6;
}
code.addListener(() {
if (errorText != null) {
setState(() => errorText = null);
}
});
void onVerify() async { void onVerify() async {
if (!validate()) {
setState(
() => errorText = translate('Too short, at least 6 characters.'));
return;
}
setState(() => isInProgress = true); setState(() => isInProgress = true);
try { try {
final resp = await gFFI.userModel.login(LoginRequest( final resp = await gFFI.userModel.login(LoginRequest(
verificationCode: code.text, verificationCode: code.text,
tfaCode: isEmailVerification ? null : code.text,
secret: secret,
username: user?.name, username: user?.name,
id: await bind.mainGetMyId(), id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(), uuid: await bind.mainGetUuid(),
@@ -636,27 +655,37 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
setState(() => isInProgress = false); setState(() => isInProgress = false);
} }
final codeField = isEmailVerification
? DialogEmailCodeField(
controller: code,
errorText: errorText,
readyCallback: onVerify,
onChanged: () => errorText = null,
)
: Dialog2FaField(
controller: code,
errorText: errorText,
readyCallback: onVerify,
onChanged: () => errorText = null,
);
getOnSubmit() => codeField.isReady ? onVerify : null;
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("Verification code")), title: Text(translate("Verification code")),
contentBoxConstraints: BoxConstraints(maxWidth: 300), contentBoxConstraints: BoxConstraints(maxWidth: 300),
content: Column( content: Column(
children: [ children: [
Offstage( Offstage(
offstage: user?.email == null, offstage: !isEmailVerification || user?.email == null,
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Email", prefixIcon: Icon(Icons.email)), labelText: "Email", prefixIcon: Icon(Icons.email)),
readOnly: true, readOnly: true,
controller: TextEditingController(text: user?.email), controller: TextEditingController(text: user?.email),
)), )),
const SizedBox(height: 8), isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
DialogTextField( codeField,
title: '${translate("Verification code")}:',
controller: code,
errorText: errorText,
focusNode: focusNode,
helperText: translate('verification_tip'),
),
/* /*
CheckboxListTile( CheckboxListTile(
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
@@ -677,10 +706,10 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
], ],
), ),
onCancel: close, onCancel: close,
onSubmit: onVerify, onSubmit: getOnSubmit(),
actions: [ actions: [
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("Verify", onPressed: onVerify), dialogButton("Verify", onPressed: getOnSubmit()),
]); ]);
}); });

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
@@ -29,49 +31,28 @@ class _MyGroupState extends State<MyGroup> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
// use username to be same with ab if (!gFFI.userModel.isLogin) {
if (gFFI.userModel.userName.value.isEmpty) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return buildBody(context);
});
}
Widget buildBody(BuildContext context) {
return Obx(() {
if (gFFI.groupModel.groupLoading.value) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
if (gFFI.groupModel.groupLoadError.isNotEmpty) { return Column(
return _buildShowError(gFFI.groupModel.groupLoadError.value); children: [
} buildErrorBanner(context,
if (isDesktop) { loading: gFFI.groupModel.groupLoading,
return _buildDesktop(); err: gFFI.groupModel.groupLoadError,
} else { retry: null,
return _buildMobile(); close: () => gFFI.groupModel.groupLoadError.value = ''),
} Expanded(child: isDesktop ? _buildDesktop() : _buildMobile())
],
);
}); });
} }
Widget _buildShowError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(error)),
TextButton(
onPressed: () {
gFFI.groupModel.pull();
},
child: Text(translate("Retry")))
],
));
}
Widget _buildDesktop() { Widget _buildDesktop() {
return Row( return Row(
children: [ children: [
@@ -100,10 +81,9 @@ class _MyGroupState extends State<MyGroup> {
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.groupModel.peers)),
initPeers: gFFI.groupModel.peersShow.value))),
) )
], ],
); );
@@ -133,16 +113,16 @@ class _MyGroupState extends State<MyGroup> {
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member initPeers: gFFI.groupModel.peers)),
initPeers: gFFI.groupModel.peersShow.value))),
) )
], ],
); );
} }
Widget _buildLeftHeader() { Widget _buildLeftHeader() {
final fontSize = 14.0;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
@@ -151,16 +131,16 @@ class _MyGroupState extends State<MyGroup> {
onChanged: (value) { onChanged: (value) {
searchUserText.value = value; searchUserText.value = value;
}, },
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration( decoration: InputDecoration(
filled: false, filled: false,
prefixIcon: Icon( prefixIcon: Icon(
Icons.search_rounded, Icons.search_rounded,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ).paddingOnly(top: 2),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
hintText: translate("Search"), hintText: translate("Search"),
hintStyle: hintStyle: TextStyle(fontSize: fontSize),
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
), ),
@@ -171,16 +151,22 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserContacts() { Widget _buildUserContacts() {
return Obx(() { return Obx(() {
return Column( final items = gFFI.groupModel.users.where((p0) {
children: gFFI.groupModel.users if (searchUserText.isNotEmpty) {
.where((p0) { return p0.name
if (searchUserText.isNotEmpty) { .toLowerCase()
return p0.name.contains(searchUserText.value); .contains(searchUserText.value.toLowerCase());
} }
return true; return true;
}) }).toList();
.map((e) => _buildUserItem(e)) final listView = ListView.builder(
.toList()); shrinkWrap: isMobile,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? listView
: LimitedBox(maxHeight: maxHeight, child: listView);
}); });
} }
@@ -195,6 +181,8 @@ class _MyGroupState extends State<MyGroup> {
}, child: Obx( }, child: Obx(
() { () {
bool selected = selectedUser.value == username; bool selected = selectedUser.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null, color: selected ? MyTheme.color(context).highlight : null,
@@ -206,9 +194,42 @@ class _MyGroupState extends State<MyGroup> {
child: Container( child: Container(
child: Row( child: Row(
children: [ children: [
Icon(Icons.person_rounded, color: Colors.grey, size: 16) Container(
.marginOnly(right: 4), width: 20,
Expanded(child: Text(username)), height: 20,
decoration: BoxDecoration(
color: str2color(username, 0xAF),
shape: BoxShape.circle,
),
child: Align(
alignment: Alignment.center,
child: Center(
child: Text(
username.characters.first.toUpperCase(),
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
).marginOnly(right: 4),
if (isMe) Flexible(child: Text(username)),
if (isMe)
Flexible(
child: Container(
margin: EdgeInsets.only(left: 5),
padding: EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: colorMe.withAlpha(20),
borderRadius: BorderRadius.all(Radius.circular(2)),
border: Border.all(color: colorMe.withAlpha(100))),
child: Text(
translate('Me'),
style: TextStyle(
color: colorMe.withAlpha(200), fontSize: 12),
),
),
),
if (!isMe) Expanded(child: Text(username)),
], ],
).paddingSymmetric(vertical: 4), ).paddingSymmetric(vertical: 4),
), ),

View File

@@ -26,29 +26,45 @@ class DraggableChatWindow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Draggable( return isIOS
checkKeyboard: true, ? IOSDraggable(
position: position, position: position,
width: width, chatModel: chatModel,
height: height, width: width,
builder: (context, onPanUpdate) { height: height,
final child = isIOS builder: (context) {
? ChatPage(chatModel: chatModel) return Column(
: Scaffold( children: [
resizeToAvoidBottomInset: false, _buildMobileAppBar(context),
appBar: CustomAppBar( Expanded(
onPanUpdate: onPanUpdate, child: ChatPage(chatModel: chatModel),
appBar: isDesktop
? _buildDesktopAppBar(context)
: _buildMobileAppBar(context),
), ),
body: ChatPage(chatModel: chatModel), ],
); );
return Container( },
decoration: )
BoxDecoration(border: Border.all(color: MyTheme.border)), : Draggable(
child: child); checkKeyboard: true,
}); position: position,
width: width,
height: height,
chatModel: chatModel,
builder: (context, onPanUpdate) {
final child = Scaffold(
resizeToAvoidBottomInset: false,
appBar: CustomAppBar(
onPanUpdate: onPanUpdate,
appBar: isDesktop
? _buildDesktopAppBar(context)
: _buildMobileAppBar(context),
),
body: ChatPage(chatModel: chatModel),
);
return Container(
decoration:
BoxDecoration(border: Border.all(color: MyTheme.border)),
child: child);
});
} }
Widget _buildMobileAppBar(BuildContext context) { Widget _buildMobileAppBar(BuildContext context) {
@@ -226,6 +242,7 @@ class Draggable extends StatefulWidget {
this.position = Offset.zero, this.position = Offset.zero,
required this.width, required this.width,
required this.height, required this.height,
this.chatModel,
required this.builder}) required this.builder})
: super(key: key); : super(key: key);
@@ -234,6 +251,7 @@ class Draggable extends StatefulWidget {
final Offset position; final Offset position;
final double width; final double width;
final double height; final double height;
final ChatModel? chatModel;
final Widget Function(BuildContext, GestureDragUpdateCallback) builder; final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override @override
@@ -242,6 +260,7 @@ class Draggable extends StatefulWidget {
class _DraggableState extends State<Draggable> { class _DraggableState extends State<Draggable> {
late Offset _position; late Offset _position;
late ChatModel? _chatModel;
bool _keyboardVisible = false; bool _keyboardVisible = false;
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@@ -250,6 +269,7 @@ class _DraggableState extends State<Draggable> {
void initState() { void initState() {
super.initState(); super.initState();
_position = widget.position; _position = widget.position;
_chatModel = widget.chatModel;
} }
void onPanUpdate(DragUpdateDetails d) { void onPanUpdate(DragUpdateDetails d) {
@@ -276,6 +296,7 @@ class _DraggableState extends State<Draggable> {
setState(() { setState(() {
_position = Offset(x, y); _position = Offset(x, y);
}); });
_chatModel?.setChatWindowPosition(_position);
} }
checkScreenSize() {} checkScreenSize() {}
@@ -331,6 +352,107 @@ class _DraggableState extends State<Draggable> {
} }
} }
class IOSDraggable extends StatefulWidget {
const IOSDraggable(
{Key? key,
this.position = Offset.zero,
this.chatModel,
required this.width,
required this.height,
required this.builder})
: super(key: key);
final Offset position;
final ChatModel? chatModel;
final double width;
final double height;
final Widget Function(BuildContext) builder;
@override
IOSDraggableState createState() => IOSDraggableState();
}
class IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel;
late double _width;
late double _height;
bool _keyboardVisible = false;
double _saveHeight = 0;
double _lastBottomHeight = 0;
@override
void initState() {
super.initState();
_position = widget.position;
_chatModel = widget.chatModel;
_width = widget.width;
_height = widget.height;
}
checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0;
// save
if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy;
}
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
_position = Offset(_position.dx, _saveHeight);
});
}
// onKeyboardVisible
if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
_position = Offset(_position.dx, y);
});
}
}
_keyboardVisible = currentVisible;
_lastBottomHeight = bottomHeight;
}
@override
Widget build(BuildContext context) {
checkKeyboard();
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
});
_chatModel?.setChatWindowPosition(_position);
},
child: Material(
child: Container(
width: _width,
height: _height,
decoration:
BoxDecoration(border: Border.all(color: MyTheme.border)),
child: widget.builder(context),
),
),
),
),
],
);
}
}
class QualityMonitor extends StatelessWidget { class QualityMonitor extends StatelessWidget {
final QualityMonitorModel qualityMonitorModel; final QualityMonitorModel qualityMonitorModel;
QualityMonitor(this.qualityMonitorModel); QualityMonitor(this.qualityMonitorModel);
@@ -376,6 +498,7 @@ class QualityMonitor extends StatelessWidget {
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
_row( _row(
"Codec", qualityMonitorModel.data.codecFormat ?? '-'), "Codec", qualityMonitorModel.data.codecFormat ?? '-'),
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
], ],
), ),
) )

View File

@@ -1,11 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -21,7 +19,7 @@ import 'dart:math' as math;
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>> typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
Function(BuildContext); Function(BuildContext);
enum PeerUiType { grid, list } enum PeerUiType { grid, tile, list }
final peerCardUiType = PeerUiType.grid.obs; final peerCardUiType = PeerUiType.grid.obs;
@@ -63,75 +61,29 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildMobile() { Widget _buildMobile() {
final peer = super.widget.peer; final peer = super.widget.peer;
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final child = Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer); peerTabModel.select(peer);
}, } else {
child: Container( if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: Row( child: _buildPeerTile(context, peer, null)),
children: [ ));
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: BorderRadius.circular(4),
),
padding: const EdgeInsets.all(6),
child: getPlatformImage(peer.platform)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
getOnline(4, peer.online),
Text(peer.alias.isEmpty
? formatID(peer.id)
: peer.alias)
]),
Text(name)
],
).paddingOnly(left: 8.0),
),
checkBoxOrActionMoreMobile(peer),
],
),
)));
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList();
return Tooltip(
message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
if (colors.isNotEmpty)
Positioned(
top: 2,
right: 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
)
]),
);
} }
Widget _buildDesktop() { Widget _buildDesktop() {
@@ -178,87 +130,96 @@ class _PeerCardState extends State<_PeerCard>
} }
Widget _buildPeerTile( Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
final name = final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Obx( final child = Row(
() => Container( mainAxisSize: MainAxisSize.max,
foregroundDecoration: deco.value, children: [
child: Row( Container(
mainAxisSize: MainAxisSize.max, decoration: BoxDecoration(
children: [ color: str2color('${peer.id}${peer.platform}', 0x7f),
Container( borderRadius: isMobile
decoration: BoxDecoration( ? BorderRadius.circular(_tileRadius)
color: str2color('${peer.id}${peer.platform}', 0x7f), : BorderRadius.only(
borderRadius: BorderRadius.only( topLeft: Radius.circular(_tileRadius),
topLeft: Radius.circular(_tileRadius), bottomLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: 42,
child: getPlatformImage(peer.platform, size: 30).paddingAll(6),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
), ),
), ),
child: Row( alignment: Alignment.center,
children: [ width: isMobile ? 50 : 42,
Expanded( height: isMobile ? 50 : null,
child: Column( child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
children: [ .paddingAll(6),
Row(children: [
getOnline(8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty
? formatID(peer.id)
: peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(bottom: 0, top: 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
), ),
), Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(children: [
getOnline(isMobile ? 4 : 8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(top: isMobile ? 0 : 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isMobile ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
isMobile
? checkBoxOrActionMoreMobile(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
); );
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList(); final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: isMobile
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? ''
: '', : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [ child: Stack(children: [
child, deco == null
? child
: Obx(
() => Container(
foregroundDecoration: deco.value,
child: child,
),
),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Positioned(
top: 2, top: 2,
right: 10, right: isMobile ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
@@ -349,7 +310,8 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList(); final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
@@ -533,7 +495,7 @@ abstract class BasePeerCard extends StatelessWidget {
return _connectCommonAction( return _connectCommonAction(
context, context,
id, id,
translate('Transfer File'), translate('Transfer file'),
isFileTransfer: true, isFileTransfer: true,
); );
} }
@@ -543,7 +505,7 @@ abstract class BasePeerCard extends StatelessWidget {
return _connectCommonAction( return _connectCommonAction(
context, context,
id, id,
translate('TCP Tunneling'), translate('TCP tunneling'),
isTcpTunneling: true, isTcpTunneling: true,
); );
} }
@@ -606,7 +568,7 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _createShortCutAction(String id) { MenuEntryBase<String> _createShortCutAction(String id) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text( childBuilder: (TextStyle? style) => Text(
translate('Create Desktop Shortcut'), translate('Create desktop shortcut'),
style: style, style: style,
), ),
proc: () { proc: () {
@@ -638,8 +600,9 @@ abstract class BasePeerCard extends StatelessWidget {
await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs); await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
_openInWindowsAction(String id) async => await _openNewConnInAction( _openInWindowsAction(String id) async => await _openNewConnInAction(
id, 'Open in New Window', kOptionOpenInWindows); id, 'Open in new window', kOptionOpenInWindows);
// ignore: unused_element
_openNewConnInOptAction(String id) async => _openNewConnInOptAction(String id) async =>
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
? await _openInWindowsAction(id) ? await _openInWindowsAction(id)
@@ -765,17 +728,18 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _unrememberPasswordAction(String id) { MenuEntryBase<String> _unrememberPasswordAction(String id) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text( childBuilder: (TextStyle? style) => Text(
translate('Unremember Password'), translate('Forget Password'),
style: style, style: style,
), ),
proc: () async { proc: () async {
bool result = gFFI.abModel.changePassword(id, ''); bool result = gFFI.abModel.changePassword(id, '');
await bind.mainForgetPassword(id: id); await bind.mainForgetPassword(id: id);
bool toast = false;
if (result) { if (result) {
bool toast = tab == PeerTabIndex.ab; toast = tab == PeerTabIndex.ab;
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast); gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
} }
showToast(translate('Successful')); if (!toast) showToast(translate('Successful'));
}, },
padding: menuPadding, padding: menuPadding,
dismissOnClicked: true, dismissOnClicked: true,
@@ -855,7 +819,7 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _addToAb(Peer peer) { MenuEntryBase<String> _addToAb(Peer peer) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text( childBuilder: (TextStyle? style) => Text(
translate('Add to Address Book'), translate('Add to address book'),
style: style, style: style,
), ),
proc: () { proc: () {
@@ -900,12 +864,12 @@ class RecentPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -954,12 +918,12 @@ class FavoritePeerCard extends BasePeerCard {
_connectAction(context, peer), _connectAction(context, peer),
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
]; ];
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -1008,12 +972,12 @@ class DiscoveredPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
menuItems.add(_wolAction(peer.id)); menuItems.add(_wolAction(peer.id));
@@ -1058,12 +1022,12 @@ class AddressBookPeerCard extends BasePeerCard {
_connectAction(context, peer), _connectAction(context, peer),
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
]; ];
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
@@ -1126,21 +1090,26 @@ class MyGroupPeerCard extends BasePeerCard {
_connectAction(context, peer), _connectAction(context, peer),
_transferFileAction(context, peer.id), _transferFileAction(context, peer.id),
]; ];
if (isDesktop && peer.platform != 'Android') { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context, peer.id));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); // menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') { if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); // menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id)); // menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) { // if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id)); // menuItems.add(_unrememberPasswordAction(peer.id));
// }
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
} }
return menuItems; return menuItems;
} }

View File

@@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart';
@@ -5,14 +7,18 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart'; import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pull_down_button/pull_down_button.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
@@ -63,14 +69,17 @@ class _PeerTabPageState extends State<PeerTabPage>
({dynamic hint}) => gFFI.groupModel.pull(force: hint == null), ({dynamic hint}) => gFFI.groupModel.pull(force: hint == null),
), ),
]; ];
RelativeRect? mobileTabContextMenuPos;
@override @override
void initState() { void initState() {
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type'); final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
if (uiType != '') { if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index peerCardUiType.value = int.parse(uiType) == 0
? PeerUiType.list ? PeerUiType.grid
: PeerUiType.grid; : int.parse(uiType) == 1
? PeerUiType.tile
: PeerUiType.list;
} }
hideAbTagsPanel.value = hideAbTagsPanel.value =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
@@ -79,6 +88,9 @@ class _PeerTabPageState extends State<PeerTabPage>
Future<void> handleTabSelection(int tabIndex) async { Future<void> handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) { if (tabIndex < entries.length) {
if (tabIndex != gFFI.peerTabModel.currentTab) {
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
}
gFFI.peerTabModel.setCurrentTab(tabIndex); gFFI.peerTabModel.setCurrentTab(tabIndex);
entries[tabIndex].load(hint: false); entries[tabIndex].load(hint: false);
} }
@@ -102,40 +114,17 @@ class _PeerTabPageState extends State<PeerTabPage>
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded(child: _createSwitchBar(context)), Expanded(
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), child:
_createRefresh(), visibleContextMenuListener(_createSwitchBar(context))),
_createMultiSelection(), if (isMobile)
Offstage( ..._mobileRightActions(context)
offstage: !isDesktop, else
child: _createPeerViewTypeSwitch(context)), ..._desktopRightActions(context)
Offstage(
offstage: gFFI.peerTabModel.currentTab == 0,
child: PeerSortDropdown(),
),
Offstage(
offstage: gFFI.peerTabModel.currentTab != 3,
child: _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel",
value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
},
),
),
], ],
)), )),
), ),
), ).paddingOnly(right: isDesktop ? 12 : 0),
_createPeersView(), _createPeersView(),
], ],
); );
@@ -147,7 +136,7 @@ class _PeerTabPageState extends State<PeerTabPage>
return ListView( return ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: model.indexs.map((t) { children: model.visibleIndexs.map((t) {
final selected = model.currentTab == t; final selected = model.currentTab == t;
final color = selected final color = selected
? MyTheme.tabbar(context).selectedTextColor ? MyTheme.tabbar(context).selectedTextColor
@@ -163,11 +152,13 @@ class _PeerTabPageState extends State<PeerTabPage>
)); ));
return Obx(() => InkWell( return Obx(() => InkWell(
child: Container( child: Container(
decoration: decoration: (hover.value
selected ? decoBorder : (hover.value ? deco : null), ? (selected ? decoBorder : deco)
: (selected ? decoBorder : null)),
child: Tooltip( child: Tooltip(
message: preferBelow: false,
model.tabTooltip(t, gFFI.groupModel.groupName.value), message: model.tabTooltip(t),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
child: Icon(model.tabIcon(t), color: color), child: Icon(model.tabIcon(t), color: color),
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4),
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4),
@@ -184,14 +175,15 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createPeersView() { Widget _createPeersView() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget child; Widget child;
if (model.indexs.isEmpty) { if (model.visibleIndexs.isEmpty) {
child = Center( child = visibleContextMenuListener(Row(
child: Text(translate('Right click to select tabs')), children: [Expanded(child: InkWell())],
); ));
} else { } else {
if (model.indexs.contains(model.currentTab)) { if (model.visibleIndexs.contains(model.currentTab)) {
child = entries[model.currentTab].widget; child = entries[model.currentTab].widget;
} else { } else {
debugPrint("should not happen! currentTab not in visibleIndexs");
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
model.setCurrentTab(model.indexs[0]); model.setCurrentTab(model.indexs[0]);
}); });
@@ -202,17 +194,19 @@ class _PeerTabPageState extends State<PeerTabPage>
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
} }
Widget _createRefresh() { Widget _createRefresh(
{required PeerTabIndex index, required RxBool loading}) {
final model = Provider.of<PeerTabModel>(context);
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage( return Offstage(
offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index, offstage: model.currentTab != index.index,
child: RefreshWidget( child: RefreshWidget(
onPressed: () { onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) { if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load(); entries[gFFI.peerTabModel.currentTab].load();
} }
}, },
spinning: gFFI.abModel.abLoading, spinning: loading,
child: RotatedBox( child: RotatedBox(
quarterTurns: 2, quarterTurns: 2,
child: Tooltip( child: Tooltip(
@@ -226,68 +220,151 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget _createPeerViewTypeSwitch(BuildContext context) { Widget _createPeerViewTypeSwitch(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color; return PeerViewDropdown();
final types = [PeerUiType.grid, PeerUiType.list];
return Obx(() => _hoverAction(
context: context,
onTap: () async {
final type = types
.elementAt(peerCardUiType.value == types.elementAt(0) ? 1 : 0);
await bind.setLocalFlutterOption(
k: 'peer-card-ui-type', v: type.index.toString());
peerCardUiType.value = type;
},
child: Tooltip(
message: peerCardUiType.value == PeerUiType.grid
? translate('List View')
: translate('Grid View'),
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.view_list_rounded
: Icons.grid_view_rounded,
size: 18,
color: textColor,
))));
} }
Widget _createMultiSelection() { Widget _createMultiSelection() {
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
if (model.currentTabCachedPeers.isEmpty) return Offstage();
return _hoverAction( return _hoverAction(
context: context, context: context,
onTap: () { onTap: () {
model.setMultiSelectionMode(true); model.setMultiSelectionMode(true);
if (isMobile && Navigator.canPop(context)) {
Navigator.pop(context);
}
}, },
child: Tooltip( child: Tooltip(
message: translate('Select'), message: translate('Select'),
child: Icon( child: SvgPicture.asset(
IconFont.checkbox, "assets/checkbox-outline.svg",
size: 18, width: 18,
color: textColor, height: 18,
colorFilter: svgColor(textColor),
)), )),
); );
} }
void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel;
final items = List<PopupMenuItem>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisible[i]),
child: Row(
children: [
Checkbox(
value: model.isVisible[i],
onChanged: (_) {
model.setTabVisible(i, !model.isVisible[i]);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}),
Expanded(child: Text(model.tabTooltip(i))),
],
),
));
}
if (mobileTabContextMenuPos != null) {
showMenu(
context: context, position: mobileTabContextMenuPos!, items: items);
}
}
Widget visibleContextMenuListener(Widget child) {
if (isMobile) {
return GestureDetector(
onLongPressDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
mobileTabContextMenuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onLongPressUp: () {
mobileShowTabVisibilityMenu();
},
child: child,
);
} else {
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
if (e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return visibleContextMenu(cancelFunc);
},
target: e.position,
);
}
},
child: child);
}
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: model.tabTooltip(i),
getter: () async {
return model.isVisible[i];
},
setter: (show) async {
model.setTabVisible(i, show);
cancelFunc();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
}
Widget createMultiSelectionBar() { Widget createMultiSelectionBar() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
deleteSelection(), Offstage(
addSelectionToFav(), offstage: model.selectedPeers.isEmpty,
addSelectionToAb(), child: Row(
editSelectionTags(), children: [
Expanded(child: Container()), deleteSelection(),
selectionCount(model.selectedPeers.length), addSelectionToFav(),
selectAll(), addSelectionToAb(),
closeSelection(), editSelectionTags(),
],
),
),
Row(
children: [
selectionCount(model.selectedPeers.length),
selectAll(),
closeSelection(),
],
)
], ],
); );
} }
Widget deleteSelection() { Widget deleteSelection() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
if (model.currentTab == PeerTabIndex.group.index) {
return Offstage();
}
return _hoverAction( return _hoverAction(
context: context, context: context,
onTap: () { onTap: () {
@@ -393,7 +470,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
}, },
child: Tooltip( child: Tooltip(
message: translate('Add to Address Book'), message: translate('Add to address book'),
child: Icon(model.icons[PeerTabIndex.ab.index])), child: Icon(model.icons[PeerTabIndex.ab.index])),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: isMobile ? 11 : 6),
); );
@@ -457,6 +534,128 @@ class _PeerTabPageState extends State<PeerTabPage>
Tooltip(message: translate('Close'), child: Icon(Icons.clear))) Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
.marginOnly(left: 6); .marginOnly(left: 6);
} }
Widget _toggleTags() {
return _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
});
}
List<Widget> _desktopRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
Offstage(
offstage: model.currentTabCachedPeers.isEmpty,
child: _createMultiSelection(),
),
_createPeerViewTypeSwitch(context),
Offstage(
offstage: model.currentTab == PeerTabIndex.recent.index,
child: PeerSortDropdown(),
),
Offstage(
offstage: model.currentTab != PeerTabIndex.ab.index,
child: _toggleTags(),
),
];
}
List<Widget> _mobileRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize =
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length;
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120;
final otherActionWidth = 18 + 10;
dropDown(List<Widget> menus) {
final padding = 6.0;
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return PullDownButton(
buttonBuilder:
(BuildContext context, Future<void> Function() showMenu) {
return _hoverAction(
context: context,
child: Tooltip(
message: translate('More'),
child: SvgPicture.asset(
"assets/chevron_up_chevron_down.svg",
width: 18,
height: 18,
colorFilter: svgColor(textColor),
)),
onTap: showMenu,
);
},
routeTheme: PullDownMenuRouteTheme(
width: menus.length * (otherActionWidth + padding * 2) * 1.0),
itemBuilder: (context) => [
PullDownMenuEntryImpl(
child: Row(
mainAxisSize: MainAxisSize.min,
children: menus
.map((e) =>
Material(child: e.paddingSymmetric(horizontal: padding)))
.toList(),
),
)
],
);
}
// Always show search, refresh
List<Widget> actions = [
const PeerSearchBar(),
if (model.currentTab == PeerTabIndex.ab.index)
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
if (model.currentTab == PeerTabIndex.group.index)
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
];
final List<Widget> dynamicActions = [
if (model.currentTabCachedPeers.isNotEmpty) _createMultiSelection(),
if (model.currentTab != PeerTabIndex.recent.index) PeerSortDropdown(),
if (model.currentTab == PeerTabIndex.ab.index) _toggleTags()
];
final rightWidth = availableWidth -
searchWidth -
(actions.length == 2 ? otherActionWidth : 0);
final availablePositions = rightWidth ~/ otherActionWidth;
if (availablePositions < dynamicActions.length &&
dynamicActions.length > 1) {
if (availablePositions < 2) {
actions.addAll([
dropDown(dynamicActions),
]);
} else {
actions.addAll([
...dynamicActions.sublist(0, availablePositions - 1),
dropDown(dynamicActions.sublist(availablePositions - 1)),
]);
}
} else {
actions.addAll(dynamicActions);
}
return actions;
}
} }
class PeerSearchBar extends StatefulWidget { class PeerSearchBar extends StatefulWidget {
@@ -570,6 +769,87 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
} }
} }
class PeerViewDropdown extends StatefulWidget {
const PeerViewDropdown({super.key});
@override
State<PeerViewDropdown> createState() => _PeerViewDropdownState();
}
class _PeerViewDropdownState extends State<PeerViewDropdown> {
@override
Widget build(BuildContext context) {
final List<PeerUiType> types = [
PeerUiType.grid,
PeerUiType.tile,
PeerUiType.list
];
final style = TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
List<PopupMenuEntry> items = List.empty(growable: true);
items.add(PopupMenuItem(
height: 36,
enabled: false,
child: Text(translate("Change view"), style: style)));
for (var e in PeerUiType.values) {
items.add(PopupMenuItem(
height: 36,
child: Obx(() => Center(
child: SizedBox(
height: 36,
child: getRadio<PeerUiType>(
Text(
translate(types.indexOf(e) == 0
? 'Big tiles'
: types.indexOf(e) == 1
? 'Small tiles'
: 'List'),
style: style),
e,
peerCardUiType.value,
dense: true, (PeerUiType? v) async {
if (v != null) {
peerCardUiType.value = v;
setState(() {});
await bind.setLocalFlutterOption(
k: "peer-card-ui-type",
v: peerCardUiType.value.index.toString(),
);
}
}),
),
))));
}
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
return _hoverAction(
context: context,
child: Tooltip(
message: translate('Change view'),
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.grid_view_rounded
: peerCardUiType.value == PeerUiType.tile
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
)),
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () => showMenu(
context: context,
position: menuPos,
items: items,
elevation: 8,
));
}
}
class PeerSortDropdown extends StatefulWidget { class PeerSortDropdown extends StatefulWidget {
const PeerSortDropdown({super.key}); const PeerSortDropdown({super.key});
@@ -732,3 +1012,14 @@ Widget _hoverAction(
child: Container(padding: padding, child: child))), child: Container(padding: padding, child: child))),
); );
} }
class PullDownMenuEntryImpl extends StatelessWidget
implements PullDownMenuEntry {
final Widget child;
const PullDownMenuEntryImpl({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
}

View File

@@ -1,12 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/peer_model.dart'; import '../../models/peer_model.dart';
@@ -35,6 +38,7 @@ class LoadEvent {
static const String favorite = 'load_fav_peers'; static const String favorite = 'load_fav_peers';
static const String lan = 'load_lan_peers'; static const String lan = 'load_lan_peers';
static const String addressBook = 'load_address_book_peers'; static const String addressBook = 'load_address_book_peers';
static const String group = 'load_group_peers';
} }
/// for peer search text, global obs value /// for peer search text, global obs value
@@ -78,7 +82,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
final _curPeers = <String>{}; final _curPeers = <String>{};
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1)); var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
@@ -93,6 +97,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return width; return width;
}(); }();
final _scrollController = ScrollController();
_PeersViewState() { _PeersViewState() {
_startCheckOnlines(); _startCheckOnlines();
} }
@@ -174,28 +180,65 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return FutureBuilder<List<Peer>>( return FutureBuilder<List<Peer>>(
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
final cards = <Widget>[]; buildOnePeer(Peer peer) {
for (final peer in peers) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer), child: widget.peerCardBuilder(peer),
); );
cards.add(isDesktop final windowWidth = MediaQuery.of(context).size.width;
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
//
// No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView().
final currentTab = Provider.of<PeerTabModel>(context, listen: false).currentTab;
final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return isDesktop
? Obx( ? Obx(
() => SizedBox( () => SizedBox(
width: 220, width: peerCardUiType.value != PeerUiType.list
? 220
: currentTab == PeerTabIndex.group.index || (currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel)
? windowWidth - 390 :
windowWidth - 227,
height: height:
peerCardUiType.value == PeerUiType.grid ? 140 : 42, peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45,
child: visibilityChild, child: visibilityChild,
), ),
) )
: SizedBox(width: mobileWidth, child: visibilityChild)); : SizedBox(width: mobileWidth, child: visibilityChild);
} }
final child =
Wrap(spacing: space, runSpacing: space, children: cards); final Widget child;
if (isMobile) {
child = DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
},
);
} else {
child = DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
);
}
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();
_curPeers.addAll(peers.map((e) => e.id)); _curPeers.addAll(peers.map((e) => e.id));
@@ -229,8 +272,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
if (_queryCount < _maxQueryCount) { if (_queryCount < _maxQueryCount) {
if (now.difference(_lastQueryTime) >= _queryInterval) { if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
platformFFI.ffiBind bind.queryOnlines(ids: _curPeers.toList(growable: false));
.queryOnlines(ids: _curPeers.toList(growable: false));
_lastQueryTime = DateTime.now(); _lastQueryTime = DateTime.now();
_queryCount += 1; _queryCount += 1;
} }
@@ -244,7 +286,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(bool isLoadEvent) { _queryOnlines(bool isLoadEvent) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
platformFFI.ffiBind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
_lastQueryPeers = {..._curPeers}; _lastQueryPeers = {..._curPeers};
if (isLoadEvent) { if (isLoadEvent) {
_lastChangeTime = DateTime.now(); _lastChangeTime = DateTime.now();
@@ -312,7 +354,7 @@ abstract class BasePeersView extends StatelessWidget {
final String loadEvent; final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final List<Peer> initPeers; final RxList<Peer>? initPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
@@ -326,7 +368,7 @@ abstract class BasePeersView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _PeersView( return _PeersView(
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers),
peerFilter: peerFilter, peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder); peerCardBuilder: peerCardBuilder);
} }
@@ -343,7 +385,7 @@ class RecentPeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@@ -365,7 +407,7 @@ class FavoritePeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@@ -387,7 +429,7 @@ class DiscoveredPeersView extends BasePeersView {
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: [], initPeers: null,
); );
@override @override
@@ -403,7 +445,7 @@ class AddressBookPeersView extends BasePeersView {
{Key? key, {Key? key,
EdgeInsets? menuPadding, EdgeInsets? menuPadding,
ScrollController? scrollController, ScrollController? scrollController,
required List<Peer> initPeers}) required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', name: 'address book peer',
@@ -421,12 +463,21 @@ class AddressBookPeersView extends BasePeersView {
if (selectedTags.isEmpty) { if (selectedTags.isEmpty) {
return true; return true;
} }
for (final tag in selectedTags) { if (gFFI.abModel.filterByIntersection.value) {
if (idents.contains(tag)) { for (final tag in selectedTags) {
return true; if (!idents.contains(tag)) {
return false;
}
} }
return true;
} else {
for (final tag in selectedTags) {
if (idents.contains(tag)) {
return true;
}
}
return false;
} }
return false;
} }
} }
@@ -435,11 +486,11 @@ class MyGroupPeerView extends BasePeersView {
{Key? key, {Key? key,
EdgeInsets? menuPadding, EdgeInsets? menuPadding,
ScrollController? scrollController, ScrollController? scrollController,
required List<Peer> initPeers}) required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'my group peer', name: 'group peer',
loadEvent: 'load_my_group_peers', loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
@@ -450,12 +501,12 @@ class MyGroupPeerView extends BasePeersView {
static bool filter(Peer peer) { static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) { if (gFFI.groupModel.searchUserText.isNotEmpty) {
if (!peer.username.contains(gFFI.groupModel.searchUserText)) { if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
return false; return false;
} }
} }
if (gFFI.groupModel.selectedUser.isNotEmpty) { if (gFFI.groupModel.selectedUser.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.username) { if (gFFI.groupModel.selectedUser.value != peer.loginName) {
return false; return false;
} }
} }

View File

@@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/input_model.dart';
@@ -33,7 +34,8 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true, canRequestFocus: true,
focusNode: focusNode, focusNode: focusNode,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
onKey: inputModel.handleRawKeyEvent, onKey: (FocusNode data, RawKeyEvent e) =>
inputModel.handleRawKeyEvent(e),
child: child)); child: child));
} }
} }
@@ -92,6 +94,7 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
// Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
inputModel.tapDown(MouseButtons.left); inputModel.tapDown(MouseButtons.left);
} }
@@ -111,7 +114,10 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
inputModel.tap(MouseButtons.left); if (!handleTouch) {
// Mobile, "Mouse mode"
inputModel.tap(MouseButtons.left);
}
} }
onDoubleTapDown(TapDownDetails d) { onDoubleTapDown(TapDownDetails d) {
@@ -263,9 +269,9 @@ class _RawTouchGestureDetectorRegionState
if (scale != 0) { if (scale != 0) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': scale} PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
})); .toJson()));
} }
} else { } else {
// mobile // mobile
@@ -283,9 +289,8 @@ class _RawTouchGestureDetectorRegionState
if (isDesktop) { if (isDesktop) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': 0} PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
}));
} else { } else {
// mobile // mobile
_scale = 1; _scale = 1;

View File

@@ -0,0 +1,239 @@
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
customImageQualityWidget(
{required double initQuality,
required double initFps,
required Function(double) setQuality,
required Function(double) setFps,
required bool showFps,
required bool showMoreQuality}) {
if (initQuality < kMinQuality ||
initQuality > (showMoreQuality ? kMaxMoreQuality : kMaxQuality)) {
initQuality = kDefaultQuality;
}
if (initFps < kMinFps || initFps > kMaxFps) {
initFps = kDefaultFps;
}
final qualityValue = initQuality.obs;
final fpsValue = initFps.obs;
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setQuality(v);
},
initialValue: qualityValue.value,
);
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setFps(v);
},
initialValue: fpsValue.value,
);
onMoreChanged(bool? value) {
if (value == null) return;
moreQualityChecked.value = value;
if (!value && qualityValue.value > 100) {
qualityValue.value = 100;
}
debouncerQuality.value = qualityValue.value;
}
return Column(
children: [
Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: qualityValue.value,
min: kMinQuality,
max: moreQualityChecked.value ? kMaxMoreQuality : kMaxQuality,
divisions: moreQualityChecked.value
? ((kMaxMoreQuality - kMinQuality) / 10).round()
: ((kMaxQuality - kMinQuality) / 5).round(),
onChanged: (double value) async {
qualityValue.value = value;
debouncerQuality.value = value;
},
),
),
Expanded(
flex: 1,
child: Text(
'${qualityValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: isMobile ? 2 : 1,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
)),
// mobile doesn't have enough space
if (showMoreQuality && !isMobile)
Expanded(
flex: 1,
child: Row(
children: [
Checkbox(
value: moreQualityChecked.value,
onChanged: onMoreChanged,
),
Expanded(
child: Text(translate('More')),
)
],
))
],
)),
if (showMoreQuality && isMobile)
Obx(() => Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Checkbox(
value: moreQualityChecked.value,
onChanged: onMoreChanged,
),
),
),
Expanded(
child: Text(translate('More')),
)
],
)),
if (showFps)
Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: fpsValue.value,
min: kMinFps,
max: kMaxFps,
divisions: ((kMaxFps - kMinFps) / 5).round(),
onChanged: (double value) async {
fpsValue.value = value;
debouncerFps.value = value;
},
),
),
Expanded(
flex: 1,
child: Text(
'${fpsValue.value.round()}',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 2,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
)),
],
);
}
customImageQualitySetting() {
final qualityKey = 'custom_image_quality';
final fpsKey = 'custom-fps';
var initQuality =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
kDefaultQuality);
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
kDefaultFps);
return customImageQualityWidget(
initQuality: initQuality,
initFps: initFps,
setQuality: (v) {
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
},
setFps: (v) {
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
},
showFps: true,
showMoreQuality: true);
}
List<Widget> ServerConfigImportExportWidgets(
List<TextEditingController> controllers,
List<RxString> errMsgs,
) {
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
importConfig(controllers, errMsgs, value?.text);
});
}
export() {
final text = ServerConfig(
idServer: controllers[0].text.trim(),
relayServer: controllers[1].text.trim(),
apiServer: controllers[2].text.trim(),
key: controllers[3].text.trim())
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
return [
Tooltip(
message: translate('Import server config'),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton(
icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
];
}
List<(String, String)> otherDefaultSettings() {
List<(String, String)> v = [
('View Mode', 'view_only'),
if (isDesktop) ('show_monitors_tip', kKeyShowMonitorsToolbar),
if (isDesktop) ('Collapse toolbar', 'collapse_toolbar'),
('Show remote cursor', 'show_remote_cursor'),
if (isDesktop) ('Zoom cursor', 'zoom-cursor'),
('Show quality monitor', 'show_quality_monitor'),
('Mute', 'disable_audio'),
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
('Disable clipboard', 'disable_clipboard'),
('Lock after session end', 'lock_after_session_end'),
('Privacy mode', 'privacy_mode'),
if (isMobile) ('Touch mode', 'touch-mode'),
('True color (4:4:4)', 'i444'),
('Reverse mouse wheel', 'reverse_mouse_wheel'),
('swap-left-right-mouse', 'swap-left-right-mouse'),
if (isDesktop && useTextureRender)
(
'Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows
),
if (isDesktop && useTextureRender)
(
'Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession
)
];
return v;
}

View File

@@ -9,6 +9,7 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
bool isEditOsPassword = false; bool isEditOsPassword = false;
@@ -49,7 +50,8 @@ class TToggleMenu {
handleOsPasswordEditIcon( handleOsPasswordEditIcon(
SessionID sessionId, OverlayDialogManager dialogManager) { SessionID sessionId, OverlayDialogManager dialogManager) {
isEditOsPassword = true; isEditOsPassword = true;
showSetOSPassword(sessionId, false, dialogManager, null, () => isEditOsPassword = false); showSetOSPassword(
sessionId, false, dialogManager, null, () => isEditOsPassword = false);
} }
handleOsPasswordAction( handleOsPasswordAction(
@@ -62,7 +64,8 @@ handleOsPasswordAction(
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ?? await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
''; '';
if (password.isEmpty) { if (password.isEmpty) {
showSetOSPassword(sessionId, true, dialogManager, password, () => isEditOsPassword = false); showSetOSPassword(sessionId, true, dialogManager, password,
() => isEditOsPassword = false);
} else { } else {
bind.sessionInputOsPassword(sessionId: sessionId, value: password); bind.sessionInputOsPassword(sessionId: sessionId, value: password);
} }
@@ -76,7 +79,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
List<TTextMenu> v = []; List<TTextMenu> v = [];
// elevation // elevation
if (ffi.elevationModel.showRequestMenu) { if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Request Elevation')), child: Text(translate('Request Elevation')),
@@ -85,29 +88,34 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// osAccount / osPassword // osAccount / osPassword
v.add( if (perms['keyboard'] != false) {
TTextMenu( v.add(
child: Row(children: [ TTextMenu(
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')), child: Row(children: [
Offstage( Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
offstage: isDesktop, ]),
child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12), trailingIcon: Transform.scale(
) scale: isDesktop ? 0.8 : 1,
]), child: IconButton(
trailingIcon: Transform.scale( onPressed: () {
scale: 0.8, if (isMobile && Navigator.canPop(context)) {
child: InkWell( Navigator.pop(context);
onTap: () => pi.is_headless }
? showSetOSAccount(sessionId, ffi.dialogManager) if (pi.isHeadless) {
: handleOsPasswordEditIcon(sessionId, ffi.dialogManager), showSetOSAccount(sessionId, ffi.dialogManager);
child: Icon(Icons.edit), } else {
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
}
},
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
),
), ),
onPressed: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordAction(sessionId, ffi.dialogManager),
), ),
onPressed: () => pi.is_headless );
? showSetOSAccount(sessionId, ffi.dialogManager) }
: handleOsPasswordAction(sessionId, ffi.dialogManager),
),
);
// paste // paste
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) { if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
v.add(TTextMenu( v.add(TTextMenu(
@@ -130,7 +138,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
if (isDesktop) { if (isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Transfer File')), child: Text(translate('Transfer file')),
onPressed: () => connect(context, id, isFileTransfer: true)), onPressed: () => connect(context, id, isFileTransfer: true)),
); );
} }
@@ -138,7 +146,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
if (isDesktop) { if (isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('TCP Tunneling')), child: Text(translate('TCP tunneling')),
onPressed: () => connect(context, id, isTcpTunneling: true)), onPressed: () => connect(context, id, isTcpTunneling: true)),
); );
} }
@@ -173,7 +181,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
pi.platform == kPeerPlatformMacOS)) { pi.platform == kPeerPlatformMacOS)) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Restart Remote Device')), child: Text(translate('Restart remote device')),
onPressed: () => onPressed: () =>
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)), showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
); );
@@ -188,6 +196,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
} }
// blockUserInput // blockUserInput
if (ffi.ffiModel.keyboard && if (ffi.ffiModel.keyboard &&
ffi.ffiModel.permissions['block_input'] != false &&
pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
{ {
v.add(TTextMenu( v.add(TTextMenu(
@@ -206,7 +215,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
ffiModel.keyboard && ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS && pi.platform != kPeerPlatformMacOS &&
version_cmp(pi.version, '1.2.0') >= 0) { versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetDefaultSessionsCount(id: id) == 1) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Switch Sides')), child: Text(translate('Switch Sides')),
onPressed: () => onPressed: () =>
@@ -215,15 +225,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
// refresh // refresh
if (pi.version.isNotEmpty) { if (pi.version.isNotEmpty) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Refresh')), child: Text(translate('Refresh')),
onPressed: () => bind.sessionRefresh(sessionId: sessionId))); onPressed: () => sessionRefreshVideo(sessionId, pi),
));
} }
// record // record
var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
if (!isDesktop && if (!isDesktop &&
(ffi.recordingModel.start || (ffi.recordingModel.start || (perms["recording"] != false))) {
(perms["recording"] != false &&
(codecFormat == "VP8" || codecFormat == "VP9")))) {
v.add(TTextMenu( v.add(TTextMenu(
child: Row( child: Row(
children: [ children: [
@@ -375,7 +383,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
// show remote cursor // show remote cursor
if (pi.platform != kPeerPlatformAndroid && if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded && !ffi.canvasModel.cursorEmbedded &&
!pi.is_wayland) { !pi.isWayland) {
final state = ShowRemoteCursorState.find(id); final state = ShowRemoteCursorState.find(id);
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor'; final option = 'show-remote-cursor';
@@ -434,19 +442,23 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Mute')))); child: Text(translate('Mute'))));
} }
// file copy and paste // file copy and paste
if (Platform.isWindows && if (ffiModel.keyboard &&
pi.platform == kPeerPlatformWindows && perms['file'] != false &&
perms['file'] != false) { bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
final enabled = !ffiModel.viewOnly;
final option = 'enable-file-transfer'; final option = 'enable-file-transfer';
final value = final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
onChanged: (value) { onChanged: enabled
if (value == null) return; ? (value) {
bind.sessionToggleOption(sessionId: sessionId, value: option); if (value == null) return;
}, bind.sessionToggleOption(sessionId: sessionId, value: option);
child: Text(translate('Allow file copy and paste')))); }
: null,
child: Text(translate('Enable file copy and paste'))));
} }
// disable clipboard // disable clipboard
if (ffiModel.keyboard && perms['clipboard'] != false) { if (ffiModel.keyboard && perms['clipboard'] != false) {
@@ -467,34 +479,158 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
} }
// lock after session end // lock after session end
if (ffiModel.keyboard) { if (ffiModel.keyboard) {
final enabled = !ffiModel.viewOnly;
final option = 'lock-after-session-end'; final option = 'lock-after-session-end';
final value = final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
onChanged: (value) { onChanged: enabled
if (value == null) return; ? (value) {
bind.sessionToggleOption(sessionId: sessionId, value: option); if (value == null) return;
}, bind.sessionToggleOption(sessionId: sessionId, value: option);
}
: null,
child: Text(translate('Lock after session end')))); child: Text(translate('Lock after session end'))));
} }
// privacy mode
if (ffiModel.keyboard && pi.features.privacyMode) { if (useTextureRender &&
final option = 'privacy-mode'; pi.isSupportMultiDisplay &&
final rxValue = PrivacyModeState.find(id); PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
'Y';
v.add(TToggleMenu( v.add(TToggleMenu(
value: rxValue.value, value: value,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
if (ffiModel.pi.currentDisplay != 0) { bind.sessionSetDisplaysAsIndividualWindows(
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info', sessionId: sessionId, value: value ? 'Y' : '');
'Please switch to Display 1 first', '', ffi.dialogManager);
return;
}
bind.sessionToggleOption(sessionId: sessionId, value: option);
}, },
child: Text(translate('Privacy mode')))); child: Text(translate('Show displays as individual windows'))));
} }
final screenList = await getScreenRectList();
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) ==
'Y';
v.add(TToggleMenu(
value: value,
onChanged: (value) {
if (value == null) return;
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId, value: value ? 'Y' : '');
},
child: Text(translate('Use all my displays for the remote session'))));
}
// 444
final codec_format = ffi.qualityMonitorModel.data.codecFormat;
if (versionCmp(pi.version, "1.2.4") >= 0 &&
(codec_format == "AV1" || codec_format == "VP9")) {
final option = 'i444';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu(
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
bind.sessionChangePreferCodec(sessionId: sessionId);
},
child: Text(translate('True color (4:4:4)'))));
}
if (isMobile) {
v.addAll(toolbarKeyboardToggles(ffi));
}
return v;
}
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
List<TToggleMenu> toolbarPrivacyMode(
RxString privacyModeState, BuildContext context, String id, FFI ffi) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final sessionId = ffi.sessionId;
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
final enabled = !ffi.ffiModel.viewOnly;
return TToggleMenu(
value: privacyModeState.isNotEmpty,
onChanged: enabled
? (value) {
if (value == null) return;
if (ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,
'custom-nook-nocancel-hasclose',
'info',
'Please switch to Display 1 first',
'',
ffi.dialogManager);
return;
}
final option = 'privacy-mode';
toggleFunc(sessionId, option);
}
: null,
child: Text(translate('Privacy mode')));
}
final privacyModeImpls =
pi.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl]
as List<dynamic>?;
if (privacyModeImpls == null) {
return [
getDefaultMenu((sid, opt) async {
bind.sessionToggleOption(sessionId: sid, value: opt);
togglePrivacyModeTime = DateTime.now();
})
];
}
if (privacyModeImpls.isEmpty) {
return [];
}
if (privacyModeImpls.length == 1) {
final implKey = (privacyModeImpls[0] as List<dynamic>)[0] as String;
return [
getDefaultMenu((sid, opt) async {
bind.sessionTogglePrivacyMode(
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
togglePrivacyModeTime = DateTime.now();
})
];
} else {
return privacyModeImpls.map((e) {
final implKey = (e as List<dynamic>)[0] as String;
final implName = (e)[1] as String;
return TToggleMenu(
child: Text(translate(implName)),
value: privacyModeState.value == implKey,
onChanged: (value) {
if (value == null) return;
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);
});
}).toList();
}
}
List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final sessionId = ffi.sessionId;
List<TToggleMenu> v = [];
// swap key // swap key
if (ffiModel.keyboard && if (ffiModel.keyboard &&
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
@@ -502,13 +638,53 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final option = 'allow_swap_key'; final option = 'allow_swap_key';
final value = final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
onChanged(bool? value) {
if (value == null) return;
bind.sessionToggleOption(sessionId: sessionId, value: option);
}
final enabled = !ffi.ffiModel.viewOnly;
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
onChanged: (value) { onChanged: enabled ? onChanged : null,
if (value == null) return;
bind.sessionToggleOption(sessionId: sessionId, value: option);
},
child: Text(translate('Swap control-command key')))); child: Text(translate('Swap control-command key'))));
} }
// reverse mouse wheel
if (ffiModel.keyboard) {
var optionValue =
bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
if (optionValue == '') {
optionValue = bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
}
onChanged(bool? value) async {
if (value == null) return;
await bind.sessionSetReverseMouseWheel(
sessionId: sessionId, value: value ? 'Y' : 'N');
}
final enabled = !ffi.ffiModel.viewOnly;
v.add(TToggleMenu(
value: optionValue == 'Y',
onChanged: enabled ? onChanged : null,
child: Text(translate('Reverse mouse wheel'))));
}
// swap left right mouse
if (ffiModel.keyboard) {
final option = 'swap-left-right-mouse';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
onChanged(bool? value) {
if (value == null) return;
bind.sessionToggleOption(sessionId: sessionId, value: option);
}
final enabled = !ffi.ffiModel.viewOnly;
v.add(TToggleMenu(
value: value,
onChanged: enabled ? onChanged : null,
child: Text(translate('swap-left-right-mouse'))));
}
return v; return v;
} }

View File

@@ -3,15 +3,35 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
const int kMaxVirtualDisplayCount = 4;
const int kAllVirtualDisplay = -1;
const double kDesktopRemoteTabBarHeight = 28.0; const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1;
const int kMainWindowId = 0; const int kMainWindowId = 0;
const kAllDisplayValue = -1;
const kKeyLegacyMode = 'legacy';
const kKeyMapMode = 'map';
const kKeyTranslateMode = 'translate';
const String kPlatformAdditionsIsWayland = "is_wayland";
const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl";
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android"; const String kPeerPlatformAndroid = "Android";
const double kScrollbarThickness = 12.0;
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main"; const String kAppTypeMain = "main";
@@ -24,6 +44,8 @@ const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info"; const String kWindowGetWindowInfo = "get_window_info";
const String kWindowGetScreenList = "get_screen_list";
// This method is not used, maybe it can be removed.
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
const String kWindowActionRebuild = "rebuild"; const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide"; const String kWindowEventHide = "hide";
@@ -34,11 +56,13 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventCloseForSeparateWindow = "close_for_separate_window"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kWindowEventOpenMonitorSession = "open_monitor_session";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
@@ -54,6 +78,15 @@ const String kTabLabelSettingPage = "Settings";
const String kWindowPrefix = "wm_"; const String kWindowPrefix = "wm_";
const int kWindowMainId = 0; const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
const String kKeyShowDisplaysAsIndividualWindows =
'displays_as_individual_windows';
const String kKeyUseAllMyDisplaysForTheRemoteSession =
'use_all_my_displays_for_the_remote_session';
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
// the executable name of the portable version // the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
@@ -68,16 +101,29 @@ const int kDesktopDefaultDisplayHeight = 720;
const int kMobileMaxDisplaySize = 1280; const int kMobileMaxDisplaySize = 1280;
const int kDesktopMaxDisplaySize = 3840; const int kDesktopMaxDisplaySize = 3840;
const double kDesktopFileTransferNameColWidth = 200;
const double kDesktopFileTransferModifiedColWidth = 120;
const double kDesktopFileTransferMinimumWidth = 100;
const double kDesktopFileTransferMaximumWidth = 300;
const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferRowHeight = 30.0;
const double kDesktopFileTransferHeaderHeight = 25.0; const double kDesktopFileTransferHeaderHeight = 25.0;
const double kMinFps = 5;
const double kDefaultFps = 30;
const double kMaxFps = 120;
const double kMinQuality = 10;
const double kDefaultQuality = 50;
const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000;
double kNewWindowOffset = Platform.isWindows
? 56.0
: Platform.isLinux
? 50.0
: Platform.isMacOS
? 30.0
: 50.0;
EdgeInsets get kDragToResizeAreaPadding => EdgeInsets get kDragToResizeAreaPadding =>
!kUseCompatibleUiMode && Platform.isLinux !kUseCompatibleUiMode && Platform.isLinux
? stateGlobal.fullscreen || stateGlobal.isMaximized.value ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero ? EdgeInsets.zero
: EdgeInsets.all(5.0) : EdgeInsets.all(5.0)
: EdgeInsets.zero; : EdgeInsets.zero;
@@ -134,6 +180,12 @@ const kRemoteScrollStyleAuto = 'scrollauto';
/// [kRemoteScrollStyleBar] Scroll image with scroll bar. /// [kRemoteScrollStyleBar] Scroll image with scroll bar.
const kRemoteScrollStyleBar = 'scrollbar'; const kRemoteScrollStyleBar = 'scrollbar';
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
const kScrollModeDefault = 'default';
/// [kScrollModeReverse] Mouse or touchpad, the reverse scroll mode.
const kScrollModeReverse = 'reverse';
/// [kRemoteImageQualityBest] Best image quality. /// [kRemoteImageQualityBest] Best image quality.
const kRemoteImageQualityBest = 'best'; const kRemoteImageQualityBest = 'best';
@@ -154,6 +206,12 @@ const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true; const kIgnoreDpi = true;
// ================================ mobile ================================
// Magic numbers, maybe need to avoid it or use a better way to get them.
const kMobileDelaySoftKeyboard = Duration(milliseconds: 30);
const kMobileDelaySoftKeyboardFocus = Duration(milliseconds: 30);
/// Android constants /// Android constants
const kActionApplicationDetailsSettings = const kActionApplicationDetailsSettings =
"android.settings.APPLICATION_DETAILS_SETTINGS"; "android.settings.APPLICATION_DETAILS_SETTINGS";

View File

@@ -7,15 +7,16 @@ import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/formatter/id_formatter.dart'; import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../widgets/button.dart'; import '../widgets/button.dart';
@@ -33,18 +34,18 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Controller for the id input bar. /// Controller for the id input bar.
final _idController = IDTextEditingController(); final _idController = IDTextEditingController();
/// Nested scroll controller
final _scrollController = ScrollController();
Timer? _updateTimer; Timer? _updateTimer;
final RxBool _idInputFocused = false.obs; final RxBool _idInputFocused = false.obs;
final FocusNode _idFocusNode = FocusNode();
var svcStopped = Get.find<RxBool>(tag: 'stop-service'); var svcStopped = Get.find<RxBool>(tag: 'stop-service');
var svcIsUsingPublicServer = true.obs; var svcIsUsingPublicServer = true.obs;
bool isWindowMinimized = false; bool isWindowMinimized = false;
List<Peer> peers = [];
bool isPeersLoading = false;
bool isPeersLoaded = false;
@override @override
void initState() { void initState() {
@@ -62,12 +63,6 @@ class _ConnectionPageState extends State<ConnectionPage>
_updateTimer = periodic_immediate(Duration(seconds: 1), () async { _updateTimer = periodic_immediate(Duration(seconds: 1), () async {
updateStatus(); updateStatus();
}); });
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
// select all to faciliate removing text, just following the behavior of address input of chrome
_idController.selection = TextSelection(
baseOffset: 0, extentOffset: _idController.value.text.length);
});
Get.put<IDTextEditingController>(_idController); Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this); windowManager.addListener(this);
} }
@@ -80,6 +75,9 @@ class _ConnectionPageState extends State<ConnectionPage>
if (Get.isRegistered<IDTextEditingController>()) { if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>(); Get.delete<IDTextEditingController>();
} }
if (Get.isRegistered<TextEditingController>()) {
Get.delete<TextEditingController>();
}
super.dispose(); super.dispose();
} }
@@ -106,7 +104,8 @@ class _ConnectionPageState extends State<ConnectionPage>
@override @override
void onWindowLeaveFullScreen() { void onWindowLeaveFullScreen() {
// Restore edge border to default edge size. // Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value = kWindowEdgeSize; stateGlobal.resizeEdgeSize.value =
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
} }
@override @override
@@ -120,30 +119,18 @@ class _ConnectionPageState extends State<ConnectionPage>
return Column( return Column(
children: [ children: [
Expanded( Expanded(
child: DesktopScrollWrapper( child: Column(
scrollController: _scrollController, children: [
child: CustomScrollView( Row(
controller: _scrollController, children: [
physics: DraggableNeverScrollableScrollPhysics(), Flexible(child: _buildRemoteIDTextField(context)),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
Row(
children: [
Flexible(child: _buildRemoteIDTextField(context)),
],
).marginOnly(top: 22),
SizedBox(height: 12),
Divider().paddingOnly(right: 12),
])),
SliverFillRemaining(
hasScrollBody: false,
child: PeerTabPage().paddingOnly(right: 12.0),
)
], ],
).paddingOnly(left: 12.0), ).marginOnly(top: 22),
), SizedBox(height: 12),
), Divider().paddingOnly(right: 12),
Expanded(child: PeerTabPage()),
],
).paddingOnly(left: 12.0)),
const Divider(height: 1), const Divider(height: 1),
buildStatus() buildStatus()
], ],
@@ -157,8 +144,20 @@ class _ConnectionPageState extends State<ConnectionPage>
connect(context, id, isFileTransfer: isFileTransfer); connect(context, id, isFileTransfer: isFileTransfer);
} }
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
}
/// UI for the remote ID TextField. /// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists. /// Search for a peer.
Widget _buildRemoteIDTextField(BuildContext context) { Widget _buildRemoteIDTextField(BuildContext context) {
var w = Container( var w = Container(
width: 320 + 20 * 2, width: 320 + 20 * 2,
@@ -172,51 +171,192 @@ class _ConnectionPageState extends State<ConnectionPage>
Row( Row(
children: [ children: [
Expanded( Expanded(
child: AutoSizeText( child: Row(
translate('Control Remote Desktop'), children: [
maxLines: 1, AutoSizeText(
style: Theme.of(context) translate('Control Remote Desktop'),
.textTheme maxLines: 1,
.titleLarge style: Theme.of(context)
?.merge(TextStyle(height: 1)), .textTheme
), .titleLarge
), ?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
], ],
).marginOnly(bottom: 15), ).marginOnly(bottom: 15),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Obx( child: Autocomplete<Peer>(
() => TextField( optionsBuilder: (TextEditingValue textEditingValue) {
maxLength: 90, if (textEditingValue.text == '') {
autocorrect: false, return const Iterable<Peer>.empty();
enableSuggestions: false, } else if (peers.isEmpty && !isPeersLoaded) {
keyboardType: TextInputType.visiblePassword, Peer emptyPeer = Peer(
focusNode: _idFocusNode, id: '',
style: const TextStyle( username: '',
fontFamily: 'WorkSans', hostname: '',
fontSize: 22, alias: '',
height: 1.4, platform: '',
), tags: [],
maxLines: 1, hash: '',
cursorColor: forceAlwaysRelay: false,
Theme.of(context).textTheme.titleLarge?.color, rdpPort: '',
decoration: InputDecoration( rdpUsername: '',
filled: false, loginName: '',
counterText: '', );
hintText: _idInputFocused.value return [emptyPeer];
? null } else {
: translate('Enter Remote ID'), String textWithoutSpaces =
contentPadding: const EdgeInsets.symmetric( textEditingValue.text.replaceAll(" ", "");
horizontal: 15, vertical: 13)), if (int.tryParse(textWithoutSpaces) != null) {
controller: _idController, textEditingValue = TextEditingValue(
inputFormatters: [IDTextInputFormatter()], text: textWithoutSpaces,
onSubmitted: (s) { selection: textEditingValue.selection,
onConnect(); );
}, }
), String textToFind = textEditingValue.text.toLowerCase();
),
), return peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
.toLowerCase()
.contains(textToFind) ||
peer.hostname
.toLowerCase()
.contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind))
.toList();
}
},
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(fieldTextEditingController);
fieldFocusNode.addListener(() async {
_idInputFocused.value = fieldFocusNode.hasFocus;
if (fieldFocusNode.hasFocus && !isPeersLoading) {
_fetchPeers();
}
});
final textLength =
fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection =
TextSelection(baseOffset: 0, extentOffset: textLength);
return Obx(() => TextField(
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
focusNode: fieldFocusNode,
style: const TextStyle(
fontFamily: 'WorkSans',
fontSize: 22,
height: 1.4,
),
maxLines: 1,
cursorColor:
Theme.of(context).textTheme.titleLarge?.color,
decoration: InputDecoration(
filled: false,
counterText: '',
hintText: _idInputFocused.value
? null
: translate('Enter Remote ID'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 13)),
controller: fieldTextEditingController,
inputFormatters: [IDTextInputFormatter()],
onChanged: (v) {
_idController.id = v;
},
));
},
onSelected: (option) {
setState(() {
_idController.id = option.id;
FocusScope.of(context).unfocus();
});
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Peer> onSelected,
Iterable<Peer> options) {
double maxHeight = options.length * 50;
if (options.length == 1) {
maxHeight = 52;
} else if (options.length == 3) {
maxHeight = 146;
} else if (options.length == 4) {
maxHeight = 193;
}
maxHeight = maxHeight.clamp(0, 200);
return Align(
alignment: Alignment.topLeft,
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 5,
spreadRadius: 1,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Material(
elevation: 4,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
maxWidth: 319,
),
child: peers.isEmpty && isPeersLoading
? Container(
height: 80,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
),
))
: Padding(
padding:
const EdgeInsets.only(top: 5),
child: ListView(
children: options
.map((peer) =>
AutocompletePeerTile(
onSelect: () =>
onSelected(peer),
peer: peer))
.toList(),
),
),
),
))),
);
},
)),
], ],
), ),
Padding( Padding(
@@ -227,7 +367,7 @@ class _ConnectionPageState extends State<ConnectionPage>
Button( Button(
isOutline: true, isOutline: true,
onTap: () => onConnect(isFileTransfer: true), onTap: () => onConnect(isFileTransfer: true),
text: "Transfer File", text: "Transfer file",
), ),
const SizedBox( const SizedBox(
width: 17, width: 17,
@@ -280,7 +420,7 @@ class _ConnectionPageState extends State<ConnectionPage>
onTap: () async { onTap: () async {
await start_service(true); await start_service(true);
}, },
child: Text(translate("Start Service"), child: Text(translate("Start service"),
style: TextStyle( style: TextStyle(
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
fontSize: em))) fontSize: em)))
@@ -323,7 +463,7 @@ class _ConnectionPageState extends State<ConnectionPage>
} }
void onUsePublicServerGuide() { void onUsePublicServerGuide() {
const url = "https://rustdesk.com/blog/id-relay-set/"; const url = "https://rustdesk.com/pricing.html";
canLaunchUrlString(url).then((can) { canLaunchUrlString(url).then((can) {
if (can) { if (can) {
launchUrlString(url); launchUrlString(url);

View File

@@ -48,6 +48,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var watchIsInputMonitoring = false; var watchIsInputMonitoring = false;
var watchIsCanRecordAudio = false; var watchIsCanRecordAudio = false;
Timer? _updateTimer; Timer? _updateTimer;
bool isCardClosed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -186,12 +187,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
? Theme.of(context).scaffoldBackgroundColor ? Theme.of(context).scaffoldBackgroundColor
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
child: Tooltip( child: Tooltip(
message: translate('Settings'), message: translate('Settings'),
child: Icon( child: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
size: 20, size: 20,
color: hover.value ? textColor : textColor?.withOpacity(0.5), color: hover.value ? textColor : textColor?.withOpacity(0.5),
)), )),
), ),
), ),
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
@@ -255,27 +256,27 @@ class _DesktopHomePageState extends State<DesktopHomePage>
child: Obx(() => RotatedBox( child: Obx(() => RotatedBox(
quarterTurns: 2, quarterTurns: 2,
child: Tooltip( child: Tooltip(
message: translate('Refresh Password'), message: translate('Refresh Password'),
child: Icon( child: Icon(
Icons.refresh, Icons.refresh,
color: refreshHover.value color: refreshHover.value
? textColor ? textColor
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
)) )))),
)),
onHover: (value) => refreshHover.value = value, onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
InkWell( InkWell(
child: Obx( child: Obx(
() => Tooltip( () => Tooltip(
message: translate('Change Password'), message: translate('Change Password'),
child: Icon( child: Icon(
Icons.edit, Icons.edit,
color: color: editHover.value
editHover.value ? textColor : Color(0xFFDDDDDD), ? textColor
size: 22, : Color(0xFFDDDDDD),
)).marginOnly(right: 8, top: 4), size: 22,
)).marginOnly(right: 8, top: 4),
), ),
onTap: () => DesktopSettingPage.switch2page(1), onTap: () => DesktopSettingPage.switch2page(1),
onHover: (value) => editHover.value = value, onHover: (value) => editHover.value = value,
@@ -321,25 +322,31 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
Future<Widget> buildHelpCards() async { Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty) { if (updateUrl.isNotEmpty && !isCardClosed) {
return buildInstallCard( return buildInstallCard(
"Status", "Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.", "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
"Click to download", () async { "Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download'); final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url); await launchUrl(url);
}); }, closeButton: true);
} }
if (systemError.isNotEmpty) { if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {}); return buildInstallCard("", systemError, "", () {});
} }
if (Platform.isWindows) { if (Platform.isWindows) {
if (!bind.mainIsInstalled()) { if (!bind.mainIsInstalled()) {
return buildInstallCard( return buildInstallCard("", "install_tip", "Install", () async {
"", "install_tip", "Install", bind.mainGotoInstall); await rustDeskWinManager.closeAllSubWindows();
bind.mainGotoInstall();
});
} else if (bind.mainIsInstalledLowerVersion()) { } else if (bind.mainIsInstalledLowerVersion()) {
return buildInstallCard("Status", "Your installation is lower version.", return buildInstallCard(
"Click to upgrade", bind.mainUpdateMe); "Status", "Your installation is lower version.", "Click to upgrade",
() async {
await rustDeskWinManager.closeAllSubWindows();
bind.mainUpdateMe();
});
} }
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
if (!bind.mainIsCanScreenRecording(prompt: false)) { if (!bind.mainIsCanScreenRecording(prompt: false)) {
@@ -377,16 +384,42 @@ class _DesktopHomePageState extends State<DesktopHomePage>
// }); // });
// } // }
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
final LinuxCards = <Widget>[];
if (bind.isSelinuxEnforcing()) {
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
final keyShowSelinuxHelpTip = "show-selinux-help-tip";
if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
LinuxCards.add(buildInstallCard(
"Warning",
"selinux_tip",
"",
() async {},
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
help: 'Help',
link:
'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
closeButton: true,
closeOption: keyShowSelinuxHelpTip,
));
}
}
if (bind.mainCurrentIsWayland()) { if (bind.mainCurrentIsWayland()) {
return buildInstallCard( LinuxCards.add(buildInstallCard(
"Warning", "wayland_experiment_tip", "", () async {}, "Warning", "wayland_experiment_tip", "", () async {},
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
help: 'Help', help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'); link: 'https://rustdesk.com/docs/en/client/linux/#x11-required'));
} else if (bind.mainIsLoginWayland()) { } else if (bind.mainIsLoginWayland()) {
return buildInstallCard("Warning", LinuxCards.add(buildInstallCard("Warning",
"Login screen using Wayland is not supported", "", () async {}, "Login screen using Wayland is not supported", "", () async {},
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
help: 'Help', help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen'); link: 'https://rustdesk.com/docs/en/client/linux/#login-screen'));
}
if (LinuxCards.isNotEmpty) {
return Column(
children: LinuxCards,
);
} }
} }
return Container(); return Container();
@@ -394,92 +427,128 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget buildInstallCard(String title, String content, String btnText, Widget buildInstallCard(String title, String content, String btnText,
GestureTapCallback onPressed, GestureTapCallback onPressed,
{String? help, String? link}) { {double marginTop = 20.0,
return Container( String? help,
margin: EdgeInsets.only(top: 20), String? link,
child: Container( bool? closeButton,
decoration: BoxDecoration( String? closeOption}) {
gradient: LinearGradient( void closeCard() async {
begin: Alignment.centerLeft, if (closeOption != null) {
end: Alignment.centerRight, await bind.mainSetLocalOption(key: closeOption, value: 'N');
colors: [ if (bind.mainGetLocalOption(key: closeOption) == 'N') {
Color.fromARGB(255, 226, 66, 188), setState(() {
Color.fromARGB(255, 244, 114, 124), isCardClosed = true;
], });
)), }
padding: EdgeInsets.all(20), } else {
child: Column( setState(() {
mainAxisAlignment: MainAxisAlignment.start, isCardClosed = true;
crossAxisAlignment: CrossAxisAlignment.start, });
children: (title.isNotEmpty }
? <Widget>[ }
Center(
child: Text( return Stack(
translate(title), children: [
style: TextStyle( Container(
color: Colors.white, margin: EdgeInsets.only(top: marginTop),
fontWeight: FontWeight.bold, child: Container(
fontSize: 15), decoration: BoxDecoration(
).marginOnly(bottom: 6)), gradient: LinearGradient(
] begin: Alignment.centerLeft,
: <Widget>[]) + end: Alignment.centerRight,
<Widget>[ colors: [
Text( Color.fromARGB(255, 226, 66, 188),
translate(content), Color.fromARGB(255, 244, 114, 124),
style: TextStyle( ],
height: 1.5, )),
color: Colors.white, padding: EdgeInsets.all(20),
fontWeight: FontWeight.normal, child: Column(
fontSize: 13), mainAxisAlignment: MainAxisAlignment.start,
).marginOnly(bottom: 20) crossAxisAlignment: CrossAxisAlignment.start,
] + children: (title.isNotEmpty
(btnText.isNotEmpty ? <Widget>[
? <Widget>[ Center(
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FixedWidthButton(
width: 150,
padding: 8,
isOutline: true,
text: translate(btnText),
textColor: Colors.white,
borderColor: Colors.white,
textSize: 20,
radius: 10,
onTap: onPressed,
)
])
]
: <Widget>[]) +
(help != null
? <Widget>[
Center(
child: InkWell(
onTap: () async =>
await launchUrl(Uri.parse(link!)),
child: Text( child: Text(
translate(help), translate(title),
style: TextStyle( style: TextStyle(
decoration: TextDecoration.underline, color: Colors.white,
color: Colors.white, fontWeight: FontWeight.bold,
fontSize: 12), fontSize: 15),
)).marginOnly(top: 6)), ).marginOnly(bottom: 6)),
] ]
: <Widget>[]))), : <Widget>[]) +
<Widget>[
Text(
translate(content),
style: TextStyle(
height: 1.5,
color: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 13),
).marginOnly(bottom: 20)
] +
(btnText.isNotEmpty
? <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FixedWidthButton(
width: 150,
padding: 8,
isOutline: true,
text: translate(btnText),
textColor: Colors.white,
borderColor: Colors.white,
textSize: 20,
radius: 10,
onTap: onPressed,
)
])
]
: <Widget>[]) +
(help != null
? <Widget>[
Center(
child: InkWell(
onTap: () async =>
await launchUrl(Uri.parse(link!)),
child: Text(
translate(help),
style: TextStyle(
decoration:
TextDecoration.underline,
color: Colors.white,
fontSize: 12),
)).marginOnly(top: 6)),
]
: <Widget>[]))),
),
if (closeButton != null && closeButton == true)
Positioned(
top: 18,
right: 0,
child: IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
size: 20,
),
onPressed: closeCard,
),
),
],
); );
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
});
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID(); await gFFI.serverModel.fetchID();
final url = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl != url) {
updateUrl = url;
setState(() {});
}
final error = await bind.mainGetError(); final error = await bind.mainGetError();
if (systemError != error) { if (systemError != error) {
systemError = error; systemError = error;
@@ -530,6 +599,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Get.put<RxBool>(svcStopped, tag: 'stop-service'); Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
screenToMap(window_size.Screen screen) => {
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
};
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint( debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
@@ -538,24 +623,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} else if (call.method == kWindowGetWindowInfo) { } else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen; final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) { if (screen == null) {
return ""; return '';
} else { } else {
return jsonEncode({ return jsonEncode(screenToMap(screen));
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
});
} }
} else if (call.method == kWindowGetScreenList) {
return jsonEncode(
(await window_size.getScreenList()).map(screenToMap).toList());
} else if (call.method == kWindowActionRebuild) { } else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow(); reloadCurrentWindow();
} else if (call.method == kWindowEventShow) { } else if (call.method == kWindowEventShow) {
@@ -579,8 +653,18 @@ class _DesktopHomePageState extends State<DesktopHomePage>
debugPrint("Failed to parse window id '${call.arguments}': $e"); debugPrint("Failed to parse window id '${call.arguments}': $e");
} }
if (windowId != null) { if (windowId != null) {
await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]); await rustDeskWinManager.moveTabToNewWindow(
windowId, args[1], args[2]);
} }
} else if (call.method == kWindowEventOpenMonitorSession) {
final args = jsonDecode(call.arguments);
final windowId = args['window_id'] as int;
final peerId = args['peer_id'] as String;
final display = args['display'] as int;
final displayCount = args['display_count'] as int;
final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect);
} }
}); });
_uniLinksSubscription = listenUniLinks(); _uniLinksSubscription = listenUniLinks();

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
@@ -5,6 +6,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
@@ -17,7 +19,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:window_manager/window_manager.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart'; import '../../common/widgets/login.dart';
@@ -88,6 +89,11 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag); Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
controller = PageController(initialPage: widget.initialPage); controller = PageController(initialPage: widget.initialPage);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag); Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() {
if (controller.page != null) {
selectedIndex.value = controller.page!.toInt();
}
});
} }
@override @override
@@ -154,7 +160,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
scrollController: controller, scrollController: controller,
child: PageView( child: PageView(
controller: controller, controller: controller,
physics: DraggableNeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: _children(), children: _children(),
)), )),
), ),
@@ -317,6 +323,7 @@ class _GeneralState extends State<_General> {
'enable-confirm-closing-tabs', 'enable-confirm-closing-tabs',
isServer: false), isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'), _OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
wallpaper(),
_OptionCheckBox( _OptionCheckBox(
context, context,
'Open connection in new tab', 'Open connection in new tab',
@@ -330,6 +337,12 @@ class _GeneralState extends State<_General> {
child: _OptionCheckBox(context, "Always use software rendering", child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'), 'allow-always-software-render'),
)); ));
children.add(_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
));
if (bind.mainShowOption(key: 'allow-linux-headless')) { if (bind.mainShowOption(key: 'allow-linux-headless')) {
children.add(_OptionCheckBox( children.add(_OptionCheckBox(
context, 'Allow linux headless', 'allow-linux-headless')); context, 'Allow linux headless', 'allow-linux-headless'));
@@ -337,11 +350,49 @@ class _GeneralState extends State<_General> {
return _Card(title: 'Other', children: children); return _Card(title: 'Other', children: children);
} }
Widget wallpaper() {
return futureBuilder(future: () async {
final support = await bind.mainSupportRemoveWallpaper();
return support;
}(), hasData: (data) {
if (data is bool && data == true) {
final option = 'allow-remove-wallpaper';
bool value = mainGetBoolOptionSync(option);
return Row(
children: [
Flexible(
child: _OptionCheckBox(
context,
'Remove wallpaper during incoming sessions',
option,
update: () {
setState(() {});
},
),
),
if (value)
_CountDownButton(
text: 'Test',
second: 5,
onPressed: () {
bind.mainTestWallpaper(second: 5);
},
)
],
);
}
return Offstage();
});
}
Widget hwcodec() { Widget hwcodec() {
final hwcodec = bind.mainHasHwcodec();
final gpucodec = bind.mainHasGpucodec();
return Offstage( return Offstage(
offstage: !bind.mainHasHwcodec(), offstage: !(hwcodec || gpucodec),
child: _Card(title: 'Hardware Codec', children: [ child: _Card(title: 'Hardware Codec', children: [
_OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec'), _OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec')
]), ]),
); );
} }
@@ -463,7 +514,7 @@ class _GeneralState extends State<_General> {
if (!keys.contains(currentKey)) { if (!keys.contains(currentKey)) {
currentKey = ''; currentKey = '';
} }
return _ComboBox( return ComboBox(
keys: keys, keys: keys,
values: values, values: values,
initialKey: currentKey, initialKey: currentKey,
@@ -515,6 +566,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: Column(children: [ child: Column(children: [
permissions(context), permissions(context),
password(context), password(context),
_Card(title: '2FA', children: [tfa()]),
_Card(title: 'ID', children: [changeId()]), _Card(title: 'ID', children: [changeId()]),
more(context), more(context),
]), ]),
@@ -523,6 +575,45 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
)).marginOnly(bottom: _kListViewBottomMargin)); )).marginOnly(bottom: _kListViewBottomMargin));
} }
Widget tfa() {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool has2fa = bind.mainHasValid2FaSync().obs;
update() async {
has2fa.value = bind.mainHasValid2FaSync();
}
onChanged(bool? checked) async {
change2fa(callback: update);
}
return GestureDetector(
child: InkWell(
child: Obx(() => Row(
children: [
Checkbox(
value: has2fa.value,
onChanged: enabled ? onChanged : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('enable-2fa-title'),
style:
TextStyle(color: disabledTextColor(context, enabled)),
))
],
)),
),
onTap: () {
onChanged(!has2fa.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
}
return tmpWrapper();
}
Widget changeId() { Widget changeId() {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: gFFI.serverModel, value: gFFI.serverModel,
@@ -563,7 +654,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
} }
return _Card(title: 'Permissions', children: [ return _Card(title: 'Permissions', children: [
_ComboBox( ComboBox(
keys: [ keys: [
'', '',
'full', 'full',
@@ -582,23 +673,27 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
}).marginOnly(left: _kContentHMargin), }).marginOnly(left: _kContentHMargin),
Column( Column(
children: [ children: [
_OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard', _OptionCheckBox(context, 'Enable keyboard/mouse', 'enable-keyboard',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', _OptionCheckBox(context, 'Enable clipboard', 'enable-clipboard',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox( _OptionCheckBox(
context, 'Enable File Transfer', 'enable-file-transfer', context, 'Enable file transfer', 'enable-file-transfer',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable Audio', 'enable-audio', _OptionCheckBox(context, 'Enable audio', 'enable-audio',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', _OptionCheckBox(context, 'Enable TCP tunneling', 'enable-tunnel',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox( _OptionCheckBox(
context, 'Enable Remote Restart', 'enable-remote-restart', context, 'Enable remote restart', 'enable-remote-restart',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox( _OptionCheckBox(
context, 'Enable Recording Session', 'enable-record-session', context, 'Enable recording session', 'enable-record-session',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
if (Platform.isWindows)
_OptionCheckBox(
context, 'Enable blocking user input', 'enable-block-input',
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable remote configuration modification', _OptionCheckBox(context, 'Enable remote configuration modification',
'allow-remote-config-modification', 'allow-remote-config-modification',
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
@@ -666,7 +761,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Text( Text(
value, value,
style: TextStyle( style: TextStyle(
color: _disabledTextColor( color: disabledTextColor(
context, onChanged != null)), context, onChanged != null)),
), ),
], ],
@@ -686,7 +781,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
final usePassword = model.approveMode != 'click'; final usePassword = model.approveMode != 'click';
return _Card(title: 'Password', children: [ return _Card(title: 'Password', children: [
_ComboBox( ComboBox(
enabled: !locked, enabled: !locked,
keys: modeKeys, keys: modeKeys,
values: modeValues, values: modeValues,
@@ -708,8 +803,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword) if (usePassword)
_SubButton('Set permanent password', setPasswordDialog, _SubButton('Set permanent password', setPasswordDialog,
permEnabled && !locked), permEnabled && !locked),
if (usePassword) // if (usePassword)
hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), // hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
if (usePassword) radios[2], if (usePassword) radios[2],
]); ]);
}))); })));
@@ -718,16 +813,12 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget more(BuildContext context) { Widget more(BuildContext context) {
bool enabled = !locked; bool enabled = !locked;
return _Card(title: 'Security', children: [ return _Card(title: 'Security', children: [
Offstage(
offstage: !Platform.isWindows,
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
enabled: enabled),
),
shareRdp(context, enabled), shareRdp(context, enabled),
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery', _OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled), reverse: true, enabled: enabled),
...directIp(context), ...directIp(context),
whitelist(), whitelist(),
...autoDisconnect(context),
]); ]);
} }
@@ -750,7 +841,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Expanded( Expanded(
child: Text(translate('Enable RDP session sharing'), child: Text(translate('Enable RDP session sharing'),
style: style:
TextStyle(color: _disabledTextColor(context, enabled))), TextStyle(color: disabledTextColor(context, enabled))),
) )
], ],
).marginOnly(left: _kCheckBoxLeftMargin), ).marginOnly(left: _kCheckBoxLeftMargin),
@@ -763,7 +854,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
update() => setState(() {}); update() => setState(() {});
RxBool applyEnabled = false.obs; RxBool applyEnabled = false.obs;
return [ return [
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', _OptionCheckBox(context, 'Enable direct IP access', 'direct-server',
update: update, enabled: !locked), update: update, enabled: !locked),
() { () {
// Simple temp wrapper for PR check // Simple temp wrapper for PR check
@@ -853,7 +944,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: Text( child: Text(
translate('Use IP Whitelisting'), translate('Use IP Whitelisting'),
style: style:
TextStyle(color: _disabledTextColor(context, enabled)), TextStyle(color: disabledTextColor(context, enabled)),
)) ))
], ],
)), )),
@@ -897,7 +988,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: Text( child: Text(
translate('Hide connection management window'), translate('Hide connection management window'),
style: TextStyle( style: TextStyle(
color: _disabledTextColor( color: disabledTextColor(
context, enabled && enableHideCm)), context, enabled && enableHideCm)),
), ),
), ),
@@ -906,6 +997,63 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
)); ));
})); }));
} }
List<Widget> autoDisconnect(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
RxBool applyEnabled = false.obs;
final optionKey = 'allow-auto-disconnect';
final timeoutKey = 'auto-disconnect-timeout';
return [
_OptionCheckBox(context, 'auto_disconnect_option_tip', optionKey,
update: update, enabled: !locked),
() {
bool enabled =
option2bool(optionKey, bind.mainGetOptionSync(key: optionKey));
if (!enabled) applyEnabled.value = false;
controller.text = bind.mainGetOptionSync(key: timeoutKey);
return Offstage(
offstage: !enabled,
child: _SubLabeledWidget(
context,
'Timeout in minutes',
Row(children: [
SizedBox(
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked,
onChanged: (_) => applyEnabled.value = true,
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(
hintText: '10',
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value && enabled && !locked
? () async {
applyEnabled.value = false;
await bind.mainSetOption(
key: timeoutKey, value: controller.text);
}
: null,
child: Text(
translate('Apply'),
),
))
]),
enabled: enabled && !locked,
),
);
}(),
];
}
} }
class _Network extends StatefulWidget { class _Network extends StatefulWidget {
@@ -952,8 +1100,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
// Simple temp wrapper for PR check // Simple temp wrapper for PR check
tmpWrapper() { tmpWrapper() {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting. // Setting page is not modal, oldOptions should only be used when getting options, never when setting.
Map<String, dynamic> oldOptions = Map<String, dynamic> oldOptions = jsonDecode(bind.mainGetOptionsSync());
jsonDecode(bind.mainGetOptionsSync() as String);
old(String key) { old(String key) {
return (oldOptions[key] ?? '').trim(); return (oldOptions[key] ?? '').trim();
} }
@@ -966,54 +1113,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
var relayController = TextEditingController(text: old('relay-server')); var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server')); var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key')); var keyController = TextEditingController(text: old('key'));
final controllers = [
set(String idServer, String relayServer, String apiServer, idController,
String key) async { relayController,
idServer = idServer.trim(); apiController,
relayServer = relayServer.trim(); keyController,
apiServer = apiServer.trim(); ];
key = key.trim(); final errMsgs = [
if (idServer.isNotEmpty) { idErrMsg,
idErrMsg.value = relayErrMsg,
translate(await bind.mainTestIfValidServer(server: idServer)); apiErrMsg,
if (idErrMsg.isNotEmpty) { ];
return false;
}
}
if (relayServer.isNotEmpty) {
relayErrMsg.value =
translate(await bind.mainTestIfValidServer(server: relayServer));
if (relayErrMsg.isNotEmpty) {
return false;
}
}
if (apiServer.isNotEmpty) {
if (!apiServer.startsWith('http://') &&
!apiServer.startsWith('https://')) {
apiErrMsg.value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final oldApiServer = await bind.mainGetApiServer();
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
await bind.mainSetOption(key: 'relay-server', value: relayServer);
await bind.mainSetOption(key: 'api-server', value: apiServer);
await bind.mainSetOption(key: 'key', value: key);
final newApiServer = await bind.mainGetApiServer();
if (oldApiServer.isNotEmpty && oldApiServer != newApiServer) {
await gFFI.userModel.logOut(apiServer: oldApiServer);
}
return true;
}
submit() async { submit() async {
bool result = await set(idController.text, relayController.text, bool result = await setServerConfig(
apiController.text, keyController.text); null,
errMsgs,
ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text));
if (result) { if (result) {
setState(() {}); setState(() {});
showToast(translate('Successful')); showToast(translate('Successful'));
@@ -1022,83 +1142,28 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
} }
} }
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
idController.text = sc.idServer;
relayController.text = sc.relayServer;
apiController.text = sc.apiServer;
keyController.text = sc.key;
Future<bool> success =
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
success.then((value) {
if (value) {
showToast(
translate('Import server configuration successfully'));
} else {
showToast(translate('Invalid server configuration'));
}
});
} else {
showToast(translate('Invalid server configuration'));
}
} catch (e) {
showToast(translate('Invalid server configuration'));
}
} else {
showToast(translate('Clipboard is empty'));
}
});
}
export() {
final text = ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text)
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
bool secure = !enabled; bool secure = !enabled;
return _Card(title: 'ID/Relay Server', title_suffix: [ return _Card(
Tooltip( title: 'ID/Relay Server',
message: translate('Import Server Config'), title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey),
onPressed: enabled ? import : null),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton(
icon: Icon(Icons.copy, color: Colors.grey),
onPressed: enabled ? export : null)),
], children: [
Column(
children: [ children: [
Obx(() => _LabeledTextField(context, 'ID Server', idController, Column(
idErrMsg.value, enabled, secure)), children: [
Obx(() => _LabeledTextField(context, 'Relay Server', Obx(() => _LabeledTextField(context, 'ID Server', idController,
relayController, relayErrMsg.value, enabled, secure)), idErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'API Server', apiController, Obx(() => _LabeledTextField(context, 'Relay Server',
apiErrMsg.value, enabled, secure)), relayController, relayErrMsg.value, enabled, secure)),
_LabeledTextField( Obx(() => _LabeledTextField(context, 'API Server',
context, 'Key', keyController, '', enabled, secure), apiController, apiErrMsg.value, enabled, secure)),
Row( _LabeledTextField(
mainAxisAlignment: MainAxisAlignment.end, context, 'Key', keyController, '', enabled, secure),
children: [_Button('Apply', submit, enabled: enabled)], Row(
).marginOnly(top: 10), mainAxisAlignment: MainAxisAlignment.end,
], children: [_Button('Apply', submit, enabled: enabled)],
) ).marginOnly(top: 10),
]); ],
)
]);
} }
return tmpWrapper(); return tmpWrapper();
@@ -1126,6 +1191,7 @@ class _DisplayState extends State<_Display> {
scrollStyle(context), scrollStyle(context),
imageQuality(context), imageQuality(context),
codec(context), codec(context),
privacyModeImpl(context),
other(context), other(context),
]).marginOnly(bottom: _kListViewBottomMargin)); ]).marginOnly(bottom: _kListViewBottomMargin));
} }
@@ -1182,15 +1248,6 @@ class _DisplayState extends State<_Display> {
} }
final groupValue = bind.mainGetUserDefaultOption(key: key); final groupValue = bind.mainGetUserDefaultOption(key: key);
final qualityKey = 'custom_image_quality';
final qualityValue =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
50.0)
.obs;
final fpsKey = 'custom-fps';
final fpsValue =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
.obs;
return _Card(title: 'Default Image Quality', children: [ return _Card(title: 'Default Image Quality', children: [
_Radio(context, _Radio(context,
value: kRemoteImageQualityBest, value: kRemoteImageQualityBest,
@@ -1214,64 +1271,7 @@ class _DisplayState extends State<_Display> {
onChanged: onChanged), onChanged: onChanged),
Offstage( Offstage(
offstage: groupValue != kRemoteImageQualityCustom, offstage: groupValue != kRemoteImageQualityCustom,
child: Column( child: customImageQualitySetting(),
children: [
Obx(() => Row(
children: [
Slider(
value: qualityValue.value,
min: 10.0,
max: 100.0,
divisions: 18,
onChanged: (double value) async {
qualityValue.value = value;
await bind.mainSetUserDefaultOption(
key: qualityKey, value: value.toString());
},
),
SizedBox(
width: 40,
child: Text(
'${qualityValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
SizedBox(
width: 50,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
))
],
)),
Obx(() => Row(
children: [
Slider(
value: fpsValue.value,
min: 5.0,
max: 120.0,
divisions: 23,
onChanged: (double value) async {
fpsValue.value = value;
await bind.mainSetUserDefaultOption(
key: fpsKey, value: value.toString());
},
),
SizedBox(
width: 40,
child: Text(
'${fpsValue.value.round()}',
style: const TextStyle(fontSize: 15),
)),
SizedBox(
width: 50,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
)),
],
),
) )
]); ]);
} }
@@ -1331,6 +1331,42 @@ class _DisplayState extends State<_Display> {
]); ]);
} }
Widget privacyModeImpl(BuildContext context) {
final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls();
late final List<dynamic> privacyModeImpls;
try {
privacyModeImpls = jsonDecode(supportedPrivacyModeImpls);
} catch (e) {
debugPrint('failed to parse supported privacy mode impls, err=$e');
return Offstage();
}
if (privacyModeImpls.length < 2) {
return Offstage();
}
final key = 'privacy-mode-impl-key';
onChanged(String value) async {
await bind.mainSetOption(key: key, value: value);
setState(() {});
}
String groupValue = bind.mainGetOptionSync(key: key);
if (groupValue.isEmpty) {
groupValue = bind.mainDefaultPrivacyModeImpl();
}
return _Card(
title: 'Privacy mode',
children: privacyModeImpls.map((impl) {
final d = impl as List<dynamic>;
return _Radio(context,
value: d[0] as String,
groupValue: groupValue,
label: d[1] as String,
onChanged: onChanged);
}).toList(),
);
}
Widget otherRow(String label, String key) { Widget otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
onChanged(bool b) async { onChanged(bool b) async {
@@ -1352,19 +1388,9 @@ class _DisplayState extends State<_Display> {
} }
Widget other(BuildContext context) { Widget other(BuildContext context) {
return _Card(title: 'Other Default Options', children: [ final children =
otherRow('View Mode', 'view_only'), otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList();
otherRow('show_monitors_tip', 'show_monitors_toolbar'), return _Card(title: 'Other Default Options', children: children);
otherRow('Collapse toolbar', 'collapse_toolbar'),
otherRow('Show remote cursor', 'show_remote_cursor'),
otherRow('Zoom cursor', 'zoom-cursor'),
otherRow('Show quality monitor', 'show_quality_monitor'),
otherRow('Mute', 'disable_audio'),
otherRow('Allow file copy and paste', 'enable_file_transfer'),
otherRow('Disable clipboard', 'disable_clipboard'),
otherRow('Lock after session end', 'lock_after_session_end'),
otherRow('Privacy mode', 'privacy_mode'),
]);
} }
} }
@@ -1660,12 +1686,6 @@ Widget _Card(
); );
} }
Color? _disabledTextColor(BuildContext context, bool enabled) {
return enabled
? null
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
}
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Widget _OptionCheckBox(BuildContext context, String label, String key, Widget _OptionCheckBox(BuildContext context, String label, String key,
{Function()? update, {Function()? update,
@@ -1684,9 +1704,14 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
isServer isServer
? await mainSetBoolOption(key, option) ? await mainSetBoolOption(key, option)
: await mainSetLocalBoolOption(key, option); : await mainSetLocalBoolOption(key, option);
ref.value = isServer final readOption = isServer
? mainGetBoolOptionSync(key) ? mainGetBoolOptionSync(key)
: mainGetLocalBoolOptionSync(key); : mainGetLocalBoolOptionSync(key);
if (reverse) {
ref.value = !readOption;
} else {
ref.value = readOption;
}
update?.call(); update?.call();
} }
} }
@@ -1709,7 +1734,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
Expanded( Expanded(
child: Text( child: Text(
translate(label), translate(label),
style: TextStyle(color: _disabledTextColor(context, enabled)), style: TextStyle(color: disabledTextColor(context, enabled)),
)) ))
], ],
), ),
@@ -1746,7 +1771,7 @@ Widget _Radio<T>(BuildContext context,
overflow: autoNewLine ? null : TextOverflow.ellipsis, overflow: autoNewLine ? null : TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: _kContentFontSize, fontSize: _kContentFontSize,
color: _disabledTextColor(context, enabled))) color: disabledTextColor(context, enabled)))
.marginOnly(left: 5), .marginOnly(left: 5),
), ),
], ],
@@ -1796,7 +1821,7 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
children: [ children: [
Text( Text(
'${translate(label)}: ', '${translate(label)}: ',
style: TextStyle(color: _disabledTextColor(context, enabled)), style: TextStyle(color: disabledTextColor(context, enabled)),
), ),
SizedBox( SizedBox(
width: 10, width: 10,
@@ -1860,7 +1885,7 @@ _LabeledTextField(
'${translate(label)}:', '${translate(label)}:',
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle( style: TextStyle(
fontSize: 16, color: _disabledTextColor(context, enabled)), fontSize: 16, color: disabledTextColor(context, enabled)),
).marginOnly(right: 10)), ).marginOnly(right: 10)),
Expanded( Expanded(
child: TextField( child: TextField(
@@ -1870,84 +1895,73 @@ _LabeledTextField(
decoration: InputDecoration( decoration: InputDecoration(
errorText: errorText.isNotEmpty ? errorText : null), errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle( style: TextStyle(
color: _disabledTextColor(context, enabled), color: disabledTextColor(context, enabled),
)), )),
), ),
], ],
).marginOnly(bottom: 8); ).marginOnly(bottom: 8);
} }
// ignore: must_be_immutable class _CountDownButton extends StatefulWidget {
class _ComboBox extends StatelessWidget { _CountDownButton({
late final List<String> keys;
late final List<String> values;
late final String initialKey;
late final Function(String key) onChanged;
late final bool enabled;
late String current;
_ComboBox({
Key? key, Key? key,
required this.keys, required this.text,
required this.values, required this.second,
required this.initialKey, required this.onPressed,
required this.onChanged,
this.enabled = true,
}) : super(key: key); }) : super(key: key);
final String text;
final VoidCallback? onPressed;
final int second;
@override
State<_CountDownButton> createState() => _CountDownButtonState();
}
class _CountDownButtonState extends State<_CountDownButton> {
bool _isButtonDisabled = false;
late int _countdownSeconds = widget.second;
Timer? _timer;
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startCountdownTimer() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdownSeconds <= 0) {
setState(() {
_isButtonDisabled = false;
});
timer.cancel();
} else {
setState(() {
_countdownSeconds--;
});
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var index = keys.indexOf(initialKey); return ElevatedButton(
if (index < 0) { onPressed: _isButtonDisabled
index = 0; ? null
} : () {
var ref = values[index].obs; widget.onPressed?.call();
current = keys[index]; setState(() {
return Container( _isButtonDisabled = true;
decoration: BoxDecoration( _countdownSeconds = widget.second;
border: Border.all( });
color: enabled _startCountdownTimer();
? MyTheme.color(context).border2 ?? MyTheme.border },
: MyTheme.border, child: Text(
), _isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text),
borderRadius:
BorderRadius.circular(8), //border raiuds of dropdown button
), ),
height: 42, // should be the height of a TextField );
child: Obx(() => DropdownButton<String>(
isExpanded: true,
value: ref.value,
elevation: 16,
underline: Container(),
style: TextStyle(
color: enabled
? Theme.of(context).textTheme.titleMedium?.color
: _disabledTextColor(context, enabled)),
icon: const Icon(
Icons.expand_more_sharp,
size: 20,
).marginOnly(right: 15),
onChanged: enabled
? (String? newValue) {
if (newValue != null && newValue != ref.value) {
ref.value = newValue;
current = newValue;
onChanged(keys[values.indexOf(newValue)]);
}
}
: null,
items: values.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: const TextStyle(fontSize: _kContentFontSize),
overflow: TextOverflow.ellipsis,
).marginOnly(left: 15),
);
}).toList(),
)),
).marginOnly(bottom: 5);
} }
} }

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