mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-12 23:41:02 +03:00
Compare commits
6 Commits
hdr-tonema
...
fix-textur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a16d9a30 | ||
|
|
8fc82d04ac | ||
|
|
c6c001a15e | ||
|
|
7da2bbe6ac | ||
|
|
c5adac828b | ||
|
|
7c23e1f4b9 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gitsubmodule"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: "master"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
commit-message:
|
||||||
|
prefix: "Git submodule"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -5,7 +5,7 @@ env:
|
|||||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# for multiarch gcc compatibility
|
# for multiarch gcc compatibility
|
||||||
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -124,6 +124,7 @@ jobs:
|
|||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
|
libpam0g-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libunwind-dev \
|
libunwind-dev \
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
|
|||||||
15
.github/workflows/flutter-build.yml
vendored
15
.github/workflows/flutter-build.yml
vendored
@@ -31,20 +31,20 @@ env:
|
|||||||
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
|
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
|
||||||
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
|
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
|
||||||
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
|
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
|
||||||
FLUTTER_WINDOWS_ARM_VERSION: "3.44.9"
|
FLUTTER_WINDOWS_ARM_VERSION: "3.44.8"
|
||||||
# for arm64 linux because official Dart SDK does not work
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2026.07.29
|
# vcpkg version: 2025.08.27
|
||||||
# If we change the `VCPKG COMMIT_ID`, please remember:
|
# If we change the `VCPKG COMMIT_ID`, please remember:
|
||||||
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
|
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
|
||||||
# Or we may face build issue like
|
# Or we may face build issue like
|
||||||
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
|
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
|
||||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||||
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||||
VERSION: "1.5.0"
|
VERSION: "1.4.9"
|
||||||
NDK_VERSION: "r28c"
|
NDK_VERSION: "r28c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -1009,6 +1009,7 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -1282,6 +1283,7 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -1572,6 +1574,7 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -1893,6 +1896,7 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -2122,6 +2126,7 @@ jobs:
|
|||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -2198,7 +2203,7 @@ jobs:
|
|||||||
mkdir -p ~/.cargo/
|
mkdir -p ~/.cargo/
|
||||||
echo """
|
echo """
|
||||||
[source.crates-io]
|
[source.crates-io]
|
||||||
registry = 'sparse+https://index.crates.io/'
|
registry = 'https://github.com/rust-lang/crates.io-index'
|
||||||
""" > ~/.cargo/config
|
""" > ~/.cargo/config
|
||||||
cat ~/.cargo/config
|
cat ~/.cargo/config
|
||||||
# install dependencies from vcpkg
|
# install dependencies from vcpkg
|
||||||
|
|||||||
11
.github/workflows/playground.yml
vendored
11
.github/workflows/playground.yml
vendored
@@ -16,8 +16,8 @@ env:
|
|||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
VERSION: "1.5.0"
|
VERSION: "1.4.9"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -271,6 +271,7 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libvdpau-dev \
|
libvdpau-dev \
|
||||||
@@ -283,7 +284,7 @@ jobs:
|
|||||||
nasm \
|
nasm \
|
||||||
yasm \
|
yasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openjdk-17-jdk-headless \
|
openjdk-11-jdk-headless \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
tree \
|
tree \
|
||||||
wget
|
wget
|
||||||
@@ -365,9 +366,9 @@ jobs:
|
|||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||||
run: |
|
run: |
|
||||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||||
# temporary use debug sign config
|
# temporary use debug sign config
|
||||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||||
case ${{ matrix.job.target }} in
|
case ${{ matrix.job.target }} in
|
||||||
|
|||||||
4
.github/workflows/update-webpki-roots.yml
vendored
4
.github/workflows/update-webpki-roots.yml
vendored
@@ -33,10 +33,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
|
||||||
# The root workspace lists libs/hbb_common as a member; without the
|
|
||||||
# submodule its manifest is missing and cargo cannot load the workspace.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Update webpki-roots in all lockfiles
|
- name: Update webpki-roots in all lockfiles
|
||||||
id: update
|
id: update
|
||||||
|
|||||||
15
AGENTS.md
15
AGENTS.md
@@ -63,24 +63,17 @@
|
|||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
|
|
||||||
* Avoid comments unless they explain a non-obvious reason, constraint, or workaround.
|
* Keep them short: one line by default, three at most.
|
||||||
* Never restate what the code does; prefer clearer code instead.
|
* Say **why**, never what. If the code already says it, delete the comment.
|
||||||
* If the code is self-explanatory, add no comment.
|
* A comment must never be longer than the code it describes.
|
||||||
|
* Applies to YAML, shell and Python too, not just Rust.
|
||||||
|
|
||||||
### Be minimally invasive
|
### Be minimally invasive
|
||||||
|
|
||||||
* Prefer purely additive changes: layer new (`#[cfg]`-gated) blocks or new functions around existing code instead of restructuring it. The ideal diff for a fix adds lines and modifies/deletes none.
|
* Prefer purely additive changes: layer new (`#[cfg]`-gated) blocks or new functions around existing code instead of restructuring it. The ideal diff for a fix adds lines and modifies/deletes none.
|
||||||
* Do not extract or reshape existing code just to enable your new code; look for a mechanism that leaves existing lines untouched (e.g. hide/show an existing object instead of refactoring its construction into a helper for rebuilding).
|
* Do not extract or reshape existing code just to enable your new code; look for a mechanism that leaves existing lines untouched (e.g. hide/show an existing object instead of refactoring its construction into a helper for rebuilding).
|
||||||
* Accept a little duplication over a restructure. A new function that repeats a few lines of an existing one is a better diff than reshaping the original so both can share it.
|
|
||||||
* Put new logic in self-contained functions in the module it belongs to (platform-specific logic in `src/platform/`, with `use` inside the function body to avoid churning shared import blocks). Call sites in shared files (`src/tray.rs`, `src/core_main.rs`, `src/server/connection.rs`, …) should be thin one-line hooks.
|
* Put new logic in self-contained functions in the module it belongs to (platform-specific logic in `src/platform/`, with `use` inside the function body to avoid churning shared import blocks). Call sites in shared files (`src/tray.rs`, `src/core_main.rs`, `src/server/connection.rs`, …) should be thin one-line hooks.
|
||||||
|
|
||||||
## Reviewing a PR
|
|
||||||
|
|
||||||
* Review only what the diff introduces. Verify ownership with `gh pr diff` before reporting a finding — if the offending lines are untouched context, it is a pre-existing problem, not this PR's.
|
|
||||||
* List pre-existing problems in a separate section at the end, or leave out the ones that are not fatal. Never mix them into the findings the author has to fix.
|
|
||||||
* Before re-reviewing, read the author's reply comments. Do not re-raise items they declined on scope grounds.
|
|
||||||
* State a finding's consequence exactly: distinguish "the value is lost" from "the shortcut is inert but the value still saves".
|
|
||||||
|
|
||||||
## Localization (`src/lang/*.rs`)
|
## Localization (`src/lang/*.rs`)
|
||||||
|
|
||||||
Each file is a `HashMap<key, translation>`. Layout:
|
Each file is a `HashMap<key, translation>`. Layout:
|
||||||
|
|||||||
148
Cargo.lock
generated
148
Cargo.lock
generated
@@ -986,6 +986,27 @@ dependencies = [
|
|||||||
"serde 1.0.228",
|
"serde 1.0.228",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
|
||||||
|
dependencies = [
|
||||||
|
"bzip2-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2-sys"
|
||||||
|
version = "0.1.11+1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cacao"
|
name = "cacao"
|
||||||
version = "0.4.0-beta2"
|
version = "0.4.0-beta2"
|
||||||
@@ -1456,8 +1477,8 @@ dependencies = [
|
|||||||
"compression-core",
|
"compression-core",
|
||||||
"flate2",
|
"flate2",
|
||||||
"memchr",
|
"memchr",
|
||||||
"zstd",
|
"zstd 0.13.1",
|
||||||
"zstd-safe",
|
"zstd-safe 7.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1528,6 +1549,12 @@ dependencies = [
|
|||||||
"unicode-xid 0.2.4",
|
"unicode-xid 0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -3791,14 +3818,14 @@ dependencies = [
|
|||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
"url",
|
"url",
|
||||||
"users",
|
"users 0.11.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.9",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
"whoami",
|
"whoami",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
"x11 2.21.0",
|
"x11 2.21.0",
|
||||||
"zstd",
|
"zstd 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5932,6 +5959,37 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pam"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "git+https://github.com/rustdesk-org/pam#7bfd25510202cd269292cbdd7c71f3977a6fd762"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"pam-macros",
|
||||||
|
"pam-sys",
|
||||||
|
"users 0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pam-macros"
|
||||||
|
version = "0.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.93",
|
||||||
|
"quote 1.0.36",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pam-sys"
|
||||||
|
version = "1.0.0-alpha4"
|
||||||
|
source = "git+https://github.com/rustdesk-org/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen 0.59.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -5999,12 +6057,35 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
"password-hash",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -7177,7 +7258,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.5.0"
|
version = "1.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7235,6 +7316,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
"os-version",
|
"os-version",
|
||||||
|
"pam",
|
||||||
"parity-tokio-ipc",
|
"parity-tokio-ipc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"piet",
|
"piet",
|
||||||
@@ -7283,11 +7365,12 @@ dependencies = [
|
|||||||
"wol-rs",
|
"wol-rs",
|
||||||
"x11-clipboard 0.8.1",
|
"x11-clipboard 0.8.1",
|
||||||
"x11rb 0.12.0",
|
"x11rb 0.12.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.5.0"
|
version = "1.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -8824,7 +8907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
|
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base32",
|
"base32",
|
||||||
"constant_time_eq",
|
"constant_time_eq 0.2.6",
|
||||||
"hmac",
|
"hmac",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -9269,6 +9352,16 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "users"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "users"
|
name = "users"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -11064,13 +11157,52 @@ dependencies = [
|
|||||||
"syn 2.0.98",
|
"syn 2.0.98",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "0.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"byteorder",
|
||||||
|
"bzip2",
|
||||||
|
"constant_time_eq 0.1.5",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"flate2",
|
||||||
|
"hmac",
|
||||||
|
"pbkdf2",
|
||||||
|
"sha1",
|
||||||
|
"time 0.3.36",
|
||||||
|
"zstd 0.11.2+zstd.1.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.11.2+zstd.1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe 5.0.2+zstd.1.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
|
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zstd-safe",
|
"zstd-safe 7.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "5.0.2+zstd.1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"zstd-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.5.0"
|
version = "1.4.9"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -37,6 +37,7 @@ drm = ["scrap/drm"]
|
|||||||
# kind of operation and deserves a switch that can remove it from the binary entirely, without
|
# kind of operation and deserves a switch that can remove it from the binary entirely, without
|
||||||
# giving up DRM capture: `--features drm` builds the capture path with no wake code compiled in.
|
# giving up DRM capture: `--features drm` builds the capture path with no wake code compiled in.
|
||||||
drm-wake = ["drm"]
|
drm-wake = ["drm"]
|
||||||
|
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 = [
|
unix-file-copy-paste = [
|
||||||
"dep:x11-clipboard",
|
"dep:x11-clipboard",
|
||||||
@@ -80,6 +81,7 @@ hex = "0.4"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
cidr-utils = "0.5"
|
cidr-utils = "0.5"
|
||||||
fon = "0.6"
|
fon = "0.6"
|
||||||
|
zip = "0.6"
|
||||||
shutdown_hooks = "0.1"
|
shutdown_hooks = "0.1"
|
||||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||||
stunclient = "0.4"
|
stunclient = "0.4"
|
||||||
@@ -189,6 +191,7 @@ async-process = "1.7"
|
|||||||
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
|
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
percent-encoding = {version = "2.3", optional = true}
|
percent-encoding = {version = "2.3", optional = true}
|
||||||
@@ -209,7 +212,7 @@ android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
|||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
|
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
|
||||||
exclude = ["vdi/host"]
|
exclude = ["vdi/host", "examples/custom_plugin"]
|
||||||
|
|
||||||
# Patch libxdo-sys to use a stub implementation that doesn't require libxdo
|
# Patch libxdo-sys to use a stub implementation that doesn't require libxdo
|
||||||
# This allows building and running on systems without libxdo installed (e.g., Wayland-only)
|
# This allows building and running on systems without libxdo installed (e.g., Wayland-only)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ RUN apt update -y && \
|
|||||||
libxcb-shape0-dev \
|
libxcb-shape0-dev \
|
||||||
libxcb-xfixes0-dev \
|
libxcb-xfixes0-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
make \
|
make \
|
||||||
wget \
|
wget \
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<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="#screenshots">Screenshots</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>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</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>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</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>
|
||||||
@@ -66,19 +66,19 @@ Please download Sciter dynamic library yourself.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,6 +168,7 @@ Please ensure that you run these commands from the root of the RustDesk reposito
|
|||||||
- **[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 desktop and 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/v1/js)**: JavaScript for Flutter web client
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.5.0
|
version: 1.4.9
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -58,6 +58,7 @@ AppDir:
|
|||||||
- libpulse0
|
- libpulse0
|
||||||
- packagekit-gtk3-module
|
- packagekit-gtk3-module
|
||||||
- libcanberra-gtk3-module
|
- libcanberra-gtk3-module
|
||||||
|
- libpam0g
|
||||||
- libdrm2
|
- libdrm2
|
||||||
exclude:
|
exclude:
|
||||||
- humanity-icon-theme
|
- humanity-icon-theme
|
||||||
@@ -76,13 +77,6 @@ AppDir:
|
|||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
||||||
GDK_BACKEND: x11
|
GDK_BACKEND: x11
|
||||||
# AppRun sets these to "$APPDIR/...:$XDG_DATA_DIRS", and setting them at all suppresses the XDG
|
|
||||||
# defaults, so a host that leaves them unset loses /usr/share and /etc/xdg. gdk-pixbuf 2.43+
|
|
||||||
# (Arch, Fedora) then finds no glycin loaders and every PNG decode fails, aborting on the first
|
|
||||||
# remote cursor. The host value goes last: unset it expands to an empty element, which GLib
|
|
||||||
# resolves against the CWD, and that must not outrank the defaults below.
|
|
||||||
XDG_DATA_DIRS: $APPDIR/usr/local/share:$APPDIR/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS
|
|
||||||
XDG_CONFIG_DIRS: $APPDIR/etc/xdg:/etc/xdg:$XDG_CONFIG_DIRS
|
|
||||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
|
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
|
||||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.5.0
|
version: 1.4.9
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -61,6 +61,7 @@ AppDir:
|
|||||||
- libpulse0
|
- libpulse0
|
||||||
- packagekit-gtk3-module
|
- packagekit-gtk3-module
|
||||||
- libcanberra-gtk3-module
|
- libcanberra-gtk3-module
|
||||||
|
- libpam0g
|
||||||
- libdrm2
|
- libdrm2
|
||||||
exclude:
|
exclude:
|
||||||
- humanity-icon-theme
|
- humanity-icon-theme
|
||||||
@@ -79,13 +80,6 @@ AppDir:
|
|||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
||||||
GDK_BACKEND: x11
|
GDK_BACKEND: x11
|
||||||
# AppRun sets these to "$APPDIR/...:$XDG_DATA_DIRS", and setting them at all suppresses the XDG
|
|
||||||
# defaults, so a host that leaves them unset loses /usr/share and /etc/xdg. gdk-pixbuf 2.43+
|
|
||||||
# (Arch, Fedora) then finds no glycin loaders and every PNG decode fails, aborting on the first
|
|
||||||
# remote cursor. The host value goes last: unset it expands to an empty element, which GLib
|
|
||||||
# resolves against the CWD, and that must not outrank the defaults below.
|
|
||||||
XDG_DATA_DIRS: $APPDIR/usr/local/share:$APPDIR/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS
|
|
||||||
XDG_CONFIG_DIRS: $APPDIR/etc/xdg:/etc/xdg:$XDG_CONFIG_DIRS
|
|
||||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
|
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
|
||||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||||
|
|||||||
20
build.py
20
build.py
@@ -364,7 +364,7 @@ Version: %s
|
|||||||
Architecture: %s
|
Architecture: %s
|
||||||
Maintainer: rustdesk <info@rustdesk.com>
|
Maintainer: rustdesk <info@rustdesk.com>
|
||||||
Homepage: https://rustdesk.com
|
Homepage: https://rustdesk.com
|
||||||
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, gstreamer1.0-pipewire%s
|
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||||
Recommends: libayatana-appindicator3-1
|
Recommends: libayatana-appindicator3-1
|
||||||
Description: A remote control software.
|
Description: A remote control software.
|
||||||
|
|
||||||
@@ -390,9 +390,9 @@ def ffi_bindgen_function_refactor():
|
|||||||
# The commit is fetched directly by sha, so no branch or tag name takes part in the build: see
|
# The commit is fetched directly by sha, so no branch or tag name takes part in the build: see
|
||||||
# build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in
|
# build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in
|
||||||
# any workflow, so a bump is one edit here (plus the informational version comment in
|
# any workflow, so a bump is one edit here (plus the informational version comment in
|
||||||
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.4.
|
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.2.
|
||||||
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
|
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
|
||||||
LIBDRMTAP_SHA_PINNED = '5da68a3a368db569716d0d0f11cefacbb11b2290'
|
LIBDRMTAP_SHA_PINNED = '653de8c774bc245eaf960611ca7a136f7a544d21'
|
||||||
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED)
|
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED)
|
||||||
LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
|
LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
|
||||||
# Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the
|
# Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the
|
||||||
@@ -704,6 +704,8 @@ def build_flutter_deb(version, features):
|
|||||||
system2('flutter build linux --release')
|
system2('flutter build linux --release')
|
||||||
system2('mkdir -p tmpdeb/usr/bin/')
|
system2('mkdir -p tmpdeb/usr/bin/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
|
system2('mkdir -p tmpdeb/etc/rustdesk/')
|
||||||
|
system2('mkdir -p tmpdeb/etc/pam.d/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
|
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
|
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
|
||||||
@@ -722,6 +724,12 @@ def build_flutter_deb(version, features):
|
|||||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||||
|
system2(
|
||||||
|
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||||
|
system2(
|
||||||
|
'cp ../res/xorg.conf tmpdeb/etc/rustdesk/')
|
||||||
|
system2(
|
||||||
|
'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||||
system2(
|
system2(
|
||||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||||
# Bundle libdrmtap.so only when this build actually enabled the `drm` feature, so stock packages
|
# Bundle libdrmtap.so only when this build actually enabled the `drm` feature, so stock packages
|
||||||
@@ -1124,7 +1132,13 @@ def main():
|
|||||||
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||||
|
os.system('mkdir -p tmpdeb/etc/rustdesk/')
|
||||||
|
os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||||
|
os.system('mkdir -p tmpdeb/etc/X11/rustdesk/')
|
||||||
|
os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/')
|
||||||
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
||||||
|
os.system('mkdir -p tmpdeb/etc/pam.d/')
|
||||||
|
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||||
system2('strip tmpdeb/usr/bin/rustdesk')
|
system2('strip tmpdeb/usr/bin/rustdesk')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
|
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
|
||||||
|
|||||||
1
build.rs
1
build.rs
@@ -72,6 +72,7 @@ fn install_android_deps() {
|
|||||||
path.join("lib").to_str().unwrap()
|
path.join("lib").to_str().unwrap()
|
||||||
);
|
);
|
||||||
println!("cargo:rustc-link-lib=ndk_compat");
|
println!("cargo:rustc-link-lib=ndk_compat");
|
||||||
|
println!("cargo:rustc-link-lib=oboe");
|
||||||
println!("cargo:rustc-link-lib=c++");
|
println!("cargo:rustc-link-lib=c++");
|
||||||
println!("cargo:rustc-link-lib=OpenSLES");
|
println!("cargo:rustc-link-lib=OpenSLES");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://
|
|||||||
|
|
||||||
## Tindakan
|
## Tindakan
|
||||||
|
|
||||||
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md>
|
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
|
||||||
|
|
||||||
## Komunikasi
|
## Komunikasi
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Per istruzioni specifiche su git, vedi [Workflow GitHub - 101](https://github.co
|
|||||||
|
|
||||||
## Condotta
|
## Condotta
|
||||||
|
|
||||||
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
|
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-IT.md
|
||||||
|
|
||||||
## Comunicazioni
|
## Comunicazioni
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ RustDesk يرجى التأكد من أنك تنفذ هذه الأوامر من
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: أو المنقول عن بُعد (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: أو المنقول عن بُعد (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهاتف المحمول
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهاتف المحمول
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**:Flutter لعميل الويب الخاص ب Javascript
|
||||||
|
|
||||||
## لقطات
|
## لقطات
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ Ujistěte se, že tyto příkazy spouštíte z kořenového adresáře RustDesk,
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komunikace s [rustdesk-server](https://github.com/rustdesk/rustdesk-server), očekávání vzdálených příméhých („proděrováváním“ TCP) nebo předávaných (relay) spojení
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komunikace s [rustdesk-server](https://github.com/rustdesk/rustdesk-server), očekávání vzdálených příméhých („proděrováváním“ TCP) nebo předávaných (relay) spojení
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: zdrojové kódy, specifické pro jednotlivé platformy
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: zdrojové kódy, specifické pro jednotlivé platformy
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: zdrojové kódy pro použití s aplikačním rámcem (framework) Flutter pro mobilní platformy
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: zdrojové kódy pro použití s aplikačním rámcem (framework) Flutter pro mobilní platformy
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript pro Flutter webový klient
|
||||||
|
|
||||||
## Ukázky
|
## Ukázky
|
||||||
|
|
||||||
|
|||||||
@@ -66,19 +66,19 @@ Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,6 +168,7 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ Por favor descarga la librería dinámica de Sciter tú mismo.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -163,6 +163,7 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed")
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed")
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter
|
||||||
|
|
||||||
> [!Precaución]
|
> [!Precaución]
|
||||||
> **Descargo de responsabilidad por uso indebido:** <br>
|
> **Descargo de responsabilidad por uso indebido:** <br>
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ target/release/rustdesk
|
|||||||
- **[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 mobile
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
|
||||||
|
|
||||||
## تصاویر محیط نرمافزار
|
## تصاویر محیط نرمافزار
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ target/release/rustdesk
|
|||||||
- **[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 mobile
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
|
||||||
|
|
||||||
## Στιγμιότυπα
|
## Στιγμιότυπα
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ A telefonos verziók Flutter-t hasznának. Később lehetséges hogy Sciterről
|
|||||||
|
|
||||||
- Futtasd a `cargo run` parancsot
|
- Futtasd a `cargo run` parancsot
|
||||||
|
|
||||||
## [Építés](https://rustdesk.com/docs/en/dev/build/)
|
## [Építés](https://rustdesk.com/docs/hu/dev/build/)
|
||||||
|
|
||||||
## Hogyan építs Linuxon
|
## Hogyan építs Linuxon
|
||||||
|
|
||||||
@@ -150,6 +150,7 @@ Kérlek mindenképpen nézd meg hogy ezeket a parancsokat a root RustDesk mappá
|
|||||||
- **[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 mobile
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
|
||||||
|
|
||||||
## Képernyőképek
|
## Képernyőképek
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
|
||||||
|
|
||||||
> [!Attenzione]
|
> [!Attenzione]
|
||||||
> **Dichiarazione di non responsabilità per uso improprio:** <br>
|
> **Dichiarazione di non responsabilità per uso improprio:** <br>
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
|
||||||
|
|
||||||
> [!注意]
|
> [!注意]
|
||||||
> **:不正使用に関する免責事項** <br>
|
> **:不正使用に関する免責事項** <br>
|
||||||
|
|||||||
@@ -66,19 +66,19 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,6 +168,7 @@ RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 클라이언트용 JavaScript
|
||||||
|
|
||||||
## 스크린샷
|
## 스크린샷
|
||||||
|
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ Venligst last ned Sciters dynamiske bibliotek selv.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -163,6 +163,7 @@ Venligst pass på att du kjører disse kommandoene fra roten av RustDesk reposit
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient
|
||||||
|
|
||||||
## Skjermbilder
|
## Skjermbilder
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript dla Flutter - klient web
|
||||||
|
|
||||||
## Zrzuty ekranu
|
## Zrzuty ekranu
|
||||||
|
|
||||||
|
|||||||
@@ -64,19 +64,19 @@ Por favor, faça o download da biblioteca dinâmica do Sciter por conta própria
|
|||||||
### Ubuntu 18 (Debian 10)
|
### Ubuntu 18 (Debian 10)
|
||||||
|
|
||||||
```sh
|
```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
|
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 libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -166,6 +166,7 @@ Certifique-se de executar esses comandos a partir da raiz do repositório do Rus
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript para o cliente web do Flutter.
|
||||||
|
|
||||||
## Capturas de Tela
|
## Capturas de Tela
|
||||||
|
|
||||||
|
|||||||
@@ -66,19 +66,19 @@ Te rugăm să descarci singur librăria dinamică Sciter.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,6 +168,7 @@ Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, a
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
|
||||||
|
|
||||||
## Capturi de ecran
|
## Capturi de ecran
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ RustDesk приветствует вклад каждого. Ознакомьт
|
|||||||
|
|
||||||
- Выполните команду `cargo run`
|
- Выполните команду `cargo run`
|
||||||
|
|
||||||
## [Сборка](https://rustdesk.com/docs/en/dev/build/)
|
## [Сборка](https://rustdesk.com/docs/ru/dev/build/)
|
||||||
|
|
||||||
## Как собрать на Linux
|
## Как собрать на Linux
|
||||||
|
|
||||||
@@ -68,19 +68,19 @@ RustDesk приветствует вклад каждого. Ознакомьт
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -170,6 +170,7 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter
|
||||||
|
|
||||||
## Скриншоты
|
## Скриншоты
|
||||||
|
|
||||||
@@ -179,4 +180,4 @@ target/release/rustdesk
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
@@ -166,6 +166,7 @@ Lütfen bu komutları RustDesk reposunun root klasöründe çalıştırdığın
|
|||||||
- **[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şime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
|
- **[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şime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript
|
||||||
|
|
||||||
|
|
||||||
## Ekran Görüntüleri
|
## Ekran Görüntüleri
|
||||||
|
|||||||
@@ -59,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```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 pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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 gstreamer1-devel gstreamer1-plugins-base-devel
|
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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -160,6 +160,7 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
|
||||||
|
|
||||||
## Знімки екрана
|
## Знімки екрана
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của th
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
||||||
|
|
||||||
## 截图
|
## 截图
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pam",
|
||||||
|
"buildsystem": "autotools",
|
||||||
|
"config-opts": ["--disable-selinux"],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
|
||||||
|
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "rustdesk",
|
"name": "rustdesk",
|
||||||
"buildsystem": "simple",
|
"buildsystem": "simple",
|
||||||
@@ -51,4 +63,4 @@
|
|||||||
"--socket=pulseaudio",
|
"--socket=pulseaudio",
|
||||||
"--talk-name=org.freedesktop.Flatpak"
|
"--talk-name=org.freedesktop.Flatpak"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -82,8 +82,7 @@ protobuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.carriez.flutter_hbb"
|
compileSdkVersion 34
|
||||||
compileSdkVersion 36
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
|
||||||
@@ -92,7 +91,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -101,7 +99,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 22
|
minSdkVersion 22
|
||||||
targetSdkVersion 36
|
targetSdkVersion 33
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
@@ -130,7 +128,6 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.carriez.flutter_hbb">
|
package="com.carriez.flutter_hbb">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@@ -30,6 +26,7 @@
|
|||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="RustDesk"
|
android:label="RustDesk"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
|
|
||||||
@@ -91,12 +88,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".MainService"
|
android:name=".MainService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:foregroundServiceType="mediaProjection" />
|
||||||
android:foregroundServiceType="specialUse|mediaProjection|microphone">
|
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="@string/foreground_service_special_use_subtype" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".FloatingWindowService"
|
android:name=".FloatingWindowService"
|
||||||
|
|||||||
@@ -18,33 +18,7 @@ const val AUDIO_SAMPLE_RATE = 48000
|
|||||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||||
|
|
||||||
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||||
companion object {
|
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
||||||
private const val LOG_TAG = "LOG_AUDIO_RECORD_HANDLE"
|
|
||||||
private const val NO_ACTIVE_PUBLISHERS = 0
|
|
||||||
private var activeAudioFramePublishers = NO_ACTIVE_PUBLISHERS
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun acquireAudioFramePublisher() {
|
|
||||||
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
|
||||||
FFI.setFrameRawEnable("audio", true)
|
|
||||||
}
|
|
||||||
activeAudioFramePublishers++
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun releaseAudioFramePublisher() {
|
|
||||||
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
|
||||||
Log.e(LOG_TAG, "No active audio frame publisher to release")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
activeAudioFramePublishers--
|
|
||||||
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
|
||||||
FFI.setFrameRawEnable("audio", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val logTag = LOG_TAG
|
|
||||||
|
|
||||||
private var audioRecorder: AudioRecord? = null
|
private var audioRecorder: AudioRecord? = null
|
||||||
private var audioReader: AudioReader? = null
|
private var audioReader: AudioReader? = null
|
||||||
@@ -105,94 +79,48 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// read f32 to byte , length * 4
|
// read f32 to byte , length * 4
|
||||||
val bufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
AUDIO_SAMPLE_RATE,
|
AUDIO_SAMPLE_RATE,
|
||||||
AUDIO_CHANNEL_MASK,
|
AUDIO_CHANNEL_MASK,
|
||||||
AUDIO_ENCODING
|
AUDIO_ENCODING
|
||||||
)
|
)
|
||||||
if (bufferSize <= 0) {
|
if (minBufferSize == 0) {
|
||||||
Log.d(logTag, "get min buffer size fail!")
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
audioReader = AudioReader(bufferSize, 4)
|
audioReader = AudioReader(minBufferSize, 4)
|
||||||
minBufferSize = bufferSize
|
|
||||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseRecorder(recorder: AudioRecord) {
|
|
||||||
try {
|
|
||||||
recorder.release()
|
|
||||||
} finally {
|
|
||||||
if (audioRecorder === recorder) {
|
|
||||||
audioRecorder = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun captureAudio(reader: AudioReader, recorder: AudioRecord) {
|
|
||||||
try {
|
|
||||||
while (audioRecordStat) {
|
|
||||||
reader.readSync(recorder)?.let {
|
|
||||||
FFI.onAudioFrameUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
minBufferSize = 0
|
|
||||||
try {
|
|
||||||
releaseRecorder(recorder)
|
|
||||||
} finally {
|
|
||||||
releaseAudioFramePublisher()
|
|
||||||
Log.d(logTag, "Exit audio thread")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
fun startAudioRecorder(): Boolean {
|
fun startAudioRecorder() {
|
||||||
val recorder = audioRecorder
|
checkAudioReader()
|
||||||
if (recorder == null) {
|
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
Log.d(logTag, "startAudioRecorder fail")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var audioFramePublisherAcquired = false
|
|
||||||
return try {
|
|
||||||
checkAudioReader()
|
|
||||||
val reader = audioReader
|
|
||||||
if (reader == null || minBufferSize == 0) {
|
|
||||||
releaseRecorder(recorder)
|
|
||||||
Log.d(logTag, "startAudioRecorder fail")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
recorder.startRecording()
|
|
||||||
if (recorder.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
|
|
||||||
throw IllegalStateException("AudioRecord failed to enter recording state")
|
|
||||||
}
|
|
||||||
audioRecordStat = true
|
|
||||||
val captureThread = thread(start = false) { captureAudio(reader, recorder) }
|
|
||||||
acquireAudioFramePublisher()
|
|
||||||
audioFramePublisherAcquired = true
|
|
||||||
audioThread = captureThread
|
|
||||||
captureThread.start()
|
|
||||||
true
|
|
||||||
} catch (error: Exception) {
|
|
||||||
audioRecordStat = false
|
|
||||||
audioThread = null
|
|
||||||
Log.e(logTag, "startAudioRecorder fail", error)
|
|
||||||
try {
|
try {
|
||||||
releaseRecorder(recorder)
|
FFI.setFrameRawEnable("audio", true)
|
||||||
} finally {
|
audioRecorder!!.startRecording()
|
||||||
if (audioFramePublisherAcquired) {
|
audioRecordStat = true
|
||||||
releaseAudioFramePublisher()
|
audioThread = thread {
|
||||||
|
while (audioRecordStat) {
|
||||||
|
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||||
|
FFI.onAudioFrameUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// let's release here rather than onDestroy to avoid threading issue
|
||||||
|
audioRecorder?.release()
|
||||||
|
audioRecorder = null
|
||||||
|
minBufferSize = 0
|
||||||
|
FFI.setFrameRawEnable("audio", false)
|
||||||
|
Log.d(logTag, "Exit audio thread")
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||||
}
|
}
|
||||||
false
|
} else {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isVoiceCallActive(): Boolean {
|
|
||||||
return audioRecorder?.audioSource == MediaRecorder.AudioSource.VOICE_COMMUNICATION
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (!isSupportVoiceCall()) {
|
if (!isSupportVoiceCall()) {
|
||||||
return false
|
return false
|
||||||
@@ -209,9 +137,11 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
if (!isSupportVoiceCall()) {
|
if (!isSupportVoiceCall()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
val switched = !isVideoStart() || switchOutVoiceCall(mediaProjection)
|
if (isVideoStart()) {
|
||||||
|
switchOutVoiceCall(mediaProjection)
|
||||||
|
}
|
||||||
tryReleaseAudio()
|
tryReleaseAudio()
|
||||||
return switched
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@@ -229,7 +159,8 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
Log.e(logTag, "createAudioRecorder fail")
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return startAudioRecorder()
|
startAudioRecorder()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@@ -246,7 +177,8 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
Log.e(logTag, "createAudioRecorder fail")
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return startAudioRecorder()
|
startAudioRecorder()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryReleaseAudio() {
|
fun tryReleaseAudio() {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package com.carriez.flutter_hbb
|
|||||||
|
|
||||||
import ffi.FFI
|
import ffi.FFI
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -25,10 +24,6 @@ import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
|||||||
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||||
import android.media.MediaCodecList
|
import android.media.MediaCodecList
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@@ -38,9 +33,6 @@ import io.flutter.embedding.android.FlutterActivity
|
|||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
@@ -54,23 +46,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val channelTag = "mChannel"
|
private val channelTag = "mChannel"
|
||||||
private val logTag = "mMainActivity"
|
private val logTag = "mMainActivity"
|
||||||
private var mainService: MainService? = null
|
private var mainService: MainService? = null
|
||||||
private sealed class PendingPicker {
|
|
||||||
data class ImportFiles(val result: MethodChannel.Result) : PendingPicker()
|
|
||||||
data class ExportFile(val source: File, val result: MethodChannel.Result) : PendingPicker()
|
|
||||||
data class ImportDirectory(val result: MethodChannel.Result) : PendingPicker()
|
|
||||||
data class ExportFiles(
|
|
||||||
val sources: List<File>,
|
|
||||||
val rejected: Int,
|
|
||||||
val result: MethodChannel.Result
|
|
||||||
) : PendingPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ExportSource(
|
|
||||||
val file: File,
|
|
||||||
val children: List<ExportSource>?
|
|
||||||
)
|
|
||||||
|
|
||||||
private var pendingPicker: PendingPicker? = null
|
|
||||||
|
|
||||||
private var isAudioStart = false
|
private var isAudioStart = false
|
||||||
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||||
@@ -116,108 +91,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
if (requestCode == REQ_IMPORT_FILES) {
|
|
||||||
val pending = pendingPicker as? PendingPicker.ImportFiles ?: return
|
|
||||||
pendingPicker = null
|
|
||||||
if (resultCode != Activity.RESULT_OK || data == null) {
|
|
||||||
pending.result.success(emptyList<Map<String, String>>())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val uris = linkedSetOf<Uri>()
|
|
||||||
data.data?.let { uris.add(it) }
|
|
||||||
data.clipData?.let { clipData ->
|
|
||||||
for (index in 0 until clipData.itemCount) {
|
|
||||||
uris.add(clipData.getItemAt(index).uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread {
|
|
||||||
val files = uris.map { uri ->
|
|
||||||
mapOf(
|
|
||||||
"uri" to uri.toString(),
|
|
||||||
"name" to (displayName(uri) ?: uri.lastPathSegment.orEmpty())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
runOnUiThread { pending.result.success(files) }
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (requestCode == REQ_EXPORT_FILE) {
|
|
||||||
val pending = pendingPicker as? PendingPicker.ExportFile ?: return
|
|
||||||
pendingPicker = null
|
|
||||||
val destination = data?.data
|
|
||||||
|
|
||||||
if (resultCode != Activity.RESULT_OK || destination == null) {
|
|
||||||
pending.result.success(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
thread {
|
|
||||||
try {
|
|
||||||
FileInputStream(pending.source).use { input ->
|
|
||||||
contentResolver.openOutputStream(destination, "wt")?.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
} ?: throw IllegalStateException("Unable to open the selected destination")
|
|
||||||
}
|
|
||||||
runOnUiThread { pending.result.success(true) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to export file", e)
|
|
||||||
runOnUiThread {
|
|
||||||
pending.result.error("export_failed", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (requestCode == REQ_IMPORT_DIRECTORY) {
|
|
||||||
val pending = pendingPicker as? PendingPicker.ImportDirectory ?: return
|
|
||||||
pendingPicker = null
|
|
||||||
val treeUri = data?.data
|
|
||||||
if (resultCode != Activity.RESULT_OK || treeUri == null) {
|
|
||||||
pending.result.success(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
thread {
|
|
||||||
val selected = mapOf(
|
|
||||||
"uri" to treeUri.toString(),
|
|
||||||
"name" to (treeDisplayName(treeUri) ?: "Imported")
|
|
||||||
)
|
|
||||||
runOnUiThread { pending.result.success(selected) }
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (requestCode == REQ_EXPORT_FILES) {
|
|
||||||
val pending = pendingPicker as? PendingPicker.ExportFiles ?: return
|
|
||||||
pendingPicker = null
|
|
||||||
val treeUri = data?.data
|
|
||||||
if (resultCode != Activity.RESULT_OK || treeUri == null) {
|
|
||||||
pending.result.success(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
thread {
|
|
||||||
var exported = 0
|
|
||||||
var failed = pending.rejected
|
|
||||||
var processed = 0
|
|
||||||
try {
|
|
||||||
val sources = pending.sources.map { snapshotExportSource(it) }
|
|
||||||
val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
|
||||||
sources.forEach { source ->
|
|
||||||
val ok = source?.let {
|
|
||||||
copyExportSourceToTree(treeUri, rootDocId, it)
|
|
||||||
} ?: false
|
|
||||||
if (ok) exported++ else failed++
|
|
||||||
processed++
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to export selected files", e)
|
|
||||||
failed += pending.sources.size - processed
|
|
||||||
}
|
|
||||||
runOnUiThread {
|
|
||||||
pending.result.success(mapOf("exported" to exported, "failed" to failed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
|
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
|
||||||
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
|
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
|
||||||
}
|
}
|
||||||
@@ -394,242 +267,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PICK_IMPORT_FILES -> {
|
|
||||||
if (pendingPicker != null) {
|
|
||||||
result.error("picker_in_progress", "Another document picker is already open", null)
|
|
||||||
} else {
|
|
||||||
pendingPicker = PendingPicker.ImportFiles(result)
|
|
||||||
try {
|
|
||||||
startActivityForResult(
|
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
||||||
},
|
|
||||||
REQ_IMPORT_FILES
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
pendingPicker = null
|
|
||||||
result.error("picker_unavailable", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IMPORT_FILE -> {
|
|
||||||
val arguments = call.arguments as? Map<*, *>
|
|
||||||
val uri = (arguments?.get("uri") as? String)?.let {
|
|
||||||
runCatching { Uri.parse(it) }.getOrNull()
|
|
||||||
}
|
|
||||||
val path = arguments?.get("path") as? String
|
|
||||||
val overwrite = arguments?.get("overwrite") as? Boolean ?: false
|
|
||||||
val destination = path?.let { canonicalAppScopedFile(it) }
|
|
||||||
|
|
||||||
if (uri?.scheme != "content") {
|
|
||||||
result.error("invalid_uri", "The selected document URI is invalid", null)
|
|
||||||
} else if (destination == null ||
|
|
||||||
destination.isDirectory ||
|
|
||||||
destination.parentFile?.isDirectory != true) {
|
|
||||||
result.error("invalid_destination", "The destination is outside app-scoped storage", null)
|
|
||||||
} else {
|
|
||||||
thread {
|
|
||||||
var temporary: File? = null
|
|
||||||
var reservedDestination = false
|
|
||||||
var errorCode = "import_failed"
|
|
||||||
try {
|
|
||||||
val temporaryFile = File.createTempFile(
|
|
||||||
".rustdesk-import-",
|
|
||||||
".tmp",
|
|
||||||
destination.parentFile
|
|
||||||
)
|
|
||||||
temporary = temporaryFile
|
|
||||||
contentResolver.openInputStream(uri)?.use { input ->
|
|
||||||
FileOutputStream(temporaryFile).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
} ?: throw IllegalStateException("Unable to open the selected document")
|
|
||||||
if (!overwrite) {
|
|
||||||
reservedDestination = destination.createNewFile()
|
|
||||||
if (!reservedDestination) {
|
|
||||||
throw IllegalStateException("The destination already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!temporaryFile.renameTo(destination)) {
|
|
||||||
if (reservedDestination) {
|
|
||||||
destination.delete()
|
|
||||||
}
|
|
||||||
errorCode = "rename_failed"
|
|
||||||
throw IllegalStateException("Unable to replace the destination")
|
|
||||||
}
|
|
||||||
runOnUiThread { result.success(true) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to import file", e)
|
|
||||||
runOnUiThread {
|
|
||||||
result.error(errorCode, e.message, null)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
temporary?.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EXPORT_FILE -> {
|
|
||||||
val path = (call.arguments as? Map<*, *>)?.get("path") as? String
|
|
||||||
val source = path?.let { canonicalExportSource(it) }
|
|
||||||
|
|
||||||
if (source?.isFile != true) {
|
|
||||||
result.error("invalid_source", "The file is outside app-scoped storage", null)
|
|
||||||
} else if (pendingPicker != null) {
|
|
||||||
result.error("picker_in_progress", "Another document picker is already open", null)
|
|
||||||
} else {
|
|
||||||
val mimeType = MimeTypeMap.getSingleton()
|
|
||||||
.getMimeTypeFromExtension(source.extension.lowercase())
|
|
||||||
?: "application/octet-stream"
|
|
||||||
pendingPicker = PendingPicker.ExportFile(source, result)
|
|
||||||
try {
|
|
||||||
startActivityForResult(
|
|
||||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = mimeType
|
|
||||||
putExtra(Intent.EXTRA_TITLE, source.name)
|
|
||||||
},
|
|
||||||
REQ_EXPORT_FILE
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
pendingPicker = null
|
|
||||||
result.error("picker_unavailable", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PICK_IMPORT_DIRECTORY -> {
|
|
||||||
if (pendingPicker != null) {
|
|
||||||
result.error("picker_in_progress", "Another document picker is already open", null)
|
|
||||||
} else {
|
|
||||||
pendingPicker = PendingPicker.ImportDirectory(result)
|
|
||||||
try {
|
|
||||||
startActivityForResult(
|
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra(Intent.EXTRA_TITLE, "Select the folder to import")
|
|
||||||
},
|
|
||||||
REQ_IMPORT_DIRECTORY
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
pendingPicker = null
|
|
||||||
result.error("picker_unavailable", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IMPORT_DIRECTORY -> {
|
|
||||||
val arguments = call.arguments as? Map<*, *>
|
|
||||||
val uri = (arguments?.get("uri") as? String)?.let {
|
|
||||||
runCatching { Uri.parse(it) }.getOrNull()
|
|
||||||
}
|
|
||||||
val path = arguments?.get("path") as? String
|
|
||||||
val overwrite = arguments?.get("overwrite") as? Boolean ?: false
|
|
||||||
val destination = path?.let { canonicalAppScopedFile(it) }
|
|
||||||
|
|
||||||
if (uri?.scheme != "content") {
|
|
||||||
result.error("invalid_uri", "The selected document URI is invalid", null)
|
|
||||||
} else if (destination == null ||
|
|
||||||
destination.parentFile?.isDirectory != true ||
|
|
||||||
(destination.exists() && !destination.isDirectory)) {
|
|
||||||
result.error("invalid_destination", "The destination is outside app-scoped storage", null)
|
|
||||||
} else {
|
|
||||||
thread {
|
|
||||||
var temporary: File? = null
|
|
||||||
var backup: File? = null
|
|
||||||
val ok = try {
|
|
||||||
val parent = destination.parentFile
|
|
||||||
?: throw IllegalStateException("The destination has no parent")
|
|
||||||
temporary = File.createTempFile(
|
|
||||||
".rustdesk-import-dir-",
|
|
||||||
".tmp",
|
|
||||||
parent
|
|
||||||
).also {
|
|
||||||
if (!it.delete() || !it.mkdir()) {
|
|
||||||
throw IllegalStateException("Unable to create a temporary folder")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!copyDocumentTreeToFile(uri, temporary!!)) {
|
|
||||||
throw IllegalStateException("Unable to read all folder contents")
|
|
||||||
}
|
|
||||||
if (destination.exists()) {
|
|
||||||
if (!overwrite) {
|
|
||||||
throw IllegalStateException("The destination already exists")
|
|
||||||
}
|
|
||||||
val backupFile = File.createTempFile(
|
|
||||||
".rustdesk-import-backup-",
|
|
||||||
".tmp",
|
|
||||||
parent
|
|
||||||
)
|
|
||||||
if (!backupFile.delete()) {
|
|
||||||
throw IllegalStateException("Unable to prepare the destination backup")
|
|
||||||
}
|
|
||||||
backup = backupFile
|
|
||||||
if (!destination.renameTo(backupFile)) {
|
|
||||||
throw IllegalStateException("Unable to replace the destination")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!temporary!!.renameTo(destination)) {
|
|
||||||
val destinationBackup = backup
|
|
||||||
if (destinationBackup != null &&
|
|
||||||
!destinationBackup.renameTo(destination)
|
|
||||||
) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Unable to move the imported folder and restore " +
|
|
||||||
"the destination from $destinationBackup"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw IllegalStateException("Unable to move the imported folder")
|
|
||||||
}
|
|
||||||
temporary = null
|
|
||||||
val destinationBackup = backup
|
|
||||||
if (destinationBackup != null &&
|
|
||||||
!destinationBackup.deleteRecursively()
|
|
||||||
) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Unable to remove the destination backup: $destinationBackup"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
backup = null
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to import directory", e)
|
|
||||||
false
|
|
||||||
} finally {
|
|
||||||
temporary?.deleteRecursively()
|
|
||||||
}
|
|
||||||
runOnUiThread { result.success(ok) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EXPORT_FILES -> {
|
|
||||||
val paths = (call.arguments as? Map<*, *>)?.get("paths") as? List<*>
|
|
||||||
if (paths.isNullOrEmpty()) {
|
|
||||||
result.error("invalid_source", "The selected files are outside app-scoped storage", null)
|
|
||||||
} else {
|
|
||||||
val sources = paths.mapNotNull {
|
|
||||||
(it as? String)?.let(::canonicalExportSource)
|
|
||||||
}
|
|
||||||
val rejected = paths.size - sources.size
|
|
||||||
if (sources.isEmpty()) {
|
|
||||||
result.success(mapOf("exported" to 0, "failed" to rejected))
|
|
||||||
} else if (pendingPicker != null) {
|
|
||||||
result.error("picker_in_progress", "Another document picker is already open", null)
|
|
||||||
} else {
|
|
||||||
pendingPicker = PendingPicker.ExportFiles(sources, rejected, result)
|
|
||||||
try {
|
|
||||||
startActivityForResult(
|
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra(Intent.EXTRA_TITLE, "Select the destination folder")
|
|
||||||
},
|
|
||||||
REQ_EXPORT_FILES
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
pendingPicker = null
|
|
||||||
result.error("picker_unavailable", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GET_VALUE -> {
|
GET_VALUE -> {
|
||||||
if (call.arguments is String) {
|
if (call.arguments is String) {
|
||||||
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
||||||
@@ -654,228 +291,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canonicalAppScopedFile(path: String): File? {
|
|
||||||
val file = runCatching { File(path).canonicalFile }.getOrNull() ?: return null
|
|
||||||
val allowedRoots = listOfNotNull(filesDir, getExternalFilesDir(null)).mapNotNull {
|
|
||||||
runCatching { it.canonicalFile }.getOrNull()
|
|
||||||
}
|
|
||||||
return file.takeIf { candidate ->
|
|
||||||
allowedRoots.any { root ->
|
|
||||||
candidate == root || candidate.path.startsWith(root.path + File.separator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun canonicalExportSource(path: String): File? {
|
|
||||||
val original = File(path).absoluteFile
|
|
||||||
val canonical = canonicalAppScopedFile(path) ?: return null
|
|
||||||
return canonical.takeIf {
|
|
||||||
original.path == canonical.path && (canonical.isFile || canonical.isDirectory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun snapshotExportSource(source: File): ExportSource? {
|
|
||||||
val safeSource = canonicalExportSource(source.path) ?: return null
|
|
||||||
if (safeSource.isFile) return ExportSource(safeSource, null)
|
|
||||||
val sourceChildren = safeSource.listFiles() ?: return null
|
|
||||||
val children = ArrayList<ExportSource>(sourceChildren.size)
|
|
||||||
for (child in sourceChildren) {
|
|
||||||
val snapshot = snapshotExportSource(child) ?: return null
|
|
||||||
children.add(snapshot)
|
|
||||||
}
|
|
||||||
return ExportSource(safeSource, children)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyExportSourceToTree(
|
|
||||||
treeUri: Uri,
|
|
||||||
parentDocId: String,
|
|
||||||
source: ExportSource
|
|
||||||
): Boolean {
|
|
||||||
val children = source.children
|
|
||||||
return if (children == null) {
|
|
||||||
copyFileToTree(treeUri, parentDocId, source.file)
|
|
||||||
} else {
|
|
||||||
copyDirToTree(treeUri, parentDocId, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun treeDisplayName(treeUri: Uri): String? {
|
|
||||||
return try {
|
|
||||||
val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
|
||||||
val docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, rootDocId)
|
|
||||||
contentResolver.query(
|
|
||||||
docUri,
|
|
||||||
arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(logTag, "Failed to read selected folder name", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyDocumentTreeToFile(treeUri: Uri, destinationDir: File): Boolean {
|
|
||||||
val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
|
||||||
return copyChildrenToFile(treeUri, rootDocId, destinationDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyChildrenToFile(
|
|
||||||
treeUri: Uri,
|
|
||||||
parentDocId: String,
|
|
||||||
destinationDir: File
|
|
||||||
): Boolean {
|
|
||||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocId)
|
|
||||||
var ok = true
|
|
||||||
val destinationNames = HashSet<String>()
|
|
||||||
val cursor = contentResolver.query(childrenUri, childColumns, null, null, null)
|
|
||||||
?: return false
|
|
||||||
cursor.use {
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val docId = cursor.getString(0)
|
|
||||||
val name = cursor.getString(1)
|
|
||||||
val mime = cursor.getString(2)
|
|
||||||
val docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docId)
|
|
||||||
if (name != null && !destinationNames.add(name)) {
|
|
||||||
ok = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val destination = safeDestinationChild(destinationDir, name)
|
|
||||||
if (destination == null || destination.exists()) {
|
|
||||||
ok = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (mime == DocumentsContract.Document.MIME_TYPE_DIR) {
|
|
||||||
if (!destination.mkdirs() && !destination.isDirectory) {
|
|
||||||
ok = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!copyChildrenToFile(treeUri, docId, destination)) {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
} else if (!copyDocumentToFile(docUri, destination)) {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun safeDestinationChild(destinationDir: File, name: String?): File? {
|
|
||||||
if (name.isNullOrEmpty() || name == "." || name == ".." ||
|
|
||||||
name.indexOf('\u0000') >= 0 || name.contains('/') || name.contains('\\')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val parent = runCatching { destinationDir.canonicalFile }.getOrNull() ?: return null
|
|
||||||
val child = runCatching { File(parent, name).canonicalFile }.getOrNull() ?: return null
|
|
||||||
return child.takeIf { it.path.startsWith(parent.path + File.separator) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyDocumentToFile(uri: Uri, destination: File): Boolean {
|
|
||||||
return try {
|
|
||||||
destination.parentFile?.mkdirs()
|
|
||||||
if (destination.exists() && !destination.delete()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
contentResolver.openInputStream(uri)?.use { input ->
|
|
||||||
FileOutputStream(destination).use { output -> input.copyTo(output) }
|
|
||||||
} != null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to copy document to $destination", e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyFileToTree(treeUri: Uri, parentDocId: String, source: File): Boolean {
|
|
||||||
val safeSource = canonicalExportSource(source.path)?.takeIf { it.isFile } ?: return false
|
|
||||||
return try {
|
|
||||||
val mime = MimeTypeMap.getSingleton()
|
|
||||||
.getMimeTypeFromExtension(safeSource.extension.lowercase())
|
|
||||||
?: "application/octet-stream"
|
|
||||||
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocId)
|
|
||||||
val docUri = DocumentsContract.createDocument(
|
|
||||||
contentResolver,
|
|
||||||
parentUri,
|
|
||||||
mime,
|
|
||||||
safeSource.name
|
|
||||||
) ?: return false
|
|
||||||
contentResolver.openOutputStream(docUri, "wt")?.use { output ->
|
|
||||||
FileInputStream(safeSource).use { input -> input.copyTo(output) }
|
|
||||||
} ?: return false
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to export file $safeSource", e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyDirToTree(
|
|
||||||
treeUri: Uri,
|
|
||||||
parentDocId: String,
|
|
||||||
source: ExportSource
|
|
||||||
): Boolean {
|
|
||||||
val children = source.children ?: return false
|
|
||||||
val safeSource = canonicalExportSource(source.file.path)?.takeIf { it.isDirectory }
|
|
||||||
?: return false
|
|
||||||
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocId)
|
|
||||||
var dirDocId = findChildDocId(treeUri, parentDocId, safeSource.name)
|
|
||||||
if (dirDocId == null) {
|
|
||||||
dirDocId = try {
|
|
||||||
DocumentsContract.createDocument(
|
|
||||||
contentResolver,
|
|
||||||
parentUri,
|
|
||||||
DocumentsContract.Document.MIME_TYPE_DIR,
|
|
||||||
safeSource.name
|
|
||||||
)?.let { DocumentsContract.getDocumentId(it) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(logTag, "Failed to create folder ${safeSource.name}", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dirDocId == null) return false
|
|
||||||
|
|
||||||
var ok = true
|
|
||||||
children.forEach { child ->
|
|
||||||
val childOk = copyExportSourceToTree(treeUri, dirDocId, child)
|
|
||||||
if (!childOk) ok = false
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findChildDocId(treeUri: Uri, parentDocId: String, name: String): String? {
|
|
||||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocId)
|
|
||||||
val cursor = contentResolver.query(childrenUri, childColumns, null, null, null)
|
|
||||||
?: throw IllegalStateException("Unable to query destination folder")
|
|
||||||
cursor.use {
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
if (cursor.getString(1) == name &&
|
|
||||||
cursor.getString(2) == DocumentsContract.Document.MIME_TYPE_DIR
|
|
||||||
) {
|
|
||||||
return cursor.getString(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val childColumns = arrayOf(
|
|
||||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
|
||||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
|
||||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun displayName(uri: Uri): String? {
|
|
||||||
return try {
|
|
||||||
contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) cursor.getString(0) else null
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(logTag, "Failed to read selected document name", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setCodecInfo() {
|
private fun setCodecInfo() {
|
||||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||||
val codecs = codecList.codecInfos
|
val codecs = codecList.codecInfos
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -151,7 +150,7 @@ class MainService : Service() {
|
|||||||
if (incomingVoiceCall) {
|
if (incomingVoiceCall) {
|
||||||
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||||
} else {
|
} else {
|
||||||
if (!switchOutVoiceCall()) {
|
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
||||||
Log.e(logTag, "switchOutVoiceCall fail")
|
Log.e(logTag, "switchOutVoiceCall fail")
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
"type" to "custom-nook-nocancel-hasclose-error",
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
@@ -160,7 +159,7 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!switchToVoiceCall()) {
|
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
||||||
Log.e(logTag, "switchToVoiceCall fail")
|
Log.e(logTag, "switchToVoiceCall fail")
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
"type" to "custom-nook-nocancel-hasclose-error",
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
@@ -215,19 +214,6 @@ class MainService : Service() {
|
|||||||
|
|
||||||
// video
|
// video
|
||||||
private var mediaProjection: MediaProjection? = null
|
private var mediaProjection: MediaProjection? = null
|
||||||
private var mediaProjectionCallback: MediaProjection.Callback? = null
|
|
||||||
private var captureRestartPending = false
|
|
||||||
private var captureRestartInVoiceCall = false
|
|
||||||
private val mediaProjectionResultReceiver =
|
|
||||||
object : ResultReceiver(Handler(Looper.getMainLooper())) {
|
|
||||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
|
||||||
if (resultCode == RES_FAILED) {
|
|
||||||
cancelMediaProjectionRecovery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var mediaProjectionForegroundService = false
|
|
||||||
private var microphoneForegroundService = false
|
|
||||||
private var surface: Surface? = null
|
private var surface: Surface? = null
|
||||||
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
||||||
private var videoEncoder: MediaCodec? = null
|
private var videoEncoder: MediaCodec? = null
|
||||||
@@ -257,9 +243,7 @@ class MainService : Service() {
|
|||||||
// keep the config dir same with flutter
|
// keep the config dir same with flutter
|
||||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||||
val homePath = applicationContext.getExternalFilesDir(null)?.absolutePath
|
FFI.startServer(configPath, "")
|
||||||
?: applicationContext.filesDir.absolutePath
|
|
||||||
FFI.startServer(configPath, homePath, "")
|
|
||||||
|
|
||||||
createForegroundNotification()
|
createForegroundNotification()
|
||||||
}
|
}
|
||||||
@@ -353,6 +337,8 @@ class MainService : Service() {
|
|||||||
Log.d("whichService", "this service: ${Thread.currentThread()}")
|
Log.d("whichService", "this service: ${Thread.currentThread()}")
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
||||||
|
createForegroundNotification()
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||||
FFI.startService()
|
FFI.startService()
|
||||||
}
|
}
|
||||||
@@ -361,7 +347,10 @@ class MainService : Service() {
|
|||||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||||
|
|
||||||
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
||||||
replaceMediaProjection(mediaProjectionManager, it)
|
mediaProjection =
|
||||||
|
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
||||||
|
checkMediaPermission()
|
||||||
|
_isReady = true
|
||||||
} ?: let {
|
} ?: let {
|
||||||
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
||||||
requestMediaProjection()
|
requestMediaProjection()
|
||||||
@@ -375,23 +364,14 @@ class MainService : Service() {
|
|||||||
updateScreenInfo(newConfig.orientation)
|
updateScreenInfo(newConfig.orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestMediaProjection(recovery: Boolean = false) {
|
private fun requestMediaProjection() {
|
||||||
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
||||||
action = ACT_REQUEST_MEDIA_PROJECTION
|
action = ACT_REQUEST_MEDIA_PROJECTION
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
if (recovery) {
|
|
||||||
putExtra(EXT_MEDIA_PROJECTION_RESULT_RECEIVER, mediaProjectionResultReceiver)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun cancelMediaProjectionRecovery() {
|
|
||||||
captureRestartPending = false
|
|
||||||
captureRestartInVoiceCall = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
private fun createSurface(): Surface? {
|
private fun createSurface(): Surface? {
|
||||||
return if (useVP9) {
|
return if (useVP9) {
|
||||||
@@ -425,149 +405,15 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseMediaProjection() {
|
|
||||||
val projection = mediaProjection
|
|
||||||
val callback = mediaProjectionCallback
|
|
||||||
mediaProjection = null
|
|
||||||
mediaProjectionCallback = null
|
|
||||||
if (projection != null && callback != null) {
|
|
||||||
projection.unregisterCallback(callback)
|
|
||||||
}
|
|
||||||
projection?.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun handleMediaProjectionStopped(stoppedProjection: MediaProjection) {
|
|
||||||
if (mediaProjection !== stoppedProjection) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.d(logTag, "MediaProjection stopped")
|
|
||||||
setMediaProjectionForegroundService(false)
|
|
||||||
stopCapture()
|
|
||||||
virtualDisplay?.release()
|
|
||||||
virtualDisplay = null
|
|
||||||
mediaProjection = null
|
|
||||||
mediaProjectionCallback = null
|
|
||||||
_isReady = false
|
|
||||||
checkMediaPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun replaceMediaProjection(
|
|
||||||
mediaProjectionManager: MediaProjectionManager,
|
|
||||||
resultIntent: Intent,
|
|
||||||
) {
|
|
||||||
val wasCapturing = isStart
|
|
||||||
val restartCapture = wasCapturing || captureRestartPending
|
|
||||||
val restartInVoiceCall = if (wasCapturing) {
|
|
||||||
audioRecordHandle.isVoiceCallActive()
|
|
||||||
} else {
|
|
||||||
captureRestartInVoiceCall
|
|
||||||
}
|
|
||||||
val hadProjection = mediaProjection != null
|
|
||||||
if (!setMediaProjectionForegroundService(true)) {
|
|
||||||
if (!hadProjection) {
|
|
||||||
cancelMediaProjectionRecovery()
|
|
||||||
_isReady = false
|
|
||||||
checkMediaPermission()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val projection =
|
|
||||||
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, resultIntent)
|
|
||||||
if (projection == null) {
|
|
||||||
if (!hadProjection) {
|
|
||||||
cancelMediaProjectionRecovery()
|
|
||||||
_isReady = false
|
|
||||||
setMediaProjectionForegroundService(false)
|
|
||||||
checkMediaPermission()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (wasCapturing) {
|
|
||||||
stopCapture()
|
|
||||||
}
|
|
||||||
captureRestartPending = restartCapture
|
|
||||||
virtualDisplay?.release()
|
|
||||||
virtualDisplay = null
|
|
||||||
releaseMediaProjection()
|
|
||||||
val callback = object : MediaProjection.Callback() {
|
|
||||||
override fun onStop() {
|
|
||||||
handleMediaProjectionStopped(projection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
projection.registerCallback(callback, Handler(Looper.getMainLooper()))
|
|
||||||
mediaProjection = projection
|
|
||||||
mediaProjectionCallback = callback
|
|
||||||
_isReady = true
|
|
||||||
checkMediaPermission()
|
|
||||||
if (restartCapture) {
|
|
||||||
captureRestartPending = false
|
|
||||||
startCapture(restartInVoiceCall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun startMicrophoneCapture(startAudio: () -> Boolean): Boolean {
|
|
||||||
if (!setMicrophoneForegroundService(true)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (startAudio()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
setMicrophoneForegroundService(false)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun stopMicrophoneCapture(stopAudio: () -> Boolean): Boolean {
|
|
||||||
val stopped = stopAudio()
|
|
||||||
val foregroundServiceUpdated = setMicrophoneForegroundService(false)
|
|
||||||
return stopped && foregroundServiceUpdated
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun switchToVoiceCall(): Boolean {
|
|
||||||
if (captureRestartPending) {
|
|
||||||
captureRestartInVoiceCall = true
|
|
||||||
}
|
|
||||||
return startMicrophoneCapture {
|
|
||||||
audioRecordHandle.switchToVoiceCall(mediaProjection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun switchOutVoiceCall(): Boolean {
|
|
||||||
captureRestartInVoiceCall = false
|
|
||||||
val switched = audioRecordHandle.switchOutVoiceCall(mediaProjection)
|
|
||||||
val foregroundServiceUpdated = setMicrophoneForegroundService(false)
|
|
||||||
return switched && foregroundServiceUpdated
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun onVoiceCallStarted(): Boolean {
|
fun onVoiceCallStarted(): Boolean {
|
||||||
if (captureRestartPending) {
|
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||||
captureRestartInVoiceCall = true
|
|
||||||
}
|
|
||||||
return startMicrophoneCapture {
|
|
||||||
audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun onVoiceCallClosed(): Boolean {
|
fun onVoiceCallClosed(): Boolean {
|
||||||
captureRestartInVoiceCall = false
|
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||||
return stopMicrophoneCapture {
|
|
||||||
audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
return startCapture(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun startCapture(inVoiceCall: Boolean): Boolean {
|
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -575,35 +421,25 @@ class MainService : Service() {
|
|||||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
captureRestartInVoiceCall = inVoiceCall
|
|
||||||
|
|
||||||
updateScreenInfo(resources.configuration.orientation)
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
Log.d(logTag, "Start Capture")
|
Log.d(logTag, "Start Capture")
|
||||||
surface = createSurface()
|
surface = createSurface()
|
||||||
|
|
||||||
val videoStarted = if (useVP9) {
|
if (useVP9) {
|
||||||
startVP9VideoRecorder(mediaProjection!!)
|
startVP9VideoRecorder(mediaProjection!!)
|
||||||
} else {
|
} else {
|
||||||
startRawVideoRecorder(mediaProjection!!)
|
startRawVideoRecorder(mediaProjection!!)
|
||||||
}
|
}
|
||||||
if (!videoStarted) {
|
|
||||||
if (!captureRestartPending) {
|
|
||||||
captureRestartInVoiceCall = false
|
|
||||||
}
|
|
||||||
releaseFailedVideoCapture()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val audioStarted = if (inVoiceCall) {
|
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
||||||
switchToVoiceCall()
|
Log.d(logTag, "createAudioRecorder fail")
|
||||||
} else {
|
} else {
|
||||||
audioRecordHandle.createAudioRecorder(false, mediaProjection) &&
|
Log.d(logTag, "audio recorder start")
|
||||||
audioRecordHandle.startAudioRecorder()
|
audioRecordHandle.startAudioRecorder()
|
||||||
}
|
}
|
||||||
Log.d(logTag, if (audioStarted) "audio recorder start" else "audio recorder start failed")
|
|
||||||
}
|
}
|
||||||
captureRestartInVoiceCall = false
|
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
FFI.setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
@@ -611,24 +447,9 @@ class MainService : Service() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseFailedVideoCapture() {
|
|
||||||
imageReader?.close()
|
|
||||||
imageReader = null
|
|
||||||
videoEncoder?.let {
|
|
||||||
it.signalEndOfInputStream()
|
|
||||||
it.stop()
|
|
||||||
it.release()
|
|
||||||
}
|
|
||||||
videoEncoder = null
|
|
||||||
surface?.release()
|
|
||||||
surface = null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
captureRestartPending = false
|
|
||||||
captureRestartInVoiceCall = false
|
|
||||||
FFI.setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
_isStart = false
|
_isStart = false
|
||||||
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
||||||
@@ -659,11 +480,8 @@ class MainService : Service() {
|
|||||||
surface?.release()
|
surface?.release()
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
stopMicrophoneCapture {
|
_isAudioStart = false
|
||||||
_isAudioStart = false
|
audioRecordHandle.tryReleaseAudio()
|
||||||
audioRecordHandle.tryReleaseAudio()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
@@ -678,9 +496,7 @@ class MainService : Service() {
|
|||||||
virtualDisplay = null
|
virtualDisplay = null
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseMediaProjection()
|
mediaProjection = null
|
||||||
mediaProjectionForegroundService = false
|
|
||||||
microphoneForegroundService = false
|
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopService(Intent(this, FloatingWindowService::class.java))
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
@@ -703,70 +519,49 @@ class MainService : Service() {
|
|||||||
return isReady
|
return isReady
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRawVideoRecorder(mp: MediaProjection): Boolean {
|
private fun startRawVideoRecorder(mp: MediaProjection) {
|
||||||
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
||||||
val captureSurface = surface
|
if (surface == null) {
|
||||||
if (captureSurface == null) {
|
|
||||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
return createOrSetVirtualDisplay(mp, captureSurface)
|
createOrSetVirtualDisplay(mp, surface!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startVP9VideoRecorder(mp: MediaProjection): Boolean {
|
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
||||||
createMediaCodec()
|
createMediaCodec()
|
||||||
val encoder = videoEncoder ?: return false
|
videoEncoder?.let {
|
||||||
val inputSurface = encoder.createInputSurface()
|
surface = it.createInputSurface()
|
||||||
surface = inputSurface
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
||||||
inputSurface.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
}
|
||||||
|
it.setCallback(cb)
|
||||||
|
it.start()
|
||||||
|
createOrSetVirtualDisplay(mp, surface!!)
|
||||||
}
|
}
|
||||||
encoder.setCallback(cb)
|
|
||||||
encoder.start()
|
|
||||||
return createOrSetVirtualDisplay(mp, inputSurface)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||||
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||||
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface): Boolean {
|
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
||||||
return try {
|
try {
|
||||||
val existingDisplay = virtualDisplay
|
virtualDisplay?.let {
|
||||||
if (existingDisplay != null) {
|
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||||
existingDisplay.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
it.setSurface(s)
|
||||||
existingDisplay.setSurface(s)
|
} ?: let {
|
||||||
true
|
virtualDisplay = mp.createVirtualDisplay(
|
||||||
} else {
|
|
||||||
val display = mp.createVirtualDisplay(
|
|
||||||
"RustDeskVD",
|
"RustDeskVD",
|
||||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
s, null, null
|
s, null, null
|
||||||
)
|
)
|
||||||
if (display == null) {
|
|
||||||
Log.e(logTag, "createOrSetVirtualDisplay failed")
|
|
||||||
handleVirtualDisplayFailure()
|
|
||||||
} else {
|
|
||||||
virtualDisplay = display
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException", e)
|
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
||||||
handleVirtualDisplayFailure()
|
// This initiates a prompt dialog for the user to confirm screen projection.
|
||||||
|
requestMediaProjection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleVirtualDisplayFailure(): Boolean {
|
|
||||||
captureRestartPending = true
|
|
||||||
virtualDisplay?.release()
|
|
||||||
virtualDisplay = null
|
|
||||||
releaseMediaProjection()
|
|
||||||
setMediaProjectionForegroundService(false)
|
|
||||||
_isReady = false
|
|
||||||
checkMediaPermission()
|
|
||||||
requestMediaProjection(true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
|
private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
|
||||||
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
|
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
|
||||||
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
|
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
|
||||||
@@ -857,63 +652,7 @@ class MainService : Service() {
|
|||||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.build()
|
.build()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
startForeground(DEFAULT_NOTIFY_ID, notification)
|
||||||
startForeground(DEFAULT_NOTIFY_ID, notification, foregroundServiceType())
|
|
||||||
} else {
|
|
||||||
startForeground(DEFAULT_NOTIFY_ID, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
private fun foregroundServiceType(): Int {
|
|
||||||
var serviceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
// Keep a valid FGS type while the unattended host is idle and no capture type is active.
|
|
||||||
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
||||||
}
|
|
||||||
if (mediaProjectionForegroundService) {
|
|
||||||
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && microphoneForegroundService) {
|
|
||||||
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
||||||
}
|
|
||||||
return serviceType
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setMediaProjectionForegroundService(enabled: Boolean): Boolean {
|
|
||||||
return updateForegroundServiceTypes(enabled, microphoneForegroundService)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setMicrophoneForegroundService(enabled: Boolean): Boolean {
|
|
||||||
return updateForegroundServiceTypes(mediaProjectionForegroundService, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForegroundServiceTypes(
|
|
||||||
mediaProjectionEnabled: Boolean,
|
|
||||||
microphoneEnabled: Boolean,
|
|
||||||
): Boolean {
|
|
||||||
if (mediaProjectionForegroundService == mediaProjectionEnabled &&
|
|
||||||
microphoneForegroundService == microphoneEnabled) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val previousMediaProjection = mediaProjectionForegroundService
|
|
||||||
val previousMicrophone = microphoneForegroundService
|
|
||||||
mediaProjectionForegroundService = mediaProjectionEnabled
|
|
||||||
microphoneForegroundService = microphoneEnabled
|
|
||||||
return try {
|
|
||||||
createForegroundNotification()
|
|
||||||
true
|
|
||||||
} catch (error: SecurityException) {
|
|
||||||
mediaProjectionForegroundService = previousMediaProjection
|
|
||||||
microphoneForegroundService = previousMicrophone
|
|
||||||
Log.e(logTag, "Failed to update foreground service types", error)
|
|
||||||
false
|
|
||||||
} catch (error: IllegalStateException) {
|
|
||||||
mediaProjectionForegroundService = previousMediaProjection
|
|
||||||
microphoneForegroundService = previousMicrophone
|
|
||||||
Log.e(logTag, "Failed to update foreground service types", error)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loginRequestNotification(
|
private fun loginRequestNotification(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
|||||||
import android.media.projection.MediaProjectionManager
|
import android.media.projection.MediaProjectionManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ResultReceiver
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
class PermissionRequestTransparentActivity: Activity() {
|
class PermissionRequestTransparentActivity: Activity() {
|
||||||
@@ -32,13 +31,7 @@ class PermissionRequestTransparentActivity: Activity() {
|
|||||||
if (resultCode == RESULT_OK && data != null) {
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
launchService(data)
|
launchService(data)
|
||||||
} else {
|
} else {
|
||||||
val resultReceiver =
|
setResult(RES_FAILED)
|
||||||
intent.getParcelableExtra<ResultReceiver>(EXT_MEDIA_PROJECTION_RESULT_RECEIVER)
|
|
||||||
if (resultReceiver != null) {
|
|
||||||
resultReceiver.send(RES_FAILED, null)
|
|
||||||
} else {
|
|
||||||
setResult(RES_FAILED)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,4 +51,4 @@ class PermissionRequestTransparentActivity: Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -33,16 +33,11 @@ const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SER
|
|||||||
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||||
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
||||||
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
||||||
const val EXT_MEDIA_PROJECTION_RESULT_RECEIVER = "MEDIA_PROJECTION_RESULT_RECEIVER"
|
|
||||||
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||||
|
|
||||||
// Activity requestCode
|
// Activity requestCode
|
||||||
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
|
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
|
||||||
const val REQ_REQUEST_MEDIA_PROJECTION = 201
|
const val REQ_REQUEST_MEDIA_PROJECTION = 201
|
||||||
const val REQ_EXPORT_FILE = 301
|
|
||||||
const val REQ_IMPORT_FILES = 302
|
|
||||||
const val REQ_IMPORT_DIRECTORY = 303
|
|
||||||
const val REQ_EXPORT_FILES = 304
|
|
||||||
|
|
||||||
// Activity responseCode
|
// Activity responseCode
|
||||||
const val RES_FAILED = -100
|
const val RES_FAILED = -100
|
||||||
@@ -52,12 +47,6 @@ const val START_ACTION = "start_action"
|
|||||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||||
const val PICK_IMPORT_FILES = "pick_import_files"
|
|
||||||
const val IMPORT_FILE = "import_file"
|
|
||||||
const val EXPORT_FILE = "export_file"
|
|
||||||
const val PICK_IMPORT_DIRECTORY = "pick_import_directory"
|
|
||||||
const val IMPORT_DIRECTORY = "import_directory"
|
|
||||||
const val EXPORT_FILES = "export_files"
|
|
||||||
const val GET_VALUE = "get_value"
|
const val GET_VALUE = "get_value"
|
||||||
|
|
||||||
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
||||||
@@ -165,4 +154,4 @@ fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
|
|||||||
fun translate(input: String): String {
|
fun translate(input: String): String {
|
||||||
Log.d("common", "translate:$LOCAL_NAME")
|
Log.d("common", "translate:$LOCAL_NAME")
|
||||||
return FFI.translateLocale(LOCAL_NAME, input)
|
return FFI.translateLocale(LOCAL_NAME, input)
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ object FFI {
|
|||||||
external fun init(ctx: Context)
|
external fun init(ctx: Context)
|
||||||
external fun onAppStart(ctx: Context)
|
external fun onAppStart(ctx: Context)
|
||||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||||
external fun startServer(app_dir: String, home_dir: String, custom_client_config: String)
|
external fun startServer(app_dir: String, custom_client_config: String)
|
||||||
external fun startService()
|
external fun startService()
|
||||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||||
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RustDesk</string>
|
<string name="app_name">RustDesk</string>
|
||||||
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
||||||
<string name="foreground_service_special_use_subtype">Keeps the RustDesk remote desktop host available for authorized unattended connections and foreground notifications without starting screen capture before user approval.</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,29 +1,3 @@
|
|||||||
def legacyPluginNamespaces = [
|
|
||||||
external_path: 'com.pinciat.external_path',
|
|
||||||
flutter_keyboard_visibility: 'com.jrai.flutter_keyboard_visibility',
|
|
||||||
qr_code_scanner: 'net.touchcapture.qr.flutterqr',
|
|
||||||
sqflite: 'com.tekartik.sqflite',
|
|
||||||
uni_links: 'name.avioli.unilinks',
|
|
||||||
]
|
|
||||||
|
|
||||||
def java8JvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
def java8KotlinJvmTargets = [
|
|
||||||
app: java8JvmTarget,
|
|
||||||
external_path: java8JvmTarget,
|
|
||||||
qr_code_scanner: java8JvmTarget,
|
|
||||||
]
|
|
||||||
|
|
||||||
def configureKotlinJvmTarget = { Project project, String kotlinJvmTarget ->
|
|
||||||
project.plugins.withId('kotlin-android') {
|
|
||||||
project.tasks.configureEach { task ->
|
|
||||||
if (!task.hasProperty('kotlinOptions')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
task.kotlinOptions.jvmTarget = kotlinJvmTarget
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -35,16 +9,6 @@ allprojects {
|
|||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
def legacyNamespace = legacyPluginNamespaces[project.name]
|
|
||||||
if (legacyNamespace != null) {
|
|
||||||
project.plugins.withId('com.android.library') {
|
|
||||||
project.android.namespace = legacyNamespace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def kotlinJvmTarget = java8KotlinJvmTargets[project.name]
|
|
||||||
if (kotlinJvmTarget != null) {
|
|
||||||
configureKotlinJvmTarget(project, kotlinJvmTarget)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.10.1" apply false
|
id "com.android.application" version "7.3.1" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ const double _kPositionEpsilon = 1e-6;
|
|||||||
bool get isMainDesktopWindow =>
|
bool get isMainDesktopWindow =>
|
||||||
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
||||||
|
|
||||||
|
String get screenInfo => screenInfo_;
|
||||||
|
|
||||||
/// Check if the app is running with single view mode.
|
/// Check if the app is running with single view mode.
|
||||||
bool isSingleViewApp() {
|
bool isSingleViewApp() {
|
||||||
return desktopType == DesktopType.cm;
|
return desktopType == DesktopType.cm;
|
||||||
@@ -1519,6 +1521,13 @@ class AndroidPermissionManager {
|
|||||||
static Timer? _timer;
|
static Timer? _timer;
|
||||||
static var _current = "";
|
static var _current = "";
|
||||||
|
|
||||||
|
static bool isWaitingFile() {
|
||||||
|
if (_completer != null) {
|
||||||
|
return !_completer!.isCompleted && _current == kManageExternalStorage;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> check(String type) {
|
static Future<bool> check(String type) {
|
||||||
if (isDesktop || isWeb) {
|
if (isDesktop || isWeb) {
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
@@ -2627,6 +2636,13 @@ connect(BuildContext context, String id,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
|
if (isAndroid) {
|
||||||
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
|
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -936,19 +936,26 @@ void enterPasswordDialog(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterUserLoginDialog(SessionID sessionId,
|
void enterUserLoginDialog(
|
||||||
OverlayDialogManager dialogManager, String osAccountDescTip) async {
|
SessionID sessionId,
|
||||||
|
OverlayDialogManager dialogManager,
|
||||||
|
String osAccountDescTip,
|
||||||
|
bool canRememberAccount) async {
|
||||||
await _connectDialog(
|
await _connectDialog(
|
||||||
sessionId,
|
sessionId,
|
||||||
dialogManager,
|
dialogManager,
|
||||||
osUsernameController: TextEditingController(),
|
osUsernameController: TextEditingController(),
|
||||||
osPasswordController: TextEditingController(),
|
osPasswordController: TextEditingController(),
|
||||||
osAccountDescTip: osAccountDescTip,
|
osAccountDescTip: osAccountDescTip,
|
||||||
|
canRememberAccount: canRememberAccount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterUserLoginAndPasswordDialog(SessionID sessionId,
|
void enterUserLoginAndPasswordDialog(
|
||||||
OverlayDialogManager dialogManager, String osAccountDescTip) async {
|
SessionID sessionId,
|
||||||
|
OverlayDialogManager dialogManager,
|
||||||
|
String osAccountDescTip,
|
||||||
|
bool canRememberAccount) async {
|
||||||
await _connectDialog(
|
await _connectDialog(
|
||||||
sessionId,
|
sessionId,
|
||||||
dialogManager,
|
dialogManager,
|
||||||
@@ -956,6 +963,7 @@ void enterUserLoginAndPasswordDialog(SessionID sessionId,
|
|||||||
osPasswordController: TextEditingController(),
|
osPasswordController: TextEditingController(),
|
||||||
passwordController: TextEditingController(),
|
passwordController: TextEditingController(),
|
||||||
osAccountDescTip: osAccountDescTip,
|
osAccountDescTip: osAccountDescTip,
|
||||||
|
canRememberAccount: canRememberAccount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,6 +974,7 @@ _connectDialog(
|
|||||||
TextEditingController? osPasswordController,
|
TextEditingController? osPasswordController,
|
||||||
TextEditingController? passwordController,
|
TextEditingController? passwordController,
|
||||||
String? osAccountDescTip,
|
String? osAccountDescTip,
|
||||||
|
bool canRememberAccount = true,
|
||||||
}) async {
|
}) async {
|
||||||
final errUsername = ''.obs;
|
final errUsername = ''.obs;
|
||||||
var rememberPassword = false;
|
var rememberPassword = false;
|
||||||
@@ -973,6 +982,11 @@ _connectDialog(
|
|||||||
rememberPassword =
|
rememberPassword =
|
||||||
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
||||||
}
|
}
|
||||||
|
var rememberAccount = false;
|
||||||
|
if (canRememberAccount && osUsernameController != null) {
|
||||||
|
rememberAccount =
|
||||||
|
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
||||||
|
}
|
||||||
if (osUsernameController != null) {
|
if (osUsernameController != null) {
|
||||||
osUsernameController.addListener(() {
|
osUsernameController.addListener(() {
|
||||||
if (errUsername.value.isNotEmpty) {
|
if (errUsername.value.isNotEmpty) {
|
||||||
@@ -1000,6 +1014,12 @@ _connectDialog(
|
|||||||
final osPassword = osPasswordController?.text.trim() ?? '';
|
final osPassword = osPasswordController?.text.trim() ?? '';
|
||||||
final password = passwordController?.text.trim() ?? '';
|
final password = passwordController?.text.trim() ?? '';
|
||||||
if (passwordController != null && password.isEmpty) return;
|
if (passwordController != null && password.isEmpty) return;
|
||||||
|
if (rememberAccount) {
|
||||||
|
bind.sessionPeerOption(
|
||||||
|
sessionId: sessionId, name: 'os-username', value: osUsername);
|
||||||
|
bind.sessionPeerOption(
|
||||||
|
sessionId: sessionId, name: 'os-password', value: osPassword);
|
||||||
|
}
|
||||||
gFFI.login(
|
gFFI.login(
|
||||||
osUsername,
|
osUsername,
|
||||||
osPassword,
|
osPassword,
|
||||||
@@ -1076,6 +1096,16 @@ _connectDialog(
|
|||||||
controller: osPasswordController,
|
controller: osPasswordController,
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
),
|
),
|
||||||
|
if (canRememberAccount)
|
||||||
|
rememberWidget(
|
||||||
|
translate('remember_account_tip'),
|
||||||
|
rememberAccount,
|
||||||
|
(v) {
|
||||||
|
if (v != null) {
|
||||||
|
setState(() => rememberAccount = v);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1512,6 +1542,91 @@ showSetOSPassword(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSetOSAccount(
|
||||||
|
SessionID sessionId,
|
||||||
|
OverlayDialogManager dialogManager,
|
||||||
|
) async {
|
||||||
|
final usernameController = TextEditingController();
|
||||||
|
final passwdController = TextEditingController();
|
||||||
|
var username =
|
||||||
|
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-username') ??
|
||||||
|
'';
|
||||||
|
var password =
|
||||||
|
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
|
||||||
|
'';
|
||||||
|
usernameController.text = username;
|
||||||
|
passwdController.text = password;
|
||||||
|
dialogManager.show((setState, close, context) {
|
||||||
|
submit() {
|
||||||
|
final username = usernameController.text.trim();
|
||||||
|
final password = usernameController.text.trim();
|
||||||
|
bind.sessionPeerOption(
|
||||||
|
sessionId: sessionId, name: 'os-username', value: username);
|
||||||
|
bind.sessionPeerOption(
|
||||||
|
sessionId: sessionId, name: 'os-password', value: password);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
descWidget(String text) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
maxLines: 3,
|
||||||
|
softWrap: true,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||||
|
Text(translate('OS Account')).paddingOnly(left: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
descWidget(translate("os_account_desk_tip")),
|
||||||
|
DialogTextField(
|
||||||
|
title: translate(DialogTextField.kUsernameTitle),
|
||||||
|
controller: usernameController,
|
||||||
|
prefixIcon: DialogTextField.kUsernameIcon,
|
||||||
|
errorText: null,
|
||||||
|
),
|
||||||
|
PasswordWidget(controller: passwdController),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton(
|
||||||
|
"Cancel",
|
||||||
|
icon: Icon(Icons.close_rounded),
|
||||||
|
onPressed: close,
|
||||||
|
isOutline: true,
|
||||||
|
),
|
||||||
|
dialogButton(
|
||||||
|
"OK",
|
||||||
|
icon: Icon(Icons.done_rounded),
|
||||||
|
onPressed: submit,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildNoteTextField({
|
Widget buildNoteTextField({
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required VoidCallback onEscape,
|
required VoidCallback onEscape,
|
||||||
@@ -1899,110 +2014,26 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
|||||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _validateTrackpadSpeed(String text) {
|
trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
|
||||||
final speed = int.tryParse(text);
|
int initSpeed = ffi.inputModel.trackpadSpeed;
|
||||||
if (speed == null || speed < kMinTrackpadSpeed || speed > kMaxTrackpadSpeed) {
|
|
||||||
BotToast.showText(
|
|
||||||
text:
|
|
||||||
'${translate('Invalid format')}: $kMinTrackpadSpeed-$kMaxTrackpadSpeed',
|
|
||||||
contentColor: Colors.red,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveTrackpadSpeed({
|
|
||||||
required SessionID sessionId,
|
|
||||||
required FFI ffi,
|
|
||||||
required int initSpeed,
|
|
||||||
required int speed,
|
|
||||||
}) async {
|
|
||||||
if (speed == initSpeed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await bind.sessionSetTrackpadSpeed(sessionId: sessionId, value: speed);
|
|
||||||
await ffi.inputModel.updateTrackpadSpeed();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showTrackpadSpeedSaveError(Object error, StackTrace stackTrace) {
|
|
||||||
debugPrint('Failed to save trackpad speed: $error');
|
|
||||||
debugPrintStack(stackTrace: stackTrace);
|
|
||||||
BotToast.showText(
|
|
||||||
text: translate('Failed'),
|
|
||||||
contentColor: Colors.red,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _trackpadSpeedDialogActions({
|
|
||||||
required bool isSubmitting,
|
|
||||||
required VoidCallback close,
|
|
||||||
required VoidCallback submit,
|
|
||||||
}) {
|
|
||||||
return [
|
|
||||||
dialogButton(
|
|
||||||
'Cancel',
|
|
||||||
icon: Icon(Icons.close_rounded),
|
|
||||||
onPressed: isSubmitting ? null : close,
|
|
||||||
isOutline: true,
|
|
||||||
),
|
|
||||||
dialogButton(
|
|
||||||
'OK',
|
|
||||||
icon: Icon(Icons.done_rounded),
|
|
||||||
onPressed: isSubmitting ? null : submit,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
void trackpadSpeedDialog(SessionID sessionId, FFI ffi) {
|
|
||||||
final initSpeed = ffi.inputModel.trackpadSpeed;
|
|
||||||
final curSpeed = SimpleWrapper(initSpeed);
|
final curSpeed = SimpleWrapper(initSpeed);
|
||||||
var speedText = initSpeed.toString();
|
final btnClose = dialogButton('Close', onPressed: () async {
|
||||||
var isSubmitting = false;
|
if (curSpeed.value <= kMaxTrackpadSpeed &&
|
||||||
ffi.dialogManager.show((setState, close, context) {
|
curSpeed.value >= kMinTrackpadSpeed &&
|
||||||
Future<void> submit([String? submittedText]) async {
|
curSpeed.value != initSpeed) {
|
||||||
if (isSubmitting) {
|
await bind.sessionSetTrackpadSpeed(
|
||||||
return;
|
sessionId: sessionId, value: curSpeed.value);
|
||||||
}
|
await ffi.inputModel.updateTrackpadSpeed();
|
||||||
speedText = submittedText ?? speedText;
|
|
||||||
final speed = _validateTrackpadSpeed(speedText);
|
|
||||||
if (speed == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() => isSubmitting = true);
|
|
||||||
try {
|
|
||||||
await _saveTrackpadSpeed(
|
|
||||||
sessionId: sessionId,
|
|
||||||
ffi: ffi,
|
|
||||||
initSpeed: initSpeed,
|
|
||||||
speed: speed,
|
|
||||||
);
|
|
||||||
close();
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
_showTrackpadSpeedSaveError(error, stackTrace);
|
|
||||||
setState(() => isSubmitting = false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ffi.dialogManager.dismissAll();
|
||||||
return CustomAlertDialog(
|
|
||||||
title: Text(
|
|
||||||
translate('Trackpad speed'),
|
|
||||||
style: TextStyle(fontSize: 21),
|
|
||||||
),
|
|
||||||
content: TrackpadSpeedWidget(
|
|
||||||
value: curSpeed,
|
|
||||||
onTextChanged: (text) => speedText = text,
|
|
||||||
onTextSubmitted: submit,
|
|
||||||
),
|
|
||||||
actions: _trackpadSpeedDialogActions(
|
|
||||||
isSubmitting: isSubmitting,
|
|
||||||
close: close,
|
|
||||||
submit: submit,
|
|
||||||
),
|
|
||||||
onSubmit: isSubmitting ? null : submit,
|
|
||||||
onCancel: isSubmitting ? null : close,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
msgBoxCommon(
|
||||||
|
ffi.dialogManager,
|
||||||
|
'Trackpad speed',
|
||||||
|
TrackpadSpeedWidget(
|
||||||
|
value: curSpeed,
|
||||||
|
),
|
||||||
|
[btnClose]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteConfirmDialog(Function onSubmit, String title) async {
|
void deleteConfirmDialog(Function onSubmit, String title) async {
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
InputModel get inputModel => widget.inputModel;
|
InputModel get inputModel => widget.inputModel;
|
||||||
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
|
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
|
||||||
SessionID get sessionId => ffi.sessionId;
|
SessionID get sessionId => ffi.sessionId;
|
||||||
bool get canvasLocked => isMobile && ffi.canvasModel.locked;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -472,8 +471,6 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canvasLocked) return;
|
|
||||||
|
|
||||||
if ((isDesktop || isWebDesktop)) {
|
if ((isDesktop || isWebDesktop)) {
|
||||||
final scale = ((d.scale - _scale) * 1000).toInt();
|
final scale = ((d.scale - _scale) * 1000).toInt();
|
||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
|
|||||||
@@ -253,18 +253,8 @@ class TrackpadSpeedWidget extends StatefulWidget {
|
|||||||
final SimpleWrapper<int> value;
|
final SimpleWrapper<int> value;
|
||||||
// If null, no debouncer will be applied.
|
// If null, no debouncer will be applied.
|
||||||
final Function(int)? onDebouncer;
|
final Function(int)? onDebouncer;
|
||||||
final ValueChanged<String>? onTextChanged;
|
|
||||||
// IME actions call TextField.onSubmitted without reaching the dialog's
|
|
||||||
// raw Enter handler, so the dialog needs a separate submission callback.
|
|
||||||
final ValueChanged<String>? onTextSubmitted;
|
|
||||||
|
|
||||||
TrackpadSpeedWidget({
|
TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer});
|
||||||
Key? key,
|
|
||||||
required this.value,
|
|
||||||
this.onDebouncer,
|
|
||||||
this.onTextChanged,
|
|
||||||
this.onTextSubmitted,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
|
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
|
||||||
@@ -286,34 +276,6 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
|
|||||||
debouncerSpeed.setValue(value);
|
debouncerSpeed.setValue(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
widget.onTextChanged?.call(_controller.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateTextValue(String text) {
|
|
||||||
widget.onTextChanged?.call(text);
|
|
||||||
final newValue = int.tryParse(text);
|
|
||||||
if (newValue == null ||
|
|
||||||
newValue < kMinTrackpadSpeed ||
|
|
||||||
newValue > kMaxTrackpadSpeed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() => value = newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
void submitTextValue(String text) {
|
|
||||||
final onTextSubmitted = widget.onTextSubmitted;
|
|
||||||
if (onTextSubmitted != null) {
|
|
||||||
onTextSubmitted(text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (widget.onTextChanged != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final newValue = int.tryParse(text);
|
|
||||||
if (newValue == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateValue(newValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -353,8 +315,12 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
|
|||||||
controller: _controller,
|
controller: _controller,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
onChanged: updateTextValue,
|
onSubmitted: (text) {
|
||||||
onSubmitted: submitTextValue,
|
int? v = int.tryParse(text);
|
||||||
|
if (v != null) {
|
||||||
|
updateValue(v);
|
||||||
|
}
|
||||||
|
},
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
|
|||||||
@@ -349,12 +349,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
showRequestElevationDialog(sessionId, ffi.dialogManager)),
|
showRequestElevationDialog(sessionId, ffi.dialogManager)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// osPassword
|
// osAccount / osPassword
|
||||||
if (isDefaultConn && perms['keyboard'] != false) {
|
if (isDefaultConn && perms['keyboard'] != false) {
|
||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Text(translate('OS Password')),
|
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
|
||||||
]),
|
]),
|
||||||
trailingIcon: Transform.scale(
|
trailingIcon: Transform.scale(
|
||||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||||
@@ -363,12 +363,18 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
if (isMobile && Navigator.canPop(context)) {
|
if (isMobile && Navigator.canPop(context)) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
|
if (pi.isHeadless) {
|
||||||
|
showSetOSAccount(sessionId, ffi.dialogManager);
|
||||||
|
} else {
|
||||||
|
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
|
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => handleOsPasswordAction(sessionId, ffi.dialogManager),
|
onPressed: () => pi.isHeadless
|
||||||
|
? showSetOSAccount(sessionId, ffi.dialogManager)
|
||||||
|
: handleOsPasswordAction(sessionId, ffi.dialogManager),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const kKeyMapMode = 'map';
|
|||||||
const kKeyTranslateMode = 'translate';
|
const kKeyTranslateMode = 'translate';
|
||||||
|
|
||||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||||
|
const String kPlatformAdditionsHeadless = "headless";
|
||||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||||
const String kPlatformAdditionsIddImpl = "idd_impl";
|
const String kPlatformAdditionsIddImpl = "idd_impl";
|
||||||
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
||||||
@@ -54,6 +55,7 @@ const String kAppTypeDesktopTerminal = "terminal";
|
|||||||
|
|
||||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||||
const String kWindowRefreshCurrentUser = "refresh_current_user";
|
const String kWindowRefreshCurrentUser = "refresh_current_user";
|
||||||
|
const String kWindowGetWindowInfo = "get_window_info";
|
||||||
const String kWindowGetScreenList = "get_screen_list";
|
const String kWindowGetScreenList = "get_screen_list";
|
||||||
// This method is not used, maybe it can be removed.
|
// This method is not used, maybe it can be removed.
|
||||||
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
|
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
|
||||||
@@ -86,6 +88,7 @@ const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
|
|||||||
const String kOptionImageQuality = "image_quality";
|
const String kOptionImageQuality = "image_quality";
|
||||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||||
const String kOptionTextureRender = "use-texture-render";
|
const String kOptionTextureRender = "use-texture-render";
|
||||||
|
const String kOptionTextureRenderHealth = "texture-render-health";
|
||||||
const String kOptionD3DRender = "allow-d3d-render";
|
const String kOptionD3DRender = "allow-d3d-render";
|
||||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||||
@@ -162,14 +165,13 @@ const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
|||||||
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||||
const String kOptionAllowAutoUpdate = "allow-auto-update";
|
const String kOptionAllowAutoUpdate = "allow-auto-update";
|
||||||
|
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
const String kOptionStopService = "stop-service";
|
const String kOptionStopService = "stop-service";
|
||||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||||
const String kOptionEnableUdpPunch = "enable-udp-punch";
|
const String kOptionEnableUdpPunch = "enable-udp-punch";
|
||||||
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
||||||
const String kOptionAllowSyncClipboardBetweenSessions =
|
|
||||||
"allow-sync-clipboard-between-sessions";
|
|
||||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||||
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||||
@@ -192,7 +194,6 @@ const String kOptionHideProxySetting = "hide-proxy-settings";
|
|||||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||||
const String kOptionHideStopService = "hide-stop-service";
|
const String kOptionHideStopService = "hide-stop-service";
|
||||||
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
||||||
const String kOptionHideGeneralSetting = "hide-general-settings";
|
|
||||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||||
const String kOptionRemovePresetPasswordWarning =
|
const String kOptionRemovePresetPasswordWarning =
|
||||||
@@ -325,11 +326,10 @@ double kNewWindowOffset = isWindows
|
|||||||
? 30.0
|
? 30.0
|
||||||
: 50.0;
|
: 50.0;
|
||||||
|
|
||||||
const kDragToResizeAreaPaddingSize = 5.0;
|
|
||||||
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
||||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
: EdgeInsets.all(kDragToResizeAreaPaddingSize)
|
: EdgeInsets.all(5.0)
|
||||||
: EdgeInsets.zero;
|
: EdgeInsets.zero;
|
||||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||||
const int $nbsp = 0x00A0;
|
const int $nbsp = 0x00A0;
|
||||||
@@ -441,6 +441,7 @@ const kActionApplicationDetailsSettings =
|
|||||||
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
|
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
|
||||||
|
|
||||||
const kRecordAudio = "android.permission.RECORD_AUDIO";
|
const kRecordAudio = "android.permission.RECORD_AUDIO";
|
||||||
|
const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
||||||
const kRequestIgnoreBatteryOptimizations =
|
const kRequestIgnoreBatteryOptimizations =
|
||||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||||
@@ -452,12 +453,6 @@ class AndroidChannel {
|
|||||||
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
|
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
|
||||||
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
|
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
|
||||||
static final kSyncAppDirConfigPath = "sync_app_dir";
|
static final kSyncAppDirConfigPath = "sync_app_dir";
|
||||||
static final kPickImportFiles = "pick_import_files";
|
|
||||||
static final kImportFile = "import_file";
|
|
||||||
static final kExportFile = "export_file";
|
|
||||||
static final kPickImportDirectory = "pick_import_directory";
|
|
||||||
static final kImportDirectory = "import_directory";
|
|
||||||
static final kExportFiles = "export_files";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:flutter_hbb/desktop/widgets/update_progress.dart';
|
|||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -24,6 +25,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:window_size/window_size.dart' as window_size;
|
import 'package:window_size/window_size.dart' as window_size;
|
||||||
import '../widgets/button.dart';
|
import '../widgets/button.dart';
|
||||||
|
import '../widgets/texture_render_probe.dart';
|
||||||
|
|
||||||
class DesktopHomePage extends StatefulWidget {
|
class DesktopHomePage extends StatefulWidget {
|
||||||
const DesktopHomePage({Key? key}) : super(key: key);
|
const DesktopHomePage({Key? key}) : super(key: key);
|
||||||
@@ -59,15 +61,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
final isIncomingOnly = bind.isIncomingOnly();
|
final isIncomingOnly = bind.isIncomingOnly();
|
||||||
return _buildBlock(
|
return Stack(
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
buildLeftPane(context),
|
_buildBlock(
|
||||||
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
child: Row(
|
||||||
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildLeftPane(context),
|
||||||
|
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
||||||
|
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
const Positioned(left: 0, top: 0, child: TextureRenderProbe()),
|
||||||
],
|
],
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBlock({required Widget child}) {
|
Widget _buildBlock({required Widget child}) {
|
||||||
@@ -110,6 +117,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
buildPluginEntry(),
|
||||||
];
|
];
|
||||||
if (isIncomingOnly) {
|
if (isIncomingOnly) {
|
||||||
children.addAll([
|
children.addAll([
|
||||||
@@ -780,6 +788,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
windowOnTop(null);
|
windowOnTop(null);
|
||||||
} else if (call.method == kWindowRefreshCurrentUser) {
|
} else if (call.method == kWindowRefreshCurrentUser) {
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
|
} else if (call.method == kWindowGetWindowInfo) {
|
||||||
|
final screen = (await window_size.getWindowInfo()).screen;
|
||||||
|
if (screen == null) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return jsonEncode(screenToMap(screen));
|
||||||
|
}
|
||||||
} else if (call.method == kWindowGetScreenList) {
|
} else if (call.method == kWindowGetScreenList) {
|
||||||
return jsonEncode(
|
return jsonEncode(
|
||||||
(await window_size.getScreenList()).map(screenToMap).toList());
|
(await window_size.getScreenList()).map(screenToMap).toList());
|
||||||
@@ -881,6 +896,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
shouldBeBlocked(_block, canBeBlocked);
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildPluginEntry() {
|
||||||
|
final entries = PluginUiManager.instance.entries.entries;
|
||||||
|
return Offstage(
|
||||||
|
offstage: entries.isEmpty,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...entries.map((entry) {
|
||||||
|
return entry.value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
|||||||
import 'package:flutter_hbb/models/printer_model.dart';
|
import 'package:flutter_hbb/models/printer_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/manager.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -53,6 +55,7 @@ enum SettingsTabKey {
|
|||||||
safety,
|
safety,
|
||||||
network,
|
network,
|
||||||
display,
|
display,
|
||||||
|
plugin,
|
||||||
account,
|
account,
|
||||||
printer,
|
printer,
|
||||||
about,
|
about,
|
||||||
@@ -61,8 +64,7 @@ enum SettingsTabKey {
|
|||||||
class DesktopSettingPage extends StatefulWidget {
|
class DesktopSettingPage extends StatefulWidget {
|
||||||
final SettingsTabKey initialTabkey;
|
final SettingsTabKey initialTabkey;
|
||||||
static final List<SettingsTabKey> tabKeys = [
|
static final List<SettingsTabKey> tabKeys = [
|
||||||
if (bind.mainGetBuildinOption(key: kOptionHideGeneralSetting) != 'Y')
|
SettingsTabKey.general,
|
||||||
SettingsTabKey.general,
|
|
||||||
if (!isWeb &&
|
if (!isWeb &&
|
||||||
!bind.isOutgoingOnly() &&
|
!bind.isOutgoingOnly() &&
|
||||||
!bind.isDisableSettings() &&
|
!bind.isDisableSettings() &&
|
||||||
@@ -72,9 +74,10 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
||||||
SettingsTabKey.network,
|
SettingsTabKey.network,
|
||||||
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
||||||
|
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||||
|
SettingsTabKey.plugin,
|
||||||
if (!bind.isDisableAccount()) SettingsTabKey.account,
|
if (!bind.isDisableAccount()) SettingsTabKey.account,
|
||||||
if (isWindows &&
|
if (isWindows &&
|
||||||
!bind.isDisableSettings() &&
|
|
||||||
bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
|
bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
|
||||||
SettingsTabKey.printer,
|
SettingsTabKey.printer,
|
||||||
SettingsTabKey.about,
|
SettingsTabKey.about,
|
||||||
@@ -92,8 +95,7 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag) &&
|
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
||||||
Get.isRegistered<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag)) {
|
|
||||||
DesktopTabPage.onAddSetting(initialPage: page);
|
DesktopTabPage.onAddSetting(initialPage: page);
|
||||||
PageController controller =
|
PageController controller =
|
||||||
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
||||||
@@ -161,23 +163,17 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final blocked = await canBeBlocked();
|
_canBeBlocked.value = await canBeBlocked();
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_canBeBlocked.value = blocked;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_videoConnTimer?.cancel();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
Get.delete<PageController>(tag: _kSettingPageControllerTag);
|
|
||||||
Get.delete<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
|
|
||||||
// Get.delete does not dispose a plain ChangeNotifier.
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
Get.delete<PageController>(tag: _kSettingPageControllerTag);
|
||||||
|
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_videoConnTimer?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<_TabInfo> _settingTabs() {
|
List<_TabInfo> _settingTabs() {
|
||||||
@@ -200,6 +196,10 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
settingTabs.add(_TabInfo(tab, 'Display',
|
settingTabs.add(_TabInfo(tab, 'Display',
|
||||||
Icons.desktop_windows_outlined, Icons.desktop_windows));
|
Icons.desktop_windows_outlined, Icons.desktop_windows));
|
||||||
break;
|
break;
|
||||||
|
case SettingsTabKey.plugin:
|
||||||
|
settingTabs.add(_TabInfo(
|
||||||
|
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
|
||||||
|
break;
|
||||||
case SettingsTabKey.account:
|
case SettingsTabKey.account:
|
||||||
settingTabs.add(
|
settingTabs.add(
|
||||||
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
|
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
|
||||||
@@ -233,6 +233,9 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
case SettingsTabKey.display:
|
case SettingsTabKey.display:
|
||||||
children.add(const _Display());
|
children.add(const _Display());
|
||||||
break;
|
break;
|
||||||
|
case SettingsTabKey.plugin:
|
||||||
|
children.add(const _Plugin());
|
||||||
|
break;
|
||||||
case SettingsTabKey.account:
|
case SettingsTabKey.account:
|
||||||
children.add(const _Account());
|
children.add(const _Account());
|
||||||
break;
|
break;
|
||||||
@@ -575,15 +578,6 @@ class _GeneralState extends State<_General> {
|
|||||||
kOptionEnableIpv6Punch,
|
kOptionEnableIpv6Punch,
|
||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
Tooltip(
|
|
||||||
message: translate('sync-clipboard-between-sessions-tip'),
|
|
||||||
child: _OptionCheckBox(
|
|
||||||
context,
|
|
||||||
'Sync clipboard between sessions',
|
|
||||||
kOptionAllowSyncClipboardBetweenSessions,
|
|
||||||
isServer: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -597,6 +591,10 @@ class _GeneralState extends State<_General> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||||
|
children.add(_OptionCheckBox(
|
||||||
|
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||||
|
}
|
||||||
if (!bind.isDisableAccount()) {
|
if (!bind.isDisableAccount()) {
|
||||||
children.add(_OptionCheckBox(
|
children.add(_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
@@ -2257,6 +2255,51 @@ class _CheckboxState extends State<_Checkbox> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Plugin extends StatefulWidget {
|
||||||
|
const _Plugin({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_Plugin> createState() => _PluginState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PluginState extends State<_Plugin> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bind.pluginListReload();
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: pluginManager,
|
||||||
|
child: Consumer<PluginManager>(builder: (context, model, child) {
|
||||||
|
return ListView(
|
||||||
|
controller: scrollController,
|
||||||
|
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
|
||||||
|
).marginOnly(bottom: _kListViewBottomMargin);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget pluginCard(PluginInfo plugin) {
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: plugin,
|
||||||
|
child: Consumer<PluginInfo>(
|
||||||
|
builder: (context, model, child) => DesktopSettingsCard(plugin: model),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget accountAction() {
|
||||||
|
return Obx(() => _Button(
|
||||||
|
gFFI.userModel.userName.value.isEmpty
|
||||||
|
? 'Login'
|
||||||
|
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
|
||||||
|
() => {
|
||||||
|
gFFI.userModel.userName.value.isEmpty
|
||||||
|
? loginDialog()
|
||||||
|
: logOutConfirmDialog()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _Printer extends StatefulWidget {
|
class _Printer extends StatefulWidget {
|
||||||
const _Printer({super.key});
|
const _Printer({super.key});
|
||||||
|
|
||||||
|
|||||||
@@ -278,39 +278,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
item.state != JobState.inProgress,
|
item.state != JobState.inProgress,
|
||||||
child: LinearPercentIndicator(
|
child: LinearPercentIndicator(
|
||||||
animateFromLastPercent: true,
|
animateFromLastPercent: true,
|
||||||
center: SizedBox.expand(
|
center: Text(item.percentText),
|
||||||
child: ShaderMask(
|
|
||||||
blendMode: BlendMode.srcATop,
|
|
||||||
shaderCallback: (bounds) =>
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Colors.white,
|
|
||||||
Colors.transparent,
|
|
||||||
],
|
|
||||||
stops: [item.percent, item.percent],
|
|
||||||
).createShader(bounds),
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
text: item.percentText,
|
|
||||||
children: [
|
|
||||||
if (item.recvJobRes)
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
' ${readableFileSize(item.speed)}/s',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
color: MyTheme.darkGray,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
barRadius: Radius.circular(15),
|
barRadius: Radius.circular(15),
|
||||||
percent: item.percent,
|
percent: item.percent,
|
||||||
progressColor: MyTheme.accent,
|
progressColor: MyTheme.accent,
|
||||||
@@ -1126,7 +1094,6 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
return element.name.contains(_searchText.value);
|
return element.name.contains(_searchText.value);
|
||||||
}).toList(growable: false)
|
}).toList(growable: false)
|
||||||
: entries;
|
: entries;
|
||||||
// Keep rows lazy so large directories only build visible list items.
|
|
||||||
final rows = filteredEntries.map((entry) {
|
final rows = filteredEntries.map((entry) {
|
||||||
final sizeStr =
|
final sizeStr =
|
||||||
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
||||||
@@ -1309,7 +1276,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
],
|
],
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
});
|
}).toList(growable: false);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -1325,7 +1292,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
itemExtent: kDesktopFileTransferRowHeight,
|
itemExtent: kDesktopFileTransferRowHeight,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return rows.elementAt(index);
|
return rows[index];
|
||||||
},
|
},
|
||||||
itemCount: rows.length,
|
itemCount: rows.length,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
import '../widgets/raster_stall_monitor.dart';
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
import 'macos_full_screen_focus_recovery.dart';
|
import 'macos_full_screen_focus_recovery.dart';
|
||||||
|
|
||||||
@@ -156,6 +157,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
||||||
}
|
}
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
|
RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
_ffi.canvasModel.activateLocalCursor();
|
_ffi.canvasModel.activateLocalCursor();
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
@@ -182,6 +184,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
WakelockManager.enable(_uniqueKey);
|
WakelockManager.enable(_uniqueKey);
|
||||||
|
|
||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -273,11 +276,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
tabState.tabs[selected].key == widget.id;
|
tabState.tabs[selected].key == widget.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every Windows requestFocus() must pass this, or a blocking dialog or an
|
|
||||||
// inactive tab could hand remote input to this page.
|
|
||||||
bool get _windowsCanFocusRemoteInput =>
|
|
||||||
_isSelectedTab && _blockableOverlayState.middleBlocked.isFalse;
|
|
||||||
|
|
||||||
bool get _isMacOSKeyboardContextActive {
|
bool get _isMacOSKeyboardContextActive {
|
||||||
return stateGlobal.isFocused.value && !_isWindowBlur && _isSelectedTab;
|
return stateGlobal.isFocused.value && !_isWindowBlur && _isSelectedTab;
|
||||||
}
|
}
|
||||||
@@ -518,15 +516,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_queueMacOSKeyboardAfterFullScreen(allowHiddenLifecycle: true);
|
_queueMacOSKeyboardAfterFullScreen(allowHiddenLifecycle: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refocus without PointerEnter: the cursor already hovers the image when
|
|
||||||
// focus returns (Alt+Tab, taskbar), so enterView() never fires again.
|
|
||||||
if (isWindows &&
|
|
||||||
_cursorOverImage.value &&
|
|
||||||
_windowsCanFocusRemoteInput &&
|
|
||||||
!_rawKeyFocusNode.hasFocus) {
|
|
||||||
_rawKeyFocusNode.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore relative mouse mode constraints when window regains focus.
|
// Restore relative mouse mode constraints when window regains focus.
|
||||||
if (_ffi.inputModel.relativeMouseMode.value) {
|
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
@@ -537,7 +526,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_cursorOverImage.value = true;
|
_cursorOverImage.value = true;
|
||||||
_macOSLocalFocusLost = false;
|
_macOSLocalFocusLost = false;
|
||||||
}
|
}
|
||||||
} else if (!isWindows || _windowsCanFocusRemoteInput) {
|
} else {
|
||||||
_rawKeyFocusNode.requestFocus();
|
_rawKeyFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
_ffi.inputModel.onWindowFocus();
|
_ffi.inputModel.onWindowFocus();
|
||||||
@@ -660,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// Clear callback reference to prevent memory leaks and stale references
|
// Clear callback reference to prevent memory leaks and stale references
|
||||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
_ffi.textureModel.onRemotePageDispose();
|
||||||
if (closeSession && !isMacOS) {
|
if (closeSession && !isMacOS) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
||||||
@@ -849,16 +838,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_macOSLocalFocusLost = false;
|
_macOSLocalFocusLost = false;
|
||||||
stateGlobal.getInputSource(force: true);
|
stateGlobal.getInputSource(force: true);
|
||||||
_syncMacOSKeyboardGrab(reassert: true, allowInactiveLifecycle: true);
|
_syncMacOSKeyboardGrab(reassert: true, allowInactiveLifecycle: true);
|
||||||
} else if (isWindows) {
|
} else if (!isWindows) {
|
||||||
// Blur unfocuses this node and nothing restores it, so the keyboard stayed
|
|
||||||
// dead until a click. Focus only while the window is really active, or a
|
|
||||||
// background window would grab system keys. onFocusChange does enterOrLeave.
|
|
||||||
if (!_isWindowBlur &&
|
|
||||||
_windowsCanFocusRemoteInput &&
|
|
||||||
!_rawKeyFocusNode.hasFocus) {
|
|
||||||
_rawKeyFocusNode.requestFocus();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!_rawKeyFocusNode.hasFocus) {
|
if (!_rawKeyFocusNode.hasFocus) {
|
||||||
_rawKeyFocusNode.requestFocus();
|
_rawKeyFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
@@ -1424,3 +1404,4 @@ class CursorPaint extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ import '../../models/file_model.dart';
|
|||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../models/server_model.dart';
|
import '../../models/server_model.dart';
|
||||||
|
|
||||||
/// Set only by this window's own close control, and only once the user has confirmed. Any other
|
|
||||||
/// way the window can go - a session logout closing every window, the window manager, a native
|
|
||||||
/// title-bar button this app does not draw - leaves it false, which is the honest answer:
|
|
||||||
/// nothing in that close says who asked for it. It lives at file scope because the control that
|
|
||||||
/// sets it (`ConnectionManagerState`) and the handler that reads it (`_DesktopServerPageState`)
|
|
||||||
/// are different widgets.
|
|
||||||
bool _cmClosedByOperator = false;
|
|
||||||
|
|
||||||
class DesktopServerPage extends StatefulWidget {
|
class DesktopServerPage extends StatefulWidget {
|
||||||
const DesktopServerPage({Key? key}) : super(key: key);
|
const DesktopServerPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -63,10 +55,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowClose() {
|
void onWindowClose() {
|
||||||
// Other platforms keep the old behaviour exactly: the ambiguity this guards against is a
|
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
|
||||||
// Linux session logout, which closes every window in the session.
|
|
||||||
final byOperator = _cmClosedByOperator || !isLinux;
|
|
||||||
Future.wait([gFFI.serverModel.closeAll(byOperator: byOperator), gFFI.close()]).then((_) {
|
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
RdPlatformChannel.instance.terminate();
|
RdPlatformChannel.instance.terminate();
|
||||||
} else {
|
} else {
|
||||||
@@ -338,7 +327,6 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
var tabController = gFFI.serverModel.tabController;
|
var tabController = gFFI.serverModel.tabController;
|
||||||
final connLength = tabController.length;
|
final connLength = tabController.length;
|
||||||
if (connLength <= 1) {
|
if (connLength <= 1) {
|
||||||
_cmClosedByOperator = true;
|
|
||||||
windowManager.close();
|
windowManager.close();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -350,9 +338,6 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
}
|
}
|
||||||
if (res) {
|
if (res) {
|
||||||
// After the dialog, never before it: an external close while it is open must not
|
|
||||||
// inherit an intent the user had not expressed yet.
|
|
||||||
_cmClosedByOperator = true;
|
|
||||||
windowManager.close();
|
windowManager.close();
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter_hbb/common.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
import 'terminal_connection_manager.dart';
|
import 'terminal_connection_manager.dart';
|
||||||
|
|
||||||
class TerminalPage extends StatefulWidget {
|
class TerminalPage extends StatefulWidget {
|
||||||
@@ -197,7 +197,7 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final heightPx = constraints.maxHeight;
|
final heightPx = constraints.maxHeight;
|
||||||
return TerminalMouseInteraction(
|
return TerminalView(
|
||||||
_terminalModel.terminal,
|
_terminalModel.terminal,
|
||||||
controller: _terminalModel.terminalController,
|
controller: _terminalModel.terminalController,
|
||||||
focusNode: _terminalFocusNode,
|
focusNode: _terminalFocusNode,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
import '../widgets/raster_stall_monitor.dart';
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/native/custom_cursor.dart'
|
import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||||
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
|
RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -127,6 +129,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
WakelockManager.enable(_uniqueKey);
|
WakelockManager.enable(_uniqueKey);
|
||||||
|
|
||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
@@ -221,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
||||||
_ffi.textureModel.onViewCameraPageDispose(closeSession);
|
_ffi.textureModel.onViewCameraPageDispose();
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
|
|||||||
52
flutter/lib/desktop/widgets/raster_stall_monitor.dart
Normal file
52
flutter/lib/desktop/widgets/raster_stall_monitor.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
|
||||||
|
/// Records a hung raster thread (frames continuously scheduled but no frame
|
||||||
|
/// timings delivered for 30s) in `texture-render-health`; a hang cannot be
|
||||||
|
/// rescued in-process, so the next launch defaults texture rendering off.
|
||||||
|
class RasterStallMonitor {
|
||||||
|
static bool _started = false;
|
||||||
|
static bool _reported = false;
|
||||||
|
static DateTime? _lastTimings;
|
||||||
|
static DateTime _lastQuiet = DateTime.now();
|
||||||
|
|
||||||
|
static void start() {
|
||||||
|
if (_started || isWeb) return;
|
||||||
|
_started = true;
|
||||||
|
SchedulerBinding.instance.addTimingsCallback((_) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
|
});
|
||||||
|
Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
if (_reported) return;
|
||||||
|
final now = DateTime.now();
|
||||||
|
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
||||||
|
// Minimized/inactive (incl. screen lock) or idle (nothing scheduled):
|
||||||
|
// no timings is legitimate, keep moving the quiet anchor forward.
|
||||||
|
if (stateGlobal.isMinimized ||
|
||||||
|
(lifecycle != null && lifecycle != AppLifecycleState.resumed) ||
|
||||||
|
!SchedulerBinding.instance.hasScheduledFrame) {
|
||||||
|
_lastQuiet = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ref = _lastQuiet;
|
||||||
|
final lastTimings = _lastTimings;
|
||||||
|
if (lastTimings != null && lastTimings.isAfter(ref)) {
|
||||||
|
ref = lastTimings;
|
||||||
|
}
|
||||||
|
if (now.difference(ref) > const Duration(seconds: 30)) {
|
||||||
|
_reported = true;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
|
||||||
|
debugPrint(
|
||||||
|
'raster thread stall detected, texture rendering disabled for next launch');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
|||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/common.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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';
|
||||||
@@ -1333,12 +1336,6 @@ class ScreenAdjustor {
|
|||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final VoidCallback cbExitFullscreen;
|
final VoidCallback cbExitFullscreen;
|
||||||
window_size.Screen? _screen;
|
window_size.Screen? _screen;
|
||||||
Size? _waylandMaximizedWorkAreaSize;
|
|
||||||
Rect? _waylandWorkAreaScreenFrame;
|
|
||||||
double? _waylandWorkAreaScaleFactor;
|
|
||||||
Rect? _x11WorkArea;
|
|
||||||
Rect? _x11WorkAreaScreenFrame;
|
|
||||||
double? _x11WorkAreaScaleFactor;
|
|
||||||
|
|
||||||
ScreenAdjustor({
|
ScreenAdjustor({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -1349,18 +1346,9 @@ class ScreenAdjustor {
|
|||||||
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
|
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
|
||||||
int get windowId => stateGlobal.windowId;
|
int get windowId => stateGlobal.windowId;
|
||||||
|
|
||||||
Future<bool?> isWindowMaximized() async {
|
|
||||||
try {
|
|
||||||
return await WindowController.fromWindowId(windowId).isMaximized();
|
|
||||||
} catch (_) {
|
|
||||||
// The delayed resolution callback may run after the window is disposed.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustWindow(BuildContext context) {
|
adjustWindow(BuildContext context) {
|
||||||
return futureBuilder(
|
return futureBuilder(
|
||||||
future: isWindowCanBeAdjusted(context),
|
future: isWindowCanBeAdjusted(),
|
||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
final visible = data as bool;
|
final visible = data as bool;
|
||||||
if (!visible) return Offstage();
|
if (!visible) return Offstage();
|
||||||
@@ -1376,201 +1364,36 @@ class ScreenAdjustor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux screen and work-area coordinates can use different units or become
|
doAdjustWindow(BuildContext context) async {
|
||||||
// unreliable across Wayland/X11 state changes, so normalize reported frames
|
await updateScreen();
|
||||||
// and cache usable work-area measurements before sizing the window.
|
if (_screen != null) {
|
||||||
|
cbExitFullscreen();
|
||||||
Future<void> _updateLinuxWorkAreaCache({
|
double scale = _screen!.scaleFactor;
|
||||||
required window_size.Screen screen,
|
final wndRect = await WindowController.fromWindowId(windowId).getFrame();
|
||||||
required Rect wndRect,
|
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
|
||||||
required bool isWayland,
|
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
|
||||||
required bool isX11,
|
|
||||||
required bool forMenu,
|
|
||||||
}) async {
|
|
||||||
if (isWayland &&
|
|
||||||
(_waylandWorkAreaScreenFrame != screen.frame ||
|
|
||||||
_waylandWorkAreaScaleFactor != screen.scaleFactor)) {
|
|
||||||
_waylandMaximizedWorkAreaSize = null;
|
|
||||||
_waylandWorkAreaScreenFrame = screen.frame;
|
|
||||||
_waylandWorkAreaScaleFactor = screen.scaleFactor;
|
|
||||||
}
|
|
||||||
if (isWayland &&
|
|
||||||
forMenu &&
|
|
||||||
!isFullscreen &&
|
|
||||||
await isWindowMaximized() == true) {
|
|
||||||
_waylandMaximizedWorkAreaSize = wndRect.size;
|
|
||||||
}
|
|
||||||
if (isX11 &&
|
|
||||||
(_x11WorkAreaScreenFrame != screen.frame ||
|
|
||||||
_x11WorkAreaScaleFactor != screen.scaleFactor)) {
|
|
||||||
_x11WorkArea = null;
|
|
||||||
_x11WorkAreaScreenFrame = screen.frame;
|
|
||||||
_x11WorkAreaScaleFactor = screen.scaleFactor;
|
|
||||||
}
|
|
||||||
if (isX11 && forMenu && !isFullscreen) {
|
|
||||||
_x11WorkArea = screen.visibleFrame;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Rect?> _getEffectiveScreenFrame({
|
|
||||||
required window_size.Screen screen,
|
|
||||||
required bool isWayland,
|
|
||||||
required bool isX11,
|
|
||||||
required bool forMenu,
|
|
||||||
}) async {
|
|
||||||
Rect frameRect = screen.visibleFrame;
|
|
||||||
if (isMacOS && forMenu && isFullscreen) {
|
|
||||||
List<double>? workArea;
|
|
||||||
try {
|
|
||||||
workArea = await kMacOSPermChannel
|
|
||||||
.invokeListMethod<double>('getMacOSWorkAreaSize');
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (workArea == null || workArea.length != 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
frameRect = Rect.fromLTWH(
|
|
||||||
frameRect.left,
|
|
||||||
frameRect.top,
|
|
||||||
workArea[0] < frameRect.width ? workArea[0] : frameRect.width,
|
|
||||||
workArea[1] < frameRect.height ? workArea[1] : frameRect.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final x11WorkArea = _x11WorkArea;
|
|
||||||
if (isX11 &&
|
|
||||||
forMenu &&
|
|
||||||
isFullscreen &&
|
|
||||||
x11WorkArea != null &&
|
|
||||||
(x11WorkArea.width < frameRect.width ||
|
|
||||||
x11WorkArea.height < frameRect.height)) {
|
|
||||||
frameRect = x11WorkArea;
|
|
||||||
}
|
|
||||||
final screenScale = screen.scaleFactor;
|
|
||||||
if (isWayland && screenScale > 1.01) {
|
|
||||||
String monitorLayoutMode;
|
|
||||||
try {
|
|
||||||
monitorLayoutMode =
|
|
||||||
await bind.mainGetCommon(key: 'gnome-monitor-layout-mode');
|
|
||||||
} catch (_) {
|
|
||||||
monitorLayoutMode = '';
|
|
||||||
}
|
|
||||||
if (monitorLayoutMode == 'physical') {
|
|
||||||
frameRect = Rect.fromLTRB(
|
|
||||||
frameRect.left / screenScale,
|
|
||||||
frameRect.top / screenScale,
|
|
||||||
frameRect.right / screenScale,
|
|
||||||
frameRect.bottom / screenScale,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return frameRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Rect?> _getAdjustedWindowFrame(Size mediaSize,
|
|
||||||
{bool forMenu = false}) async {
|
|
||||||
final screen = _screen;
|
|
||||||
if (screen != null) {
|
|
||||||
// Windows window frames use physical pixels while Flutter view sizes are
|
|
||||||
// logical. macOS and Linux window frames use the same units as Flutter.
|
|
||||||
double scale = isWindows ? screen.scaleFactor : 1.0;
|
|
||||||
final Rect wndRect;
|
|
||||||
try {
|
|
||||||
wndRect = await WindowController.fromWindowId(windowId).getFrame();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Failed to get frame of window $windowId, it may be hidden");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// On Windows, wndRect is GetWindowRect while mediaSize is GetClientRect.
|
|
||||||
// https://stackoverflow.com/a/7561083
|
// https://stackoverflow.com/a/7561083
|
||||||
double magicWidth =
|
double magicWidth =
|
||||||
wndRect.right - wndRect.left - mediaSize.width * scale;
|
wndRect.right - wndRect.left - mediaSize.width * scale;
|
||||||
double magicHeight =
|
double magicHeight =
|
||||||
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
||||||
final canvasModel = ffi.canvasModel;
|
final canvasModel = ffi.canvasModel;
|
||||||
// canvasModel.scale is the rendered scale and already applies kIgnoreDpi.
|
|
||||||
// Use it instead of the remote source resolution.
|
|
||||||
final isWayland = isLinux && bind.mainCurrentIsWayland();
|
|
||||||
final isX11 = isLinux && !isWayland;
|
|
||||||
await _updateLinuxWorkAreaCache(
|
|
||||||
screen: screen,
|
|
||||||
wndRect: wndRect,
|
|
||||||
isWayland: isWayland,
|
|
||||||
isX11: isX11,
|
|
||||||
forMenu: forMenu,
|
|
||||||
);
|
|
||||||
if (isWindows && forMenu && isFullscreen) {
|
|
||||||
// desktop_multi_window's hidden title bar keeps 8 physical pixels on
|
|
||||||
// each horizontal edge and at the bottom, plus up to 1px at the top.
|
|
||||||
// Fullscreen removes these in WM_NCCALCSIZE, so predict the restored
|
|
||||||
// frame's worst-case padding when deciding whether to show the menu.
|
|
||||||
magicWidth = 16.0;
|
|
||||||
magicHeight = 9.0;
|
|
||||||
}
|
|
||||||
double horizontalEdges;
|
|
||||||
double verticalEdges;
|
|
||||||
if (forMenu && (isLinux || ((isMacOS || isWindows) && isFullscreen))) {
|
|
||||||
// Linux Adjust Window unmaximizes; macOS and Windows exit fullscreen
|
|
||||||
// before resizing. Predict the restored normal-window edges when
|
|
||||||
// deciding whether to show the menu item.
|
|
||||||
final resizePadding = isLinux && !kUseCompatibleUiMode
|
|
||||||
? kDragToResizeAreaPaddingSize
|
|
||||||
: 0.0;
|
|
||||||
final windowEdge = kWindowBorderWidth + resizePadding;
|
|
||||||
horizontalEdges = windowEdge * 2;
|
|
||||||
verticalEdges = kDesktopRemoteTabBarHeight + windowEdge * 2;
|
|
||||||
} else {
|
|
||||||
horizontalEdges = CanvasModel.leftToEdge + CanvasModel.rightToEdge;
|
|
||||||
verticalEdges = CanvasModel.topToEdge + CanvasModel.bottomToEdge;
|
|
||||||
}
|
|
||||||
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
|
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
|
||||||
horizontalEdges) *
|
CanvasModel.leftToEdge +
|
||||||
|
CanvasModel.rightToEdge) *
|
||||||
scale +
|
scale +
|
||||||
magicWidth;
|
magicWidth;
|
||||||
final height =
|
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
|
||||||
(canvasModel.getDisplayHeight() * canvasModel.scale + verticalEdges) *
|
CanvasModel.topToEdge +
|
||||||
scale +
|
CanvasModel.bottomToEdge) *
|
||||||
magicHeight;
|
scale +
|
||||||
|
magicHeight;
|
||||||
double left = wndRect.left + (wndRect.width - width) / 2;
|
double left = wndRect.left + (wndRect.width - width) / 2;
|
||||||
double top = wndRect.top + (wndRect.height - height) / 2;
|
double top = wndRect.top + (wndRect.height - height) / 2;
|
||||||
|
|
||||||
final frameRect = await _getEffectiveScreenFrame(
|
Rect frameRect = _screen!.frame;
|
||||||
screen: screen,
|
if (!isFullscreen) {
|
||||||
isWayland: isWayland,
|
frameRect = _screen!.visibleFrame;
|
||||||
isX11: isX11,
|
|
||||||
forMenu: forMenu,
|
|
||||||
);
|
|
||||||
if (frameRect == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var availableSize = frameRect.size;
|
|
||||||
if (isWayland && forMenu && _waylandMaximizedWorkAreaSize != null) {
|
|
||||||
final cachedSize = _waylandMaximizedWorkAreaSize!;
|
|
||||||
availableSize = Size(
|
|
||||||
cachedSize.width < availableSize.width
|
|
||||||
? cachedSize.width
|
|
||||||
: availableSize.width,
|
|
||||||
cachedSize.height < availableSize.height
|
|
||||||
? cachedSize.height
|
|
||||||
: availableSize.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// A window frame cannot be smaller than its client area. Tolerate small
|
|
||||||
// floating-point differences; larger negative values mean the native
|
|
||||||
// frame and Flutter view metrics are not synchronized.
|
|
||||||
if (magicWidth < -0.1 || magicHeight < -0.1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Reject implausibly small targets to avoid hiding the window.
|
|
||||||
if (width < 300 || height < 300) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// The remote size may change after the menu is built. Reject targets
|
|
||||||
// that exceed the available area.
|
|
||||||
final exceedsScreen =
|
|
||||||
width > availableSize.width || height > availableSize.height;
|
|
||||||
if (exceedsScreen) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (left < frameRect.left) {
|
if (left < frameRect.left) {
|
||||||
left = frameRect.left;
|
left = frameRect.left;
|
||||||
@@ -1584,101 +1407,69 @@ class ScreenAdjustor {
|
|||||||
if ((top + height) > frameRect.bottom) {
|
if ((top + height) > frameRect.bottom) {
|
||||||
top = frameRect.bottom - height;
|
top = frameRect.bottom - height;
|
||||||
}
|
}
|
||||||
return Rect.fromLTWH(left, top, width, height);
|
await WindowController.fromWindowId(windowId)
|
||||||
}
|
.setFrame(Rect.fromLTWH(left, top, width, height));
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
doAdjustWindow([BuildContext? context]) async {
|
|
||||||
// A resolution change is adjusted after a delay, when the menu context may
|
|
||||||
// already be disposed. Each desktop_multi_window window has its own engine,
|
|
||||||
// so that engine's first view is the current window.
|
|
||||||
final views = WidgetsBinding.instance.platformDispatcher.views;
|
|
||||||
if (context == null && views.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final view = context != null ? View.of(context) : views.first;
|
|
||||||
await updateScreen();
|
|
||||||
if (_screen != null) {
|
|
||||||
final wc = WindowController.fromWindowId(windowId);
|
|
||||||
final wasFullscreen = isFullscreen;
|
|
||||||
cbExitFullscreen();
|
|
||||||
if (wasFullscreen) {
|
|
||||||
// Wait for the native fullscreen exit to update the window frame.
|
|
||||||
await Future.delayed(Duration(milliseconds: 700));
|
|
||||||
await updateScreen();
|
|
||||||
}
|
|
||||||
if (isLinux) {
|
|
||||||
final isMaximized = await isWindowMaximized();
|
|
||||||
if (isMaximized == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isMaximized == true) {
|
|
||||||
// setFrame may be ignored while the native window is maximized.
|
|
||||||
try {
|
|
||||||
await wc.unmaximize();
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stateGlobal.setMaximized(false);
|
|
||||||
// Wait for the window manager and Flutter view metrics to reflect
|
|
||||||
// the restored window before calculating and setting its frame.
|
|
||||||
await Future.delayed(Duration(milliseconds: 300));
|
|
||||||
await updateScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final mediaSize = MediaQueryData.fromView(view).size;
|
|
||||||
final frame = await _getAdjustedWindowFrame(mediaSize);
|
|
||||||
if (frame == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await wc.setFrame(frame);
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stateGlobal.setMaximized(false);
|
stateGlobal.setMaximized(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScreen() async {
|
updateScreen() async {
|
||||||
_screen = await _getCurrentScreen();
|
final String info =
|
||||||
}
|
isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
|
||||||
|
if (info.isEmpty) {
|
||||||
Future<window_size.Screen?> _getCurrentScreen() async {
|
_screen = null;
|
||||||
try {
|
} else {
|
||||||
return (await window_size.getWindowInfo()).screen;
|
final screenMap = jsonDecode(info);
|
||||||
} catch (e) {
|
_screen = window_size.Screen(
|
||||||
debugPrint('Failed to get current window screen: $e');
|
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
|
||||||
return null;
|
screenMap['frame']['r'], screenMap['frame']['b']),
|
||||||
|
Rect.fromLTRB(
|
||||||
|
screenMap['visibleFrame']['l'],
|
||||||
|
screenMap['visibleFrame']['t'],
|
||||||
|
screenMap['visibleFrame']['r'],
|
||||||
|
screenMap['visibleFrame']['b']),
|
||||||
|
screenMap['scaleFactor']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isWindowCanBeAdjusted([BuildContext? context]) async {
|
_getScreenInfoDesktop() async {
|
||||||
if (isWeb) {
|
final v = await rustDeskWinManager.call(
|
||||||
return false;
|
WindowType.Main, kWindowGetWindowInfo, '');
|
||||||
}
|
return v.result;
|
||||||
// Capture the view before awaiting because the menu context may be disposed.
|
}
|
||||||
final views = WidgetsBinding.instance.platformDispatcher.views;
|
|
||||||
if (context == null && views.isEmpty) {
|
Future<bool> isWindowCanBeAdjusted() async {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final view = context != null ? View.of(context) : views.first;
|
|
||||||
final mediaSize = MediaQueryData.fromView(view).size;
|
|
||||||
final viewStyle =
|
final viewStyle =
|
||||||
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
||||||
if (viewStyle != kRemoteViewStyleOriginal) {
|
if (viewStyle != kRemoteViewStyleOriginal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final remoteCount = RemoteCountState.find().value;
|
if (!isWeb) {
|
||||||
if (remoteCount != 1) {
|
final remoteCount = RemoteCountState.find().value;
|
||||||
return false;
|
if (remoteCount != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await updateScreen();
|
|
||||||
if (_screen == null) {
|
if (_screen == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return await _getAdjustedWindowFrame(mediaSize, forMenu: true) != null;
|
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
|
||||||
|
double selfWidth = _screen!.visibleFrame.width;
|
||||||
|
double selfHeight = _screen!.visibleFrame.height;
|
||||||
|
if (isFullscreen) {
|
||||||
|
selfWidth = _screen!.frame.width;
|
||||||
|
selfHeight = _screen!.frame.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
final canvasModel = ffi.canvasModel;
|
||||||
|
final displayWidth = canvasModel.getDisplayWidth();
|
||||||
|
final displayHeight = canvasModel.getDisplayHeight();
|
||||||
|
final requiredWidth =
|
||||||
|
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
|
||||||
|
final requiredHeight =
|
||||||
|
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
|
||||||
|
return selfWidth > (requiredWidth * scale) &&
|
||||||
|
selfHeight > (requiredHeight * scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1687,11 +1478,20 @@ class _DisplayMenu extends StatefulWidget {
|
|||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final ToolbarState state;
|
final ToolbarState state;
|
||||||
final Function(bool) setFullscreen;
|
final Function(bool) setFullscreen;
|
||||||
const _DisplayMenu(
|
final Widget pluginItem;
|
||||||
{required this.id,
|
_DisplayMenu(
|
||||||
|
{Key? key,
|
||||||
|
required this.id,
|
||||||
required this.ffi,
|
required this.ffi,
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.setFullscreen});
|
required this.setFullscreen})
|
||||||
|
: pluginItem = LocationItem.createLocationItem(
|
||||||
|
id,
|
||||||
|
ffi,
|
||||||
|
kLocationClientRemoteToolbarDisplay,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_DisplayMenu> createState() => _DisplayMenuState();
|
State<_DisplayMenu> createState() => _DisplayMenuState();
|
||||||
@@ -1729,6 +1529,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
_screenAdjustor.updateScreen();
|
||||||
menuChildrenGetter(_IconSubmenuButtonState state) {
|
menuChildrenGetter(_IconSubmenuButtonState state) {
|
||||||
final menuChildren = <Widget>[
|
final menuChildren = <Widget>[
|
||||||
_screenAdjustor.adjustWindow(context),
|
_screenAdjustor.adjustWindow(context),
|
||||||
@@ -1781,6 +1582,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ffi.connType == ConnType.defaultConn) {
|
||||||
|
menuChildren.add(widget.pluginItem);
|
||||||
|
}
|
||||||
return menuChildren;
|
return menuChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2292,19 +2096,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
|
|
||||||
Future<void> _getLocalResolutionWayland() async {
|
Future<void> _getLocalResolutionWayland() async {
|
||||||
if (!isWayland) return _getLocalResolution();
|
if (!isWayland) return _getLocalResolution();
|
||||||
try {
|
final window = await window_size.getWindowInfo();
|
||||||
final window = await window_size.getWindowInfo();
|
final screen = window.screen;
|
||||||
final screen = window.screen;
|
if (screen != null) {
|
||||||
if (screen != null) {
|
setState(() {
|
||||||
setState(() {
|
_localResolution = Resolution(
|
||||||
_localResolution = Resolution(
|
screen.frame.width.toInt(),
|
||||||
screen.frame.width.toInt(),
|
screen.frame.height.toInt(),
|
||||||
screen.frame.height.toInt(),
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to get local resolution on Wayland: $e');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2376,16 +2176,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (w == rect.width.toInt() && h == rect.height.toInt()) {
|
if (w == rect.width.toInt() && h == rect.height.toInt()) {
|
||||||
if (!await widget.screenAdjustor.isWindowCanBeAdjusted()) {
|
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
|
||||||
return;
|
widget.screenAdjustor.doAdjustWindow(context);
|
||||||
}
|
|
||||||
if (widget.screenAdjustor.isFullscreen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((await widget.screenAdjustor.isWindowMaximized()) == false) {
|
|
||||||
// This delayed callback can outlive the menu State, so its context
|
|
||||||
// is unsafe.
|
|
||||||
widget.screenAdjustor.doAdjustWindow();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.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/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -388,6 +389,7 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowMinimize() {
|
void onWindowMinimize() {
|
||||||
stateGlobal.setMinimized(true);
|
stateGlobal.setMinimized(true);
|
||||||
stateGlobal.setMaximized(false);
|
stateGlobal.setMaximized(false);
|
||||||
|
_updateSessionsRenderVisible(false);
|
||||||
super.onWindowMinimize();
|
super.onWindowMinimize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,6 +397,7 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowMaximize() {
|
void onWindowMaximize() {
|
||||||
stateGlobal.setMinimized(false);
|
stateGlobal.setMinimized(false);
|
||||||
_setMaximized(true);
|
_setMaximized(true);
|
||||||
|
_updateSessionsRenderVisible(true);
|
||||||
super.onWindowMaximize();
|
super.onWindowMaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,9 +405,34 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowUnmaximize() {
|
void onWindowUnmaximize() {
|
||||||
stateGlobal.setMinimized(false);
|
stateGlobal.setMinimized(false);
|
||||||
_setMaximized(false);
|
_setMaximized(false);
|
||||||
|
_updateSessionsRenderVisible(true);
|
||||||
super.onWindowUnmaximize();
|
super.onWindowUnmaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowRestore() {
|
||||||
|
// A plain restore (no maximize involved) must clear the minimized flag.
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
_updateSessionsRenderVisible(true);
|
||||||
|
super.onWindowRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hidden window composites nothing; pause the Rust-side texture watchdog
|
||||||
|
// for its sessions so it cannot record a false failure.
|
||||||
|
void _updateSessionsRenderVisible(bool visible) {
|
||||||
|
if (tabType != DesktopTabType.remoteScreen &&
|
||||||
|
tabType != DesktopTabType.viewCamera) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final tab in controller.state.value.tabs) {
|
||||||
|
try {
|
||||||
|
final ffi = Get.find<FFI>(tag: tab.key);
|
||||||
|
bind.sessionSetRenderVisible(
|
||||||
|
sessionId: ffi.sessionId, visible: visible);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_saveFrame({bool? flush}) async {
|
_saveFrame({bool? flush}) async {
|
||||||
try {
|
try {
|
||||||
if (tabType == DesktopTabType.main) {
|
if (tabType == DesktopTabType.main) {
|
||||||
|
|||||||
172
flutter/lib/desktop/widgets/texture_render_probe.dart
Normal file
172
flutter/lib/desktop/widgets/texture_render_probe.dart
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
|
||||||
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||||
|
|
||||||
|
/// Startup probe: renders one frame through a 1x1 external texture and
|
||||||
|
/// records in `texture-render-health` whether the engine consumed it, so a
|
||||||
|
/// broken environment is detected before the first session goes black.
|
||||||
|
class TextureRenderProbe extends StatefulWidget {
|
||||||
|
const TextureRenderProbe({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||||
|
static bool _ranThisLaunch = false;
|
||||||
|
final _renderer = TextureRgbaRenderer();
|
||||||
|
int _textureId = -1;
|
||||||
|
int _textureKey = -1;
|
||||||
|
int _ptr = 0;
|
||||||
|
Timer? _timer;
|
||||||
|
int _ticks = 0;
|
||||||
|
bool _sawTimings = false;
|
||||||
|
bool _wasEffectiveOn = false;
|
||||||
|
DateTime? _lastTimings;
|
||||||
|
DateTime? _firstPush;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
||||||
|
_ranThisLaunch = true;
|
||||||
|
if (bind.isIncomingOnly()) return;
|
||||||
|
// An old plugin without the consumed counter cannot be judged, and a
|
||||||
|
// recorded raster-stall means compositing a texture may hang this window.
|
||||||
|
if (!bind.mainTextureRenderProbeSupported()) return;
|
||||||
|
if (bind
|
||||||
|
.mainGetLocalOption(key: kOptionTextureRenderHealth)
|
||||||
|
.startsWith('failed-raster-stall')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_wasEffectiveOn = bind.mainGetUseTextureRender();
|
||||||
|
// Only probe after the window has really rendered a frame: a hidden
|
||||||
|
// window (silent/tray start) must not record a false failure.
|
||||||
|
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
||||||
|
Future.delayed(const Duration(seconds: 5), () {
|
||||||
|
if (!_sawTimings) {
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
_finish(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTimings(List<FrameTiming> timings) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
|
if (_sawTimings) return;
|
||||||
|
_sawTimings = true;
|
||||||
|
_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _start() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
_textureKey = bind.getNextTextureKey();
|
||||||
|
final id = await _renderer.createTexture(_textureKey);
|
||||||
|
if (!mounted || id == -1) {
|
||||||
|
_finish(!mounted ? null : false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ptr = await _renderer.getTexturePtr(_textureKey);
|
||||||
|
if (!mounted || _ptr <= 0) {
|
||||||
|
_finish(!mounted ? null : false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _textureId = id);
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
|
_ticks += 1;
|
||||||
|
_firstPush ??= DateTime.now();
|
||||||
|
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
||||||
|
final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
|
||||||
|
// "Consumed" advances inside the plugin callback, before the GL/Metal
|
||||||
|
// upload; only a frame timing after the push proves a completed frame.
|
||||||
|
final frameCompleted = consumed &&
|
||||||
|
_lastTimings != null &&
|
||||||
|
_lastTimings!.isAfter(_firstPush!);
|
||||||
|
if (frameCompleted) {
|
||||||
|
_finish(true);
|
||||||
|
} else if (_ticks >= 10) {
|
||||||
|
if (consumed) {
|
||||||
|
_finish(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only a window that is visibly compositing can prove a failure.
|
||||||
|
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
||||||
|
final active =
|
||||||
|
lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
||||||
|
final timingsFresh = _lastTimings != null &&
|
||||||
|
DateTime.now().difference(_lastTimings!) <
|
||||||
|
const Duration(milliseconds: 1500);
|
||||||
|
_finish(!stateGlobal.isMinimized && active && timingsFresh
|
||||||
|
? false
|
||||||
|
: null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finish(bool? ok) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
if (ok != null) {
|
||||||
|
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
||||||
|
if (ok) {
|
||||||
|
// This rgba probe disproves only the rgba black-texture class: gpu
|
||||||
|
// failures and raster stalls clear via the option toggle alone.
|
||||||
|
final clearable = old.isEmpty ||
|
||||||
|
old.startsWith('failed-probe') ||
|
||||||
|
old.startsWith('failed-watchdog-rgba');
|
||||||
|
if (clearable) {
|
||||||
|
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
||||||
|
}
|
||||||
|
} else if (!old.startsWith('failed')) {
|
||||||
|
debugPrint('texture render probe failed, disabling texture rendering');
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
||||||
|
if (_wasEffectiveOn) {
|
||||||
|
showToast(translate('texture-render-fallback-tip'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_textureKey != -1) {
|
||||||
|
_renderer.closeTexture(_textureKey);
|
||||||
|
_textureKey = -1;
|
||||||
|
}
|
||||||
|
_ptr = 0;
|
||||||
|
if (mounted && _textureId != -1) {
|
||||||
|
setState(() => _textureId = -1);
|
||||||
|
} else {
|
||||||
|
_textureId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
if (_textureKey != -1) {
|
||||||
|
_renderer.closeTexture(_textureKey);
|
||||||
|
_textureKey = -1;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_textureId == -1) return const SizedBox.shrink();
|
||||||
|
// Must actually composite for the engine to sample the texture; the
|
||||||
|
// pushed pixel is fully transparent.
|
||||||
|
return IgnorePointer(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ import 'mobile/pages/server_page.dart';
|
|||||||
import 'mobile/widgets/deploy_dialog.dart';
|
import 'mobile/widgets/deploy_dialog.dart';
|
||||||
import 'models/platform_model.dart';
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_hbb/plugin/handlers.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
|
||||||
|
|
||||||
/// Basic window and launch properties.
|
/// Basic window and launch properties.
|
||||||
int? kWindowId;
|
int? kWindowId;
|
||||||
WindowType? kWindowType;
|
WindowType? kWindowType;
|
||||||
@@ -138,6 +141,8 @@ void runMainApp(bool startService) async {
|
|||||||
await bind.mainCheckConnectStatus();
|
await bind.mainCheckConnectStatus();
|
||||||
if (startService) {
|
if (startService) {
|
||||||
gFFI.serverModel.startService();
|
gFFI.serverModel.startService();
|
||||||
|
bind.pluginSyncUi(syncTo: kAppTypeMain);
|
||||||
|
bind.pluginListReload();
|
||||||
}
|
}
|
||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
@@ -565,6 +570,12 @@ _registerEventHandler() {
|
|||||||
reloadAllWindows();
|
reloadAllWindows();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Register native handlers.
|
||||||
|
if (isDesktop) {
|
||||||
|
platformFFI.registerEventHandler('native_ui', 'native_ui', (evt) async {
|
||||||
|
NativeUiHandler.instance.onEvent(evt);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
platformFFI.registerEventHandler(
|
platformFFI.registerEventHandler(
|
||||||
'android_needs_deploy', 'android_needs_deploy', (_) async {
|
'android_needs_deploy', 'android_needs_deploy', (_) async {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||||
@@ -9,7 +8,6 @@ import 'package:toggle_switch/toggle_switch.dart';
|
|||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
import '../../consts.dart';
|
|
||||||
|
|
||||||
class FileManagerPage extends StatefulWidget {
|
class FileManagerPage extends StatefulWidget {
|
||||||
FileManagerPage(
|
FileManagerPage(
|
||||||
@@ -75,173 +73,6 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
DirectoryOptions get currentOptions => currentFileController.options.value;
|
DirectoryOptions get currentOptions => currentFileController.options.value;
|
||||||
final _uniqueKey = UniqueKey();
|
final _uniqueKey = UniqueKey();
|
||||||
|
|
||||||
Future<T> _runAndroidDocumentPicker<T>(Future<T> Function() action) async {
|
|
||||||
gFFI.ffiModel.beginAndroidDocumentPicker();
|
|
||||||
try {
|
|
||||||
return await action();
|
|
||||||
} finally {
|
|
||||||
gFFI.ffiModel.endAndroidDocumentPicker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _importFiles() async {
|
|
||||||
var imported = 0;
|
|
||||||
var failed = false;
|
|
||||||
final importController = currentFileController;
|
|
||||||
final importDirectory = currentDir.path;
|
|
||||||
final importIsWindows = currentOptions.isWindows;
|
|
||||||
try {
|
|
||||||
final selectedFiles = await _runAndroidDocumentPicker(() =>
|
|
||||||
gFFI.invokeMethodWithResult<List<dynamic>>(
|
|
||||||
AndroidChannel.kPickImportFiles));
|
|
||||||
if (selectedFiles == null || selectedFiles.isEmpty) return;
|
|
||||||
|
|
||||||
for (final selected in selectedFiles) {
|
|
||||||
final uri = (selected as Map<dynamic, dynamic>)['uri'] as String?;
|
|
||||||
final selectedName = selected['name'] as String?;
|
|
||||||
final name = selectedName?.replaceAll('\\', '/').split('/').last;
|
|
||||||
if (uri == null ||
|
|
||||||
name == null ||
|
|
||||||
!PathUtil.validName(name, importIsWindows)) {
|
|
||||||
failed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final destination =
|
|
||||||
PathUtil.join(importDirectory, name, importIsWindows);
|
|
||||||
var overwrite = false;
|
|
||||||
if (await File(destination).exists()) {
|
|
||||||
final overwriteResult = await model.showFileConfirmDialog(
|
|
||||||
translate('Overwrite'), destination, false, false);
|
|
||||||
if (overwriteResult == false) break;
|
|
||||||
if (overwriteResult != true) continue;
|
|
||||||
overwrite = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final success = await gFFI.invokeMethod(
|
|
||||||
AndroidChannel.kImportFile,
|
|
||||||
{'uri': uri, 'path': destination, 'overwrite': overwrite});
|
|
||||||
if (success == true) {
|
|
||||||
imported++;
|
|
||||||
} else {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failed = true;
|
|
||||||
debugPrint('Failed to import $name: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failed = true;
|
|
||||||
debugPrint('Failed to select files for import: $e');
|
|
||||||
}
|
|
||||||
await importController.refresh();
|
|
||||||
if (failed) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
} else if (imported > 0) {
|
|
||||||
showToast(translate('Successful'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _exportFile(Entry entry) async {
|
|
||||||
try {
|
|
||||||
final exported = await _runAndroidDocumentPicker(() => gFFI
|
|
||||||
.invokeMethod(AndroidChannel.kExportFile, {'path': entry.path}));
|
|
||||||
if (exported == true) {
|
|
||||||
showToast(translate('Successful'));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to export ${entry.name}: $e');
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _importFolder() async {
|
|
||||||
final importController = currentFileController;
|
|
||||||
final importDirectory = currentDir.path;
|
|
||||||
final importIsWindows = currentOptions.isWindows;
|
|
||||||
try {
|
|
||||||
final picked = await _runAndroidDocumentPicker(() =>
|
|
||||||
gFFI.invokeMethodWithResult<Map<dynamic, dynamic>>(
|
|
||||||
AndroidChannel.kPickImportDirectory));
|
|
||||||
if (picked == null || picked.isEmpty) return;
|
|
||||||
final uri = picked['uri'] as String?;
|
|
||||||
final name =
|
|
||||||
(picked['name'] as String?)?.replaceAll('\\', '/').split('/').last;
|
|
||||||
if (uri == null ||
|
|
||||||
name == null ||
|
|
||||||
name == '.' ||
|
|
||||||
name == '..' ||
|
|
||||||
!PathUtil.validName(name, importIsWindows)) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final destination = PathUtil.join(importDirectory, name, importIsWindows);
|
|
||||||
final destinationType = await FileSystemEntity.type(destination);
|
|
||||||
var overwrite = false;
|
|
||||||
if (destinationType == FileSystemEntityType.directory) {
|
|
||||||
final overwriteResult = await model.showFileConfirmDialog(
|
|
||||||
translate('Overwrite'), destination, false, false);
|
|
||||||
if (overwriteResult != true) return;
|
|
||||||
overwrite = true;
|
|
||||||
} else if (destinationType != FileSystemEntityType.notFound) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final success = await gFFI.invokeMethod(AndroidChannel.kImportDirectory,
|
|
||||||
{'uri': uri, 'path': destination, 'overwrite': overwrite});
|
|
||||||
if (success == true) {
|
|
||||||
showToast(translate('Successful'));
|
|
||||||
} else {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to import folder: $e');
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
}
|
|
||||||
await importController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _exportItems(SelectedItems items) async {
|
|
||||||
await _exportPaths(items.items.map((e) => e.path));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _exportLogs() async {
|
|
||||||
final home = currentFileController.homePath;
|
|
||||||
if (home.isEmpty) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final appDir = PathUtil.join(home, appName, false);
|
|
||||||
final paths = [
|
|
||||||
PathUtil.join(appDir, 'Logs', false),
|
|
||||||
PathUtil.join(appDir, 'ScreenRecord', false),
|
|
||||||
].where((p) => File(p).existsSync() || Directory(p).existsSync()).toList();
|
|
||||||
if (paths.isEmpty) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _exportPaths(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _exportPaths(Iterable<String> paths) async {
|
|
||||||
try {
|
|
||||||
final result = await _runAndroidDocumentPicker(() =>
|
|
||||||
gFFI.invokeMethodWithResult<Map<dynamic, dynamic>>(
|
|
||||||
AndroidChannel.kExportFiles, {'paths': paths.toList()}));
|
|
||||||
if (result == null) return;
|
|
||||||
final exported = result['exported'] as int? ?? 0;
|
|
||||||
final failed = result['failed'] as int? ?? 0;
|
|
||||||
if (failed > 0) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
} else if (exported > 0) {
|
|
||||||
showToast(translate('Successful'));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to export paths: $e');
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -328,45 +159,6 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
),
|
),
|
||||||
value: "refresh",
|
value: "refresh",
|
||||||
),
|
),
|
||||||
if (isAndroid)
|
|
||||||
PopupMenuItem(
|
|
||||||
enabled: showLocal && currentDir.path.isNotEmpty,
|
|
||||||
value: "import",
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.add_to_drive,
|
|
||||||
color: Theme.of(context).iconTheme.color),
|
|
||||||
SizedBox(width: 5),
|
|
||||||
Text(translate("Add"))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isAndroid)
|
|
||||||
PopupMenuItem(
|
|
||||||
enabled: showLocal && currentDir.path.isNotEmpty,
|
|
||||||
value: "import_folder",
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.create_new_folder_outlined,
|
|
||||||
color: Theme.of(context).iconTheme.color),
|
|
||||||
SizedBox(width: 5),
|
|
||||||
Text(translate("Import Folder"))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isAndroid)
|
|
||||||
PopupMenuItem(
|
|
||||||
enabled: showLocal && currentDir.path.isNotEmpty,
|
|
||||||
value: "export_logs",
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.article_outlined,
|
|
||||||
color: Theme.of(context).iconTheme.color),
|
|
||||||
SizedBox(width: 5),
|
|
||||||
Text(translate("Export Logs"))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: currentDir.path != "/",
|
enabled: currentDir.path != "/",
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -411,12 +203,6 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
onSelected: (v) {
|
onSelected: (v) {
|
||||||
if (v == "refresh") {
|
if (v == "refresh") {
|
||||||
currentFileController.refresh();
|
currentFileController.refresh();
|
||||||
} else if (v == "import") {
|
|
||||||
_importFiles();
|
|
||||||
} else if (v == "import_folder") {
|
|
||||||
_importFolder();
|
|
||||||
} else if (v == "export_logs") {
|
|
||||||
_exportLogs();
|
|
||||||
} else if (v == "select") {
|
} else if (v == "select") {
|
||||||
model.localController.selectedItems.clear();
|
model.localController.selectedItems.clear();
|
||||||
model.remoteController.selectedItems.clear();
|
model.remoteController.selectedItems.clear();
|
||||||
@@ -514,24 +300,6 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
actions: [
|
actions: [
|
||||||
if (isAndroid &&
|
|
||||||
selectedItems?.isLocal == true &&
|
|
||||||
selectedItems?.items.isNotEmpty == true) ...[
|
|
||||||
if (selectedItems!.items.length == 1 &&
|
|
||||||
selectedItems!.items.single.isFile)
|
|
||||||
IconButton(
|
|
||||||
tooltip: translate("Save as"),
|
|
||||||
icon: Icon(Icons.save_alt),
|
|
||||||
onPressed: () =>
|
|
||||||
_exportFile(selectedItems!.items.single),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
tooltip: translate("Export"),
|
|
||||||
icon: Icon(Icons.drive_folder_upload),
|
|
||||||
onPressed: () => _exportItems(selectedItems!),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.compare_arrows),
|
icon: Icon(Icons.compare_arrows),
|
||||||
onPressed: () => setState(() => showLocal = !showLocal),
|
onPressed: () => setState(() => showLocal = !showLocal),
|
||||||
@@ -598,7 +366,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
return BottomSheetBody(
|
return BottomSheetBody(
|
||||||
leading: CircularProgressIndicator(),
|
leading: CircularProgressIndicator(),
|
||||||
title: translate("Waiting"),
|
title: translate("Waiting"),
|
||||||
text: "${readableFileSize(activeJob.speed)}/s",
|
text:
|
||||||
|
"${translate("Speed")}: ${readableFileSize(activeJob.speed)}/s",
|
||||||
onCanceled: () {
|
onCanceled: () {
|
||||||
model.jobController.cancelJob(activeJob.id);
|
model.jobController.cancelJob(activeJob.id);
|
||||||
jobTable.clear();
|
jobTable.clear();
|
||||||
|
|||||||
@@ -1276,14 +1276,6 @@ void showOptions(
|
|||||||
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
||||||
List<TToggleMenu> displayToggles =
|
List<TToggleMenu> displayToggles =
|
||||||
await toolbarDisplayToggle(context, id, gFFI);
|
await toolbarDisplayToggle(context, id, gFFI);
|
||||||
if (isMobile) {
|
|
||||||
displayToggles.insert(
|
|
||||||
0,
|
|
||||||
TToggleMenu(
|
|
||||||
child: Text(translate('Lock canvas')),
|
|
||||||
value: gFFI.canvasModel.locked,
|
|
||||||
onChanged: (value) => gFFI.canvasModel.setLocked(value == true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TToggleMenu> privacyModeList = [];
|
List<TToggleMenu> privacyModeList = [];
|
||||||
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
||||||
|
|||||||
@@ -225,6 +225,12 @@ class _ServerPageState extends State<ServerPage> {
|
|||||||
|
|
||||||
void checkService() async {
|
void checkService() async {
|
||||||
gFFI.invokeMethod("check_service");
|
gFFI.invokeMethod("check_service");
|
||||||
|
// for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
|
||||||
|
if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
|
||||||
|
AndroidPermissionManager.complete(kManageExternalStorage,
|
||||||
|
await AndroidPermissionManager.check(kManageExternalStorage));
|
||||||
|
debugPrint("file permission finished");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServiceNotRunningNotification extends StatelessWidget {
|
class ServiceNotRunningNotification extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -9,7 +8,6 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|||||||
import 'package:flutter_hbb/models/input_modifier_utils.dart';
|
import 'package:flutter_hbb/models/input_modifier_utils.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/terminal_copy_shortcut.dart';
|
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
||||||
import 'package:flutter_hbb/web/dummy.dart'
|
import 'package:flutter_hbb/web/dummy.dart'
|
||||||
@@ -192,7 +190,6 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
||||||
final hardwareKeyboard = HardwareKeyboard.instance;
|
final hardwareKeyboard = HardwareKeyboard.instance;
|
||||||
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
||||||
platform: defaultTargetPlatform,
|
|
||||||
logicalKey: event.logicalKey,
|
logicalKey: event.logicalKey,
|
||||||
isKeyDown: event is KeyDownEvent,
|
isKeyDown: event is KeyDownEvent,
|
||||||
isKeyRepeat: event is KeyRepeatEvent,
|
isKeyRepeat: event is KeyRepeatEvent,
|
||||||
@@ -247,12 +244,7 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
//
|
//
|
||||||
// Android works fine without this workaround.
|
// Android works fine without this workaround.
|
||||||
deleteDetection: isIOS,
|
deleteDetection: isIOS,
|
||||||
shortcuts: platformTerminalShortcuts(),
|
onKeyEvent: _handleTerminalKeyEvent,
|
||||||
onKeyEvent: terminalCopyHandler(
|
|
||||||
_terminalModel.terminal,
|
|
||||||
_terminalModel.terminalController,
|
|
||||||
fallback: _handleTerminalKeyEvent,
|
|
||||||
),
|
|
||||||
padding: _calculatePadding(heightPx),
|
padding: _calculatePadding(heightPx),
|
||||||
onSecondaryTapDown: (details, offset) async {
|
onSecondaryTapDown: (details, offset) async {
|
||||||
final selection = _terminalModel.terminalController.selection;
|
final selection = _terminalModel.terminalController.selection;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class _PixelbufferTexture {
|
|||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
|
bool _closed = false;
|
||||||
|
int _ptr = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
final textureRenderer = TextureRgbaRenderer();
|
final textureRenderer = TextureRgbaRenderer();
|
||||||
@@ -27,11 +29,22 @@ class _PixelbufferTexture {
|
|||||||
_textureKey = bind.getNextTextureKey();
|
_textureKey = bind.getNextTextureKey();
|
||||||
_sessionId = sessionId;
|
_sessionId = sessionId;
|
||||||
|
|
||||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
final textureKey = _textureKey;
|
||||||
|
textureRenderer.createTexture(textureKey).then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
|
if (_closed) {
|
||||||
|
// Destroyed while creation was still in flight (rapid
|
||||||
|
// connect/disconnect); nobody else will close this texture.
|
||||||
|
await textureRenderer.closeTexture(textureKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
final ptr = await textureRenderer.getTexturePtr(textureKey);
|
||||||
|
if (_closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ptr = ptr;
|
||||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||||
@@ -39,13 +52,16 @@ class _PixelbufferTexture {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(FFI ffi) async {
|
||||||
|
_closed = true;
|
||||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
if (_ptr != 0) {
|
||||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
// Compare-and-clear: only clears if Rust still holds this pointer
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
// (#8016-safe); returning from this synchronous call also means no
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
// push through the old pointer is still in flight.
|
||||||
|
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
||||||
|
_ptr = 0;
|
||||||
}
|
}
|
||||||
await textureRenderer.closeTexture(_textureKey);
|
await textureRenderer.closeTexture(_textureKey);
|
||||||
_textureKey = -1;
|
_textureKey = -1;
|
||||||
@@ -61,6 +77,7 @@ class _GpuTexture {
|
|||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasGpuTextureRender();
|
final support = bind.mainHasGpuTextureRender();
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
|
bool _closed = false;
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
int? _output;
|
int? _output;
|
||||||
@@ -79,9 +96,18 @@ class _GpuTexture {
|
|||||||
gpuTextureRenderer.registerTexture().then((id) async {
|
gpuTextureRenderer.registerTexture().then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
|
if (_closed) {
|
||||||
|
// Destroyed while creation was still in flight (rapid
|
||||||
|
// connect/disconnect); nobody else will unregister this texture.
|
||||||
|
await gpuTextureRenderer.unregisterTexture(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_textureId = id;
|
_textureId = id;
|
||||||
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
||||||
final output = await gpuTextureRenderer.output(id);
|
final output = await gpuTextureRenderer.output(id);
|
||||||
|
if (_closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_output = output;
|
_output = output;
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
platformFFI.registerGpuTexture(sessionId, d, output);
|
platformFFI.registerGpuTexture(sessionId, d, output);
|
||||||
@@ -95,20 +121,22 @@ class _GpuTexture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(FFI ffi) async {
|
||||||
// must stop texture render, render unregistered texture cause crash
|
// must stop texture render, render unregistered texture cause crash
|
||||||
|
_closed = true;
|
||||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
final output = _output;
|
||||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
if (output != null) {
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
// Compare-and-clear, see _PixelbufferTexture.destroy.
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
|
||||||
|
_output = null;
|
||||||
}
|
}
|
||||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||||
_textureId = -1;
|
_textureId = -1;
|
||||||
_destroying = false;
|
_destroying = false;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output");
|
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,11 +228,11 @@ class TextureModel {
|
|||||||
tryRemoveTexture(int idx) {
|
tryRemoveTexture(int idx) {
|
||||||
_control.remove(idx);
|
_control.remove(idx);
|
||||||
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
||||||
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
|
_pixelbufferRenderTextures[idx]!.destroy(ffi);
|
||||||
_pixelbufferRenderTextures.remove(idx);
|
_pixelbufferRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
if (_gpuRenderTextures.containsKey(idx)) {
|
if (_gpuRenderTextures.containsKey(idx)) {
|
||||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||||
_gpuRenderTextures.remove(idx);
|
_gpuRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,25 +252,25 @@ class TextureModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemotePageDispose(bool closeSession) async {
|
onRemotePageDispose() async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewCameraPageDispose(bool closeSession) async {
|
onViewCameraPageDispose() async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,6 @@ class JobID {
|
|||||||
|
|
||||||
typedef GetSessionID = SessionID Function();
|
typedef GetSessionID = SessionID Function();
|
||||||
typedef GetDialogManager = OverlayDialogManager? Function();
|
typedef GetDialogManager = OverlayDialogManager? Function();
|
||||||
typedef ReadRemoteDirectory = Future<void> Function(
|
|
||||||
SessionID sessionId, String path, bool includeHidden);
|
|
||||||
|
|
||||||
const _kRemoteReadDirTimeout = Duration(seconds: 30);
|
|
||||||
const _kRemoteSessionChangedError =
|
|
||||||
'Remote directory read cancelled because the session changed';
|
|
||||||
|
|
||||||
class FileModel {
|
class FileModel {
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
@@ -90,7 +84,6 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onReady() async {
|
Future<void> onReady() async {
|
||||||
fileFetcher.beginRemoteSession();
|
|
||||||
await evtLoop.onReady();
|
await evtLoop.onReady();
|
||||||
if (!isWeb) await localController.onReady();
|
if (!isWeb) await localController.onReady();
|
||||||
await remoteController.onReady();
|
await remoteController.onReady();
|
||||||
@@ -140,11 +133,7 @@ class FileModel {
|
|||||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
final err = evt['err']?.toString() ?? 'Unknown error';
|
final err = evt['err']?.toString() ?? 'Unknown error';
|
||||||
if (id == 0) {
|
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
||||||
fileFetcher.tryCompleteRemoteTaskWithError(err);
|
|
||||||
} else {
|
|
||||||
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
||||||
// even if the event does not have a valid job ID. This allows for generic error handling
|
// even if the event does not have a valid job ID. This allows for generic error handling
|
||||||
@@ -361,8 +350,6 @@ class FileController {
|
|||||||
final history = RxList<String>.empty(growable: true);
|
final history = RxList<String>.empty(growable: true);
|
||||||
final sortBy = SortBy.name.obs;
|
final sortBy = SortBy.name.obs;
|
||||||
var sortAscending = true;
|
var sortAscending = true;
|
||||||
// Incremented for each navigation; only the latest generation applies results.
|
|
||||||
int _directoryRequestGeneration = 0;
|
|
||||||
final JobController jobController;
|
final JobController jobController;
|
||||||
final WeakReference<FFI> rootState;
|
final WeakReference<FFI> rootState;
|
||||||
|
|
||||||
@@ -381,14 +368,6 @@ class FileController {
|
|||||||
void set homePath(String path) => options.value.home = path;
|
void set homePath(String path) => options.value.home = path;
|
||||||
OverlayDialogManager? get dialogManager => rootState.target?.dialogManager;
|
OverlayDialogManager? get dialogManager => rootState.target?.dialogManager;
|
||||||
|
|
||||||
bool _isPathAllowed(String candidate) {
|
|
||||||
if (!isAndroid || !isLocal) return true;
|
|
||||||
if (homePath.isEmpty || candidate.isEmpty) return false;
|
|
||||||
final home = PathUtil.posixContext.normalize(homePath);
|
|
||||||
final target = PathUtil.posixContext.normalize(candidate);
|
|
||||||
return target == home || PathUtil.posixContext.isWithin(home, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
String get shortPath {
|
String get shortPath {
|
||||||
final dirPath = directory.value.path;
|
final dirPath = directory.value.path;
|
||||||
if (dirPath.startsWith(homePath)) {
|
if (dirPath.startsWith(homePath)) {
|
||||||
@@ -422,13 +401,8 @@ class FileController {
|
|||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
var savedDir = (await bind.sessionGetPeerOption(
|
final savedDir = (await bind.sessionGetPeerOption(
|
||||||
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
|
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
|
||||||
if (savedDir.isNotEmpty && !_isPathAllowed(savedDir)) {
|
|
||||||
savedDir = options.value.home;
|
|
||||||
await bind.sessionPeerOption(
|
|
||||||
sessionId: sessionId, name: "local_dir", value: savedDir);
|
|
||||||
}
|
|
||||||
Future<bool> tryOpenReadyDirs() async {
|
Future<bool> tryOpenReadyDirs() async {
|
||||||
final dirs = <String>{
|
final dirs = <String>{
|
||||||
if (directory.value.path.isNotEmpty) directory.value.path,
|
if (directory.value.path.isNotEmpty) directory.value.path,
|
||||||
@@ -498,9 +472,6 @@ class FileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
|
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
|
||||||
if (!_isPathAllowed(path)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isBack) {
|
if (!isBack) {
|
||||||
pushHistory();
|
pushHistory();
|
||||||
}
|
}
|
||||||
@@ -513,20 +484,12 @@ class FileController {
|
|||||||
path = "$path\\";
|
path = "$path\\";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final requestGeneration = ++_directoryRequestGeneration;
|
|
||||||
try {
|
try {
|
||||||
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
||||||
if (requestGeneration != _directoryRequestGeneration) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
fd.format(isWindows, sort: sortBy.value);
|
fd.format(isWindows, sort: sortBy.value);
|
||||||
selectedItems.reconcile(fd.entries);
|
|
||||||
directory.value = fd;
|
directory.value = fd;
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (requestGeneration != _directoryRequestGeneration) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
debugPrint("Failed to openDirectory $path: $e");
|
debugPrint("Failed to openDirectory $path: $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -567,9 +530,6 @@ class FileController {
|
|||||||
final isWindows = options.value.isWindows;
|
final isWindows = options.value.isWindows;
|
||||||
final dirPath = directory.value.path;
|
final dirPath = directory.value.path;
|
||||||
var parent = PathUtil.dirname(dirPath, isWindows);
|
var parent = PathUtil.dirname(dirPath, isWindows);
|
||||||
if (!_isPathAllowed(parent)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// specially for C:\, D:\, goto '/'
|
// specially for C:\, D:\, goto '/'
|
||||||
if (parent == dirPath && isWindows) {
|
if (parent == dirPath && isWindows) {
|
||||||
return await _openDirectoryPath('/', isBack: isBack);
|
return await _openDirectoryPath('/', isBack: isBack);
|
||||||
@@ -581,7 +541,6 @@ class FileController {
|
|||||||
void initDirAndHome(Map<String, dynamic> evt) {
|
void initDirAndHome(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
||||||
final isHomeResponse = fileFetcher.isLikelyRemoteHomeResponse(fd.path);
|
|
||||||
fd.format(options.value.isWindows, sort: sortBy.value);
|
fd.format(options.value.isWindows, sort: sortBy.value);
|
||||||
if (fd.id > 0) {
|
if (fd.id > 0) {
|
||||||
final jobIndex = jobController.getJob(fd.id);
|
final jobIndex = jobController.getJob(fd.id);
|
||||||
@@ -597,12 +556,10 @@ class FileController {
|
|||||||
debugPrint("update receive details: ${fd.path}");
|
debugPrint("update receive details: ${fd.path}");
|
||||||
jobController.jobTable.refresh();
|
jobController.jobTable.refresh();
|
||||||
}
|
}
|
||||||
} else if (options.value.home.isEmpty && isHomeResponse) {
|
} else if (options.value.home.isEmpty) {
|
||||||
options.value.home = fd.path;
|
options.value.home = fd.path;
|
||||||
debugPrint("init remote home: ${fd.path}");
|
debugPrint("init remote home: ${fd.path}");
|
||||||
if (_directoryRequestGeneration == 0) {
|
directory.value = fd;
|
||||||
directory.value = fd;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("initDirAndHome err=$e");
|
debugPrint("initDirAndHome err=$e");
|
||||||
@@ -1405,78 +1362,16 @@ class JobResultListener<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RemoteReadTask {
|
|
||||||
final bool includeHidden;
|
|
||||||
final Completer<FileDirectory> completer = Completer<FileDirectory>();
|
|
||||||
final Completer<void> released = Completer<void>();
|
|
||||||
late final Timer timer;
|
|
||||||
|
|
||||||
_RemoteReadTask(this.includeHidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileFetcher {
|
class FileFetcher {
|
||||||
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
|
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
|
||||||
final Map<String, _RemoteReadTask> _remoteReadTasks = {};
|
Map<String, Completer<FileDirectory>> remoteTasks = {};
|
||||||
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
|
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
|
||||||
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
|
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
|
||||||
int _remoteSessionGeneration = 0;
|
|
||||||
|
|
||||||
final GetSessionID getSessionID;
|
final GetSessionID getSessionID;
|
||||||
final ReadRemoteDirectory _readRemoteDirectory;
|
|
||||||
SessionID get sessionId => getSessionID();
|
SessionID get sessionId => getSessionID();
|
||||||
|
|
||||||
FileFetcher(this.getSessionID, {ReadRemoteDirectory? readRemoteDirectory})
|
FileFetcher(this.getSessionID);
|
||||||
: _readRemoteDirectory = readRemoteDirectory ??
|
|
||||||
((sessionId, path, includeHidden) => bind.sessionReadRemoteDir(
|
|
||||||
sessionId: sessionId,
|
|
||||||
path: path,
|
|
||||||
includeHidden: includeHidden));
|
|
||||||
|
|
||||||
bool hasPendingRemoteRead(String path) => _remoteReadTasks.containsKey(path);
|
|
||||||
|
|
||||||
bool isLikelyRemoteHomeResponse(String path) =>
|
|
||||||
_remoteReadTasks.isEmpty ||
|
|
||||||
(_remoteReadTasks.length == 1 &&
|
|
||||||
hasPendingRemoteRead("") &&
|
|
||||||
!hasPendingRemoteRead(path));
|
|
||||||
|
|
||||||
void beginRemoteSession() {
|
|
||||||
_remoteSessionGeneration++;
|
|
||||||
final pendingTasks = _remoteReadTasks.entries.toList(growable: false);
|
|
||||||
for (final entry in pendingTasks) {
|
|
||||||
final task = entry.value;
|
|
||||||
if (!_removeRemoteReadTask(entry.key, task)) continue;
|
|
||||||
task.completer.completeError(StateError(_kRemoteSessionChangedError));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_RemoteReadTask _registerRemoteReadTask(String path, bool includeHidden) {
|
|
||||||
if (hasPendingRemoteRead(path)) {
|
|
||||||
throw "Failed to registerReadTask, already have same read job";
|
|
||||||
}
|
|
||||||
final task = _RemoteReadTask(includeHidden);
|
|
||||||
_remoteReadTasks[path] = task;
|
|
||||||
task.timer = Timer(_kRemoteReadDirTimeout, () {
|
|
||||||
if (!_removeRemoteReadTask(path, task)) return;
|
|
||||||
task.completer.completeError("Failed to read dir, timeout");
|
|
||||||
});
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _removeRemoteReadTask(String path, _RemoteReadTask task) {
|
|
||||||
if (!identical(_remoteReadTasks[path], task)) return false;
|
|
||||||
_remoteReadTasks.remove(path);
|
|
||||||
task.timer.cancel();
|
|
||||||
task.released.complete();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _completeRemoteReadTask(String path, FileDirectory directory) {
|
|
||||||
final task = _remoteReadTasks[path];
|
|
||||||
if (task == null || !_removeRemoteReadTask(path, task)) return false;
|
|
||||||
task.completer.complete(directory);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<FileDirectory>> registerReadEmptyDirsTask(
|
Future<List<FileDirectory>> registerReadEmptyDirsTask(
|
||||||
bool isLocal, String path) {
|
bool isLocal, String path) {
|
||||||
@@ -1496,6 +1391,23 @@ class FileFetcher {
|
|||||||
return c.future;
|
return c.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
|
||||||
|
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
||||||
|
final tasks = remoteTasks; // bypass now
|
||||||
|
if (tasks.containsKey(path)) {
|
||||||
|
throw "Failed to registerReadTask, already have same read job";
|
||||||
|
}
|
||||||
|
final c = Completer<FileDirectory>();
|
||||||
|
tasks[path] = c;
|
||||||
|
|
||||||
|
Timer(Duration(seconds: 2), () {
|
||||||
|
tasks.remove(path);
|
||||||
|
if (c.isCompleted) return;
|
||||||
|
c.completeError("Failed to read dir, timeout");
|
||||||
|
});
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
|
||||||
Future<FileDirectory> registerReadRecursiveTask(int actID) {
|
Future<FileDirectory> registerReadRecursiveTask(int actID) {
|
||||||
final tasks = readRecursiveTasks;
|
final tasks = readRecursiveTasks;
|
||||||
if (tasks.containsKey(actID)) {
|
if (tasks.containsKey(actID)) {
|
||||||
@@ -1533,37 +1445,27 @@ class FileFetcher {
|
|||||||
|
|
||||||
tryCompleteTask(String? msg, String? isLocalStr) {
|
tryCompleteTask(String? msg, String? isLocalStr) {
|
||||||
if (msg == null || isLocalStr == null) return;
|
if (msg == null || isLocalStr == null) return;
|
||||||
|
late final Map<Object, Completer<FileDirectory>> tasks;
|
||||||
try {
|
try {
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(msg));
|
final fd = FileDirectory.fromJson(jsonDecode(msg));
|
||||||
if (fd.id > 0) {
|
if (fd.id > 0) {
|
||||||
// fd.id > 0 is result for read recursive
|
// fd.id > 0 is result for read recursive
|
||||||
final completer = readRecursiveTasks.remove(fd.id);
|
// to-do later,will be better if every fetch use ID,so that there will only one task map for read and recursive read
|
||||||
|
tasks = readRecursiveTasks;
|
||||||
|
final completer = tasks.remove(fd.id);
|
||||||
|
completer?.complete(fd);
|
||||||
|
} else if (fd.path.isNotEmpty) {
|
||||||
|
// result for normal read dir
|
||||||
|
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
||||||
|
tasks = remoteTasks; // bypass now
|
||||||
|
final completer = tasks.remove(fd.path);
|
||||||
completer?.complete(fd);
|
completer?.complete(fd);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isLocalStr == "false" && fd.path.isNotEmpty) {
|
|
||||||
if (_completeRemoteReadTask(fd.path, fd)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// A Home request uses an empty path but returns its resolved path.
|
|
||||||
if (isLikelyRemoteHomeResponse(fd.path)) {
|
|
||||||
_completeRemoteReadTask("", fd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("tryCompleteJob err: $e");
|
debugPrint("tryCompleteJob err: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tryCompleteRemoteTaskWithError(String error) {
|
|
||||||
if (_remoteReadTasks.length != 1) return false;
|
|
||||||
final entry = _remoteReadTasks.entries.single;
|
|
||||||
final task = entry.value;
|
|
||||||
if (!_removeRemoteReadTask(entry.key, task)) return false;
|
|
||||||
task.completer.completeError(error);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete a pending recursive read task with an error.
|
// Complete a pending recursive read task with an error.
|
||||||
// See FileModel.handleJobError() for why this is necessary.
|
// See FileModel.handleJobError() for why this is necessary.
|
||||||
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
||||||
@@ -1604,26 +1506,9 @@ class FileFetcher {
|
|||||||
final fd = FileDirectory.fromJson(jsonDecode(res));
|
final fd = FileDirectory.fromJson(jsonDecode(res));
|
||||||
return fd;
|
return fd;
|
||||||
} else {
|
} else {
|
||||||
final remoteSessionGeneration = _remoteSessionGeneration;
|
await bind.sessionReadRemoteDir(
|
||||||
final pendingTask = _remoteReadTasks[path];
|
sessionId: sessionId, path: path, includeHidden: showHidden);
|
||||||
if (pendingTask != null) {
|
return registerReadTask(isLocal, path);
|
||||||
if (pendingTask.includeHidden == showHidden) {
|
|
||||||
return pendingTask.completer.future;
|
|
||||||
}
|
|
||||||
await pendingTask.released.future;
|
|
||||||
if (remoteSessionGeneration != _remoteSessionGeneration) {
|
|
||||||
throw StateError(_kRemoteSessionChangedError);
|
|
||||||
}
|
|
||||||
return fetchDirectory(path, isLocal, showHidden);
|
|
||||||
}
|
|
||||||
final task = _registerRemoteReadTask(path, showHidden);
|
|
||||||
unawaited(Future<void>.sync(
|
|
||||||
() => _readRemoteDirectory(sessionId, path, showHidden))
|
|
||||||
.catchError((Object error, StackTrace stackTrace) {
|
|
||||||
if (!_removeRemoteReadTask(path, task)) return;
|
|
||||||
task.completer.completeError(error, stackTrace);
|
|
||||||
}));
|
|
||||||
return task.completer.future;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Future.error(e);
|
return Future.error(e);
|
||||||
@@ -1905,7 +1790,7 @@ class PathUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool validName(String name, bool isWindows) {
|
static bool validName(String name, bool isWindows) {
|
||||||
final unixFileNamePattern = RegExp(r'^[^/\x00]+$');
|
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
|
||||||
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
||||||
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
||||||
return reg.hasMatch(name);
|
return reg.hasMatch(name);
|
||||||
@@ -1948,21 +1833,6 @@ class SelectedItems {
|
|||||||
items.clear();
|
items.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reconcile(List<Entry> entries) {
|
|
||||||
if (items.isEmpty) return;
|
|
||||||
final currentByPath = {for (final entry in entries) entry.path: entry};
|
|
||||||
final reconciled = <Entry>[];
|
|
||||||
for (final item in items) {
|
|
||||||
final current = currentByPath[item.path];
|
|
||||||
if (current != null && current.entryType == item.entryType) {
|
|
||||||
reconciled.add(current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items
|
|
||||||
..clear()
|
|
||||||
..addAll(reconciled);
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectAll(List<Entry> entries) {
|
void selectAll(List<Entry> entries) {
|
||||||
items.clear();
|
items.clear();
|
||||||
items.addAll(entries);
|
items.addAll(entries);
|
||||||
|
|||||||
@@ -1787,11 +1787,6 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _checkPeerControlProtected(double x, double y) {
|
bool _checkPeerControlProtected(double x, double y) {
|
||||||
if (isViewOnly && showMyCursor) {
|
|
||||||
lastMousePos = ui.Offset(x, y);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final cursorModel = parent.target!.cursorModel;
|
final cursorModel = parent.target!.cursorModel;
|
||||||
if (cursorModel.isPeerControlProtected) {
|
if (cursorModel.isPeerControlProtected) {
|
||||||
lastMousePos = ui.Offset(x, y);
|
lastMousePos = ui.Offset(x, y);
|
||||||
|
|||||||
@@ -117,11 +117,10 @@ String prepareTerminalInputPayload(
|
|||||||
|
|
||||||
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
||||||
///
|
///
|
||||||
/// xterm already handles each platform's paste shortcut in the common case.
|
/// xterm already handles hardware Ctrl/Cmd+V correctly in the common case. Only
|
||||||
/// Only intercept while a virtual Ctrl/Alt lock is active, because xterm can
|
/// intercept while a virtual Ctrl/Alt lock is active, because xterm can emit a
|
||||||
/// emit a one-character paste as normal text when bracketed paste mode is off.
|
/// one-character paste as normal text when bracketed paste mode is disabled.
|
||||||
bool shouldHandleTerminalPasteShortcut({
|
bool shouldHandleTerminalPasteShortcut({
|
||||||
required TargetPlatform platform,
|
|
||||||
required LogicalKeyboardKey logicalKey,
|
required LogicalKeyboardKey logicalKey,
|
||||||
required bool isKeyDown,
|
required bool isKeyDown,
|
||||||
required bool isKeyRepeat,
|
required bool isKeyRepeat,
|
||||||
@@ -134,18 +133,8 @@ bool shouldHandleTerminalPasteShortcut({
|
|||||||
if (!modifierLockActive) return false;
|
if (!modifierLockActive) return false;
|
||||||
if (!isKeyDown && !isKeyRepeat) return false;
|
if (!isKeyDown && !isKeyRepeat) return false;
|
||||||
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
||||||
if (altPressed) return false;
|
if (altPressed || shiftPressed) return false;
|
||||||
switch (platform) {
|
return controlPressed != metaPressed;
|
||||||
case TargetPlatform.linux:
|
|
||||||
return controlPressed && !metaPressed && shiftPressed;
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
return !controlPressed && metaPressed && !shiftPressed;
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
return controlPressed && !metaPressed && !shiftPressed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true when collapsing Row3 should also clear hidden modifier state.
|
/// Returns true when collapsing Row3 should also clear hidden modifier state.
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import 'package:flutter_hbb/models/user_model.dart';
|
|||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/event.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/manager.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||||
@@ -124,8 +127,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
Timer? _restartReconnectDelayTimer;
|
Timer? _restartReconnectDelayTimer;
|
||||||
var _reconnects = 1;
|
var _reconnects = 1;
|
||||||
DateTime? _offlineReconnectStartTime;
|
DateTime? _offlineReconnectStartTime;
|
||||||
bool _androidDocumentPickerActive = false;
|
|
||||||
bool _androidDocumentPickerInterruptedConnection = false;
|
|
||||||
bool _viewOnly = false;
|
bool _viewOnly = false;
|
||||||
bool _showMyCursor = false;
|
bool _showMyCursor = false;
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
@@ -257,8 +258,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
_inputBlocked = false;
|
_inputBlocked = false;
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
_androidDocumentPickerActive = false;
|
|
||||||
_androidDocumentPickerInterruptedConnection = false;
|
|
||||||
resetRestartReconnectState();
|
resetRestartReconnectState();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
waitForImageTimer?.cancel();
|
waitForImageTimer?.cancel();
|
||||||
@@ -438,6 +437,15 @@ class FfiModel with ChangeNotifier {
|
|||||||
parent.target?.serverModel.updateVoiceCallState(evt);
|
parent.target?.serverModel.updateVoiceCallState(evt);
|
||||||
} else if (name == 'fingerprint') {
|
} else if (name == 'fingerprint') {
|
||||||
FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
|
FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
|
||||||
|
} else if (name == 'plugin_manager') {
|
||||||
|
pluginManager.handleEvent(evt);
|
||||||
|
} else if (name == 'plugin_event') {
|
||||||
|
handlePluginEvent(evt,
|
||||||
|
(Map<String, dynamic> e) => handleMsgBox(e, sessionId, peerId));
|
||||||
|
} else if (name == 'plugin_reload') {
|
||||||
|
handleReloading(evt);
|
||||||
|
} else if (name == 'plugin_option') {
|
||||||
|
handleOption(evt);
|
||||||
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
||||||
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
||||||
final id = evt['id'];
|
final id = evt['id'];
|
||||||
@@ -727,6 +735,11 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handleUseTextureRender(
|
_handleUseTextureRender(
|
||||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||||
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
||||||
|
if (evt['reason'] == 'fallback') {
|
||||||
|
// The Rust watchdog detected that pushed frames were never rendered
|
||||||
|
// and switched this session to software rendering.
|
||||||
|
showToast(translate('texture-render-fallback-tip'));
|
||||||
|
}
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
||||||
@@ -896,13 +909,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
final text = evt['text'];
|
final text = evt['text'];
|
||||||
final link = evt['link'];
|
final link = evt['link'];
|
||||||
|
|
||||||
if (isAndroid &&
|
|
||||||
_androidDocumentPickerActive &&
|
|
||||||
title == 'Connection Error') {
|
|
||||||
_androidDocumentPickerInterruptedConnection = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable relative mouse mode on any error-type message to ensure cursor is released.
|
// Disable relative mouse mode on any error-type message to ensure cursor is released.
|
||||||
// This includes connection errors, session-ending messages, elevation errors, etc.
|
// This includes connection errors, session-ending messages, elevation errors, etc.
|
||||||
// Safety: releasing pointer lock on errors prevents the user from being stuck.
|
// Safety: releasing pointer lock on errors prevents the user from being stuck.
|
||||||
@@ -919,12 +925,17 @@ class FfiModel with ChangeNotifier {
|
|||||||
enter2FaDialog(sessionId, dialogManager);
|
enter2FaDialog(sessionId, dialogManager);
|
||||||
} else if (type == 'input-password') {
|
} else if (type == 'input-password') {
|
||||||
enterPasswordDialog(sessionId, dialogManager);
|
enterPasswordDialog(sessionId, dialogManager);
|
||||||
|
} else if (type == 'session-login' || type == 'session-re-login') {
|
||||||
|
enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true);
|
||||||
|
} else if (type == 'session-login-password') {
|
||||||
|
enterUserLoginAndPasswordDialog(
|
||||||
|
sessionId, dialogManager, 'login_linux_tip', true);
|
||||||
} else if (type == 'terminal-admin-login') {
|
} else if (type == 'terminal-admin-login') {
|
||||||
enterUserLoginDialog(
|
enterUserLoginDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip');
|
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||||
} else if (type == 'terminal-admin-login-password') {
|
} else if (type == 'terminal-admin-login-password') {
|
||||||
enterUserLoginAndPasswordDialog(
|
enterUserLoginAndPasswordDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip');
|
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||||
} else if (type == 'restarting') {
|
} else if (type == 'restarting') {
|
||||||
// Treat restart messages as reconnect control events. Rust still sends
|
// Treat restart messages as reconnect control events. Rust still sends
|
||||||
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
||||||
@@ -979,23 +990,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
_restartReconnectDelayTimer = null;
|
_restartReconnectDelayTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void beginAndroidDocumentPicker() {
|
|
||||||
if (!isAndroid) return;
|
|
||||||
_androidDocumentPickerActive = true;
|
|
||||||
_androidDocumentPickerInterruptedConnection = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void endAndroidDocumentPicker() {
|
|
||||||
if (!isAndroid) return;
|
|
||||||
_androidDocumentPickerActive = false;
|
|
||||||
if (!_androidDocumentPickerInterruptedConnection ||
|
|
||||||
parent.target?.closed == true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_androidDocumentPickerInterruptedConnection = false;
|
|
||||||
reconnect(parent.target!.dialogManager, sessionId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Auto-retry check for "Remote desktop is offline" error.
|
/// Auto-retry check for "Remote desktop is offline" error.
|
||||||
/// returns true to auto-retry, false otherwise.
|
/// returns true to auto-retry, false otherwise.
|
||||||
bool shouldAutoRetryOnOffline(
|
bool shouldAutoRetryOnOffline(
|
||||||
@@ -2236,7 +2230,6 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double _y = 0;
|
double _y = 0;
|
||||||
// image scale
|
// image scale
|
||||||
double _scale = 1.0;
|
double _scale = 1.0;
|
||||||
bool _locked = false;
|
|
||||||
double _devicePixelRatio = 1.0;
|
double _devicePixelRatio = 1.0;
|
||||||
Size _size = Size.zero;
|
Size _size = Size.zero;
|
||||||
// the tabbar over the image
|
// the tabbar over the image
|
||||||
@@ -2285,19 +2278,12 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double get x => _x;
|
double get x => _x;
|
||||||
double get y => _y;
|
double get y => _y;
|
||||||
double get scale => _scale;
|
double get scale => _scale;
|
||||||
bool get locked => _locked;
|
|
||||||
double get devicePixelRatio => _devicePixelRatio;
|
double get devicePixelRatio => _devicePixelRatio;
|
||||||
Size get size => _size;
|
Size get size => _size;
|
||||||
ScrollStyle get scrollStyle => _scrollStyle;
|
ScrollStyle get scrollStyle => _scrollStyle;
|
||||||
ViewStyle get viewStyle => _lastViewStyle;
|
ViewStyle get viewStyle => _lastViewStyle;
|
||||||
RxBool get imageOverflow => _imageOverflow;
|
RxBool get imageOverflow => _imageOverflow;
|
||||||
|
|
||||||
void setLocked(bool value) {
|
|
||||||
if (_locked == value) return;
|
|
||||||
_locked = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
_resetScroll() => setScrollPercent(0.0, 0.0);
|
_resetScroll() => setScrollPercent(0.0, 0.0);
|
||||||
|
|
||||||
void setScrollPercent(double x, double y) {
|
void setScrollPercent(double x, double y) {
|
||||||
@@ -2526,7 +2512,6 @@ class CanvasModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateLocalCursor(double x, double y) {
|
void updateLocalCursor(double x, double y) {
|
||||||
if (parent.target?.ffiModel.viewOnly == true) return;
|
|
||||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||||
// Draw cursor if is not desktop.
|
// Draw cursor if is not desktop.
|
||||||
@@ -2759,7 +2744,6 @@ class CanvasModel with ChangeNotifier {
|
|||||||
_x = 0;
|
_x = 0;
|
||||||
_y = 0;
|
_y = 0;
|
||||||
_scale = 1.0;
|
_scale = 1.0;
|
||||||
_locked = false;
|
|
||||||
_lastViewStyle = ViewStyle.defaultViewStyle();
|
_lastViewStyle = ViewStyle.defaultViewStyle();
|
||||||
_timerMobileFocusCanvasCursor?.cancel();
|
_timerMobileFocusCanvasCursor?.cancel();
|
||||||
_timerMobileRestoreCanvasOffset?.cancel();
|
_timerMobileRestoreCanvasOffset?.cancel();
|
||||||
@@ -2871,7 +2855,7 @@ class CursorData {
|
|||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
}) : hotx = hotxOrigin * scale,
|
}) : hotx = hotxOrigin * scale,
|
||||||
hoty = hotyOrigin * scale;
|
hoty = hotxOrigin * scale;
|
||||||
|
|
||||||
int _doubleToInt(double v) => (v * 10e6).round().toInt();
|
int _doubleToInt(double v) => (v * 10e6).round().toInt();
|
||||||
|
|
||||||
@@ -4088,11 +4072,6 @@ class FFI {
|
|||||||
return await platformFFI.invokeMethod(method, arguments);
|
return await platformFFI.invokeMethod(method, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T?> invokeMethodWithResult<T>(String method,
|
|
||||||
[dynamic arguments]) async {
|
|
||||||
return await platformFFI.invokeMethodWithResult<T>(method, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminal model management
|
// Terminal model management
|
||||||
void registerTerminalModel(int terminalId, TerminalModel model) {
|
void registerTerminalModel(int terminalId, TerminalModel model) {
|
||||||
debugPrint('[FFI] Registering terminal model for terminal $terminalId');
|
debugPrint('[FFI] Registering terminal model for terminal $terminalId');
|
||||||
@@ -4197,6 +4176,7 @@ class PeerInfo with ChangeNotifier {
|
|||||||
RxBool isSet = false.obs;
|
RxBool isSet = false.obs;
|
||||||
|
|
||||||
bool get isWayland => platformAdditions[kPlatformAdditionsIsWayland] == true;
|
bool get isWayland => platformAdditions[kPlatformAdditionsIsWayland] == true;
|
||||||
|
bool get isHeadless => platformAdditions[kPlatformAdditionsHeadless] == true;
|
||||||
bool get isInstalled =>
|
bool get isInstalled =>
|
||||||
platform != kPeerPlatformWindows ||
|
platform != kPeerPlatformWindows ||
|
||||||
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:external_path/external_path.dart';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -130,6 +131,12 @@ class PlatformFFI {
|
|||||||
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
_ffiBind.sessionRegisterGpuTexture(
|
_ffiBind.sessionRegisterGpuTexture(
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
|
_ffiBind.sessionUnregisterPixelbufferTexture(
|
||||||
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
|
_ffiBind.sessionUnregisterGpuTexture(
|
||||||
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
|
||||||
/// Init the FFI class, loads the native Rust core library.
|
/// Init the FFI class, loads the native Rust core library.
|
||||||
Future<void> init(String appType) async {
|
Future<void> init(String appType) async {
|
||||||
@@ -170,10 +177,8 @@ class PlatformFFI {
|
|||||||
_startListenEvent(_ffiBind); // global event
|
_startListenEvent(_ffiBind); // global event
|
||||||
try {
|
try {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
// Android file transfer uses app-specific storage. User-selected
|
// only support for android
|
||||||
// files enter and leave this workspace through the system picker.
|
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
|
||||||
_homeDir = (await getExternalStorageDirectory())?.path ??
|
|
||||||
(await getApplicationSupportDirectory()).path;
|
|
||||||
} else if (isIOS) {
|
} else if (isIOS) {
|
||||||
// The previous code was `_homeDir = (await getDownloadsDirectory())?.path ?? '';`,
|
// The previous code was `_homeDir = (await getDownloadsDirectory())?.path ?? '';`,
|
||||||
// which provided the `downloads` path in the sandbox.
|
// which provided the `downloads` path in the sandbox.
|
||||||
@@ -307,12 +312,6 @@ class PlatformFFI {
|
|||||||
return await _toAndroidChannel.invokeMethod(method, arguments);
|
return await _toAndroidChannel.invokeMethod(method, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T?> invokeMethodWithResult<T>(String method,
|
|
||||||
[dynamic arguments]) async {
|
|
||||||
if (!isAndroid) return null;
|
|
||||||
return await _toAndroidChannel.invokeMethod<T>(method, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
void syncAndroidServiceAppDirConfigPath() {
|
void syncAndroidServiceAppDirConfigPath() {
|
||||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,10 +210,15 @@ class ServerModel with ChangeNotifier {
|
|||||||
_audioOk = audioOption != 'N';
|
_audioOk = audioOption != 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android file transfer is confined to app-specific storage. Files enter
|
// file
|
||||||
// and leave the workspace through Android's system document picker.
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
final fileOption = await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
_fileOk = false;
|
||||||
_fileOk = fileOption != 'N';
|
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
|
||||||
|
} else {
|
||||||
|
final fileOption =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||||
|
_fileOk = fileOption != 'N';
|
||||||
|
}
|
||||||
|
|
||||||
// clipboard
|
// clipboard
|
||||||
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
|
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
|
||||||
@@ -314,6 +319,16 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (clients.any((c) => !c.disconnected)) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
|
if (!_fileOk &&
|
||||||
|
!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
|
final res =
|
||||||
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
|
if (!res) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_fileOk = !_fileOk;
|
_fileOk = !_fileOk;
|
||||||
bind.mainSetOption(
|
bind.mainSetOption(
|
||||||
key: kOptionEnableFileTransfer,
|
key: kOptionEnableFileTransfer,
|
||||||
@@ -403,6 +418,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
||||||
await checkFloatingWindowPermission();
|
await checkFloatingWindowPermission();
|
||||||
}
|
}
|
||||||
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
|
}
|
||||||
final res = await parent.target?.dialogManager
|
final res = await parent.target?.dialogManager
|
||||||
.show<bool>((setState, close, context) {
|
.show<bool>((setState, close, context) {
|
||||||
submit() => close(true);
|
submit() => close(true);
|
||||||
@@ -720,13 +738,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `byOperator` false means the CM's window went away rather than a person asking for the
|
Future<void> closeAll() async {
|
||||||
/// peers to go. The sessions end either way; only the close reason differs, and with it
|
await Future.wait(
|
||||||
/// whether the peer is allowed to reconnect. See `ipc::Data::CmWindowClosed`.
|
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||||
Future<void> closeAll({bool byOperator = true}) async {
|
|
||||||
await Future.wait(_clients.map((client) => byOperator
|
|
||||||
? bind.cmCloseConnection(connId: client.id)
|
|
||||||
: bind.cmCloseConnectionWindow(connId: client.id)));
|
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
tabController.state.value.tabs.clear();
|
tabController.state.value.tabs.clear();
|
||||||
if (isAndroid) androidUpdatekeepScreenOn();
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:xterm/xterm.dart';
|
|
||||||
|
|
||||||
const _controlShiftVPasteShortcut = SingleActivator(
|
|
||||||
LogicalKeyboardKey.keyV,
|
|
||||||
control: true,
|
|
||||||
shift: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> writeTerminalClipboard(String text) async {
|
|
||||||
try {
|
|
||||||
await Clipboard.setData(ClipboardData(text: text));
|
|
||||||
} catch (error) {
|
|
||||||
debugPrint('[Terminal] Failed to write clipboard: $error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<ShortcutActivator, Intent>? platformTerminalShortcuts() {
|
|
||||||
final platform = defaultTargetPlatform;
|
|
||||||
if (platform == TargetPlatform.linux) {
|
|
||||||
return {
|
|
||||||
for (final entry in defaultTerminalShortcuts.entries)
|
|
||||||
if (!_isControlShortcut(entry.key, LogicalKeyboardKey.keyV))
|
|
||||||
entry.key: entry.value,
|
|
||||||
_controlShiftVPasteShortcut:
|
|
||||||
const PasteTextIntent(SelectionChangedCause.keyboard),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (platform != TargetPlatform.windows &&
|
|
||||||
platform != TargetPlatform.android) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
for (final entry in defaultTerminalShortcuts.entries)
|
|
||||||
if (!_isControlShortcut(
|
|
||||||
entry.key,
|
|
||||||
LogicalKeyboardKey.keyC,
|
|
||||||
shift: true,
|
|
||||||
))
|
|
||||||
entry.key: entry.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isControlShortcut(
|
|
||||||
ShortcutActivator shortcut,
|
|
||||||
LogicalKeyboardKey key, {
|
|
||||||
bool shift = false,
|
|
||||||
}) =>
|
|
||||||
shortcut is SingleActivator &&
|
|
||||||
shortcut.trigger == key &&
|
|
||||||
shortcut.control &&
|
|
||||||
shortcut.shift == shift &&
|
|
||||||
!shortcut.alt &&
|
|
||||||
!shortcut.meta;
|
|
||||||
|
|
||||||
FocusOnKeyEventCallback terminalCopyHandler(
|
|
||||||
Terminal terminal,
|
|
||||||
TerminalController controller, {
|
|
||||||
FocusOnKeyEventCallback? fallback,
|
|
||||||
}) =>
|
|
||||||
(focusNode, event) {
|
|
||||||
if (_isSelectionCopyShortcut(event)) {
|
|
||||||
final selection = controller.selection;
|
|
||||||
if (selection != null && !selection.isCollapsed) {
|
|
||||||
if (event is KeyDownEvent) {
|
|
||||||
final text = terminal.buffer.getText(selection);
|
|
||||||
unawaited(writeTerminalClipboard(text));
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback?.call(focusNode, event) ?? KeyEventResult.ignored;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool _isSelectionCopyShortcut(KeyEvent event) {
|
|
||||||
final keyboard = HardwareKeyboard.instance;
|
|
||||||
final platform = defaultTargetPlatform;
|
|
||||||
final usesControlCopy =
|
|
||||||
platform == TargetPlatform.windows || platform == TargetPlatform.android;
|
|
||||||
return usesControlCopy &&
|
|
||||||
(event is KeyDownEvent || event is KeyRepeatEvent) &&
|
|
||||||
event.logicalKey == LogicalKeyboardKey.keyC &&
|
|
||||||
keyboard.isControlPressed &&
|
|
||||||
!keyboard.isShiftPressed &&
|
|
||||||
!keyboard.isAltPressed &&
|
|
||||||
!keyboard.isMetaPressed;
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:xterm/xterm.dart';
|
|
||||||
|
|
||||||
const _cellIndexOffset = 1;
|
|
||||||
const _legacyCodeOffset = 32;
|
|
||||||
const _leftButtonCode = 0;
|
|
||||||
const _motionButtonCode = 32;
|
|
||||||
const _releaseButtonCode = 3;
|
|
||||||
const _shiftModifierCode = 4;
|
|
||||||
const _metaModifierCode = 8;
|
|
||||||
const _controlModifierCode = 16;
|
|
||||||
const _modifierCodeMask =
|
|
||||||
_shiftModifierCode | _metaModifierCode | _controlModifierCode;
|
|
||||||
const _normalCoordinateLimit = 223;
|
|
||||||
const _utfCoordinateLimit = 2015;
|
|
||||||
|
|
||||||
String encodeTerminalMouseReport(
|
|
||||||
MouseReportMode mode,
|
|
||||||
int button,
|
|
||||||
CellOffset position, {
|
|
||||||
bool release = false,
|
|
||||||
}) {
|
|
||||||
final x = position.x + _cellIndexOffset;
|
|
||||||
final y = position.y + _cellIndexOffset;
|
|
||||||
final reportedButton =
|
|
||||||
release ? _releaseButtonCode | (button & _modifierCodeMask) : button;
|
|
||||||
switch (mode) {
|
|
||||||
case MouseReportMode.normal:
|
|
||||||
case MouseReportMode.utf:
|
|
||||||
final limit = mode == MouseReportMode.normal
|
|
||||||
? _normalCoordinateLimit
|
|
||||||
: _utfCoordinateLimit;
|
|
||||||
final encodedButton =
|
|
||||||
String.fromCharCode(_legacyCodeOffset + reportedButton);
|
|
||||||
return '\x1b[M$encodedButton${_legacyCoordinate(x, limit)}'
|
|
||||||
'${_legacyCoordinate(y, limit)}';
|
|
||||||
case MouseReportMode.sgr:
|
|
||||||
final suffix = release ? 'm' : 'M';
|
|
||||||
return '\x1b[<$button;$x;$y$suffix';
|
|
||||||
case MouseReportMode.urxvt:
|
|
||||||
return '\x1b[${_legacyCodeOffset + reportedButton};$x;${y}M';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _legacyCoordinate(int value, int limit) =>
|
|
||||||
value > limit ? '\x00' : String.fromCharCode(_legacyCodeOffset + value);
|
|
||||||
|
|
||||||
int _activeModifierCode() {
|
|
||||||
final keyboard = HardwareKeyboard.instance;
|
|
||||||
return (keyboard.isShiftPressed ? _shiftModifierCode : 0) |
|
|
||||||
(keyboard.isAltPressed ? _metaModifierCode : 0) |
|
|
||||||
(keyboard.isControlPressed ? _controlModifierCode : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
class TerminalMouseDragReporter {
|
|
||||||
int? _pointerId;
|
|
||||||
TerminalController? _controller;
|
|
||||||
late CellOffset _lastReportedPosition;
|
|
||||||
var _ownsControllerSuspension = false;
|
|
||||||
var _releasePending = false;
|
|
||||||
var _reporting = false;
|
|
||||||
|
|
||||||
bool handleDown(
|
|
||||||
PointerDownEvent event,
|
|
||||||
Terminal terminal,
|
|
||||||
TerminalViewState? terminalView,
|
|
||||||
) {
|
|
||||||
if (!_isPrimaryMouse(event) || !_reportsDrag(terminal.mouseMode)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (terminalView == null || terminalView.widget.readOnly) return false;
|
|
||||||
final controller = terminalView.widget.controller;
|
|
||||||
if (controller == null ||
|
|
||||||
controller.suspendedPointerInputs ||
|
|
||||||
!controller.pointerInput.inputs.contains(PointerInput.tap)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel();
|
|
||||||
_pointerId = event.pointer;
|
|
||||||
_controller = controller;
|
|
||||||
_ownsControllerSuspension = true;
|
|
||||||
_releasePending = true;
|
|
||||||
_reporting = true;
|
|
||||||
controller.setSuspendPointerInput(true);
|
|
||||||
_clearSelection(controller);
|
|
||||||
final position = _cellAt(event, terminalView);
|
|
||||||
_lastReportedPosition = position;
|
|
||||||
terminal.textInput(
|
|
||||||
_report(terminal.mouseReportMode, position),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool handleMove(
|
|
||||||
PointerMoveEvent event,
|
|
||||||
Terminal terminal,
|
|
||||||
TerminalViewState? terminalView,
|
|
||||||
) {
|
|
||||||
if (event.pointer != _pointerId) return false;
|
|
||||||
if (terminalView == null) {
|
|
||||||
cancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final reportsDrag = _reportsDrag(terminal.mouseMode);
|
|
||||||
if (!_isPrimaryMouse(event)) {
|
|
||||||
if (_releasePending && reportsDrag) {
|
|
||||||
_reportRelease(
|
|
||||||
terminal,
|
|
||||||
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
cancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!_reporting || !reportsDrag) {
|
|
||||||
if (!reportsDrag) _releasePending = false;
|
|
||||||
_reporting = false;
|
|
||||||
// Keep ownership until the matching end event to suppress local selection.
|
|
||||||
final controller = _controller;
|
|
||||||
scheduleMicrotask(() => _clearSelection(controller));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final position = _cellAt(event, terminalView);
|
|
||||||
_lastReportedPosition = position;
|
|
||||||
terminal.textInput(
|
|
||||||
_report(terminal.mouseReportMode, position, motion: true),
|
|
||||||
);
|
|
||||||
final controller = _controller;
|
|
||||||
scheduleMicrotask(() => _clearSelection(controller));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool handleEnd(
|
|
||||||
PointerEvent event,
|
|
||||||
Terminal terminal,
|
|
||||||
TerminalViewState? terminalView,
|
|
||||||
) {
|
|
||||||
if (event.pointer != _pointerId) return false;
|
|
||||||
if (terminalView != null &&
|
|
||||||
_releasePending &&
|
|
||||||
_reportsDrag(terminal.mouseMode)) {
|
|
||||||
_reportRelease(
|
|
||||||
terminal,
|
|
||||||
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_clearSelection(_controller);
|
|
||||||
final controller = _controller;
|
|
||||||
_pointerId = null;
|
|
||||||
// Keep xterm's tap recognizer suspended for this pointer event.
|
|
||||||
scheduleMicrotask(() {
|
|
||||||
if (_pointerId == null && identical(_controller, controller)) {
|
|
||||||
_clearSelection(controller);
|
|
||||||
cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancel() {
|
|
||||||
final controller = _controller;
|
|
||||||
if (_ownsControllerSuspension) {
|
|
||||||
controller?.setSuspendPointerInput(false);
|
|
||||||
}
|
|
||||||
_pointerId = null;
|
|
||||||
_controller = null;
|
|
||||||
_ownsControllerSuspension = false;
|
|
||||||
_releasePending = false;
|
|
||||||
_reporting = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateController(TerminalController controller) {
|
|
||||||
final oldController = _controller;
|
|
||||||
if (_pointerId == null || oldController == null) {
|
|
||||||
cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (identical(oldController, controller)) return;
|
|
||||||
if (_ownsControllerSuspension) {
|
|
||||||
oldController.setSuspendPointerInput(false);
|
|
||||||
}
|
|
||||||
final acceptsPointerInput = !controller.suspendedPointerInputs &&
|
|
||||||
controller.pointerInput.inputs.contains(PointerInput.tap);
|
|
||||||
_controller = controller;
|
|
||||||
_ownsControllerSuspension = acceptsPointerInput;
|
|
||||||
_reporting = _reporting && acceptsPointerInput;
|
|
||||||
if (_ownsControllerSuspension) controller.setSuspendPointerInput(true);
|
|
||||||
_clearSelection(controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _reportRelease(Terminal terminal, CellOffset position) {
|
|
||||||
terminal.textInput(
|
|
||||||
_report(
|
|
||||||
terminal.mouseReportMode,
|
|
||||||
position,
|
|
||||||
release: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) {
|
|
||||||
final renderTerminal = terminalView.renderTerminal;
|
|
||||||
return renderTerminal.getCellOffset(
|
|
||||||
renderTerminal.globalToLocal(event.position),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isPrimaryMouse(PointerEvent event) =>
|
|
||||||
event.kind == PointerDeviceKind.mouse &&
|
|
||||||
(event.buttons & kPrimaryMouseButton) == kPrimaryMouseButton;
|
|
||||||
|
|
||||||
bool _reportsDrag(MouseMode mode) =>
|
|
||||||
mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove;
|
|
||||||
|
|
||||||
void _clearSelection(TerminalController? controller) {
|
|
||||||
if (controller == null || controller.selection == null) return;
|
|
||||||
controller.clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
String _report(
|
|
||||||
MouseReportMode mode,
|
|
||||||
CellOffset position, {
|
|
||||||
bool release = false,
|
|
||||||
bool motion = false,
|
|
||||||
}) {
|
|
||||||
final baseButton = motion ? _motionButtonCode : _leftButtonCode;
|
|
||||||
final button = baseButton | _activeModifierCode();
|
|
||||||
return encodeTerminalMouseReport(mode, button, position, release: release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,10 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
import 'terminal_copy_shortcut.dart';
|
|
||||||
import 'terminal_mouse_drag_reporter.dart';
|
|
||||||
|
|
||||||
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
|
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
|
||||||
/// modifier, so strict full-screen apps ignore the report and never scroll.
|
/// modifier, so strict full-screen apps ignore the report and never scroll.
|
||||||
/// Upstream fix: TerminalStudio/xterm.dart#238.
|
/// Upstream fix: TerminalStudio/xterm.dart#238.
|
||||||
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
||||||
const WheelButtonFixMouseHandler({this.positionProvider});
|
const WheelButtonFixMouseHandler();
|
||||||
|
|
||||||
final CellOffset? Function()? positionProvider;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? call(TerminalMouseEvent event) {
|
String? call(TerminalMouseEvent event) {
|
||||||
@@ -32,270 +23,20 @@ class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
|||||||
String _reportWheel(TerminalMouseEvent event) {
|
String _reportWheel(TerminalMouseEvent event) {
|
||||||
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
|
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
|
||||||
final button = event.button.id - 4;
|
final button = event.button.id - 4;
|
||||||
final position = positionProvider?.call() ?? event.position;
|
final x = event.position.x + 1;
|
||||||
return encodeTerminalMouseReport(
|
final y = event.position.y + 1;
|
||||||
event.state.mouseReportMode,
|
switch (event.state.mouseReportMode) {
|
||||||
button,
|
case MouseReportMode.normal:
|
||||||
position,
|
case MouseReportMode.utf:
|
||||||
);
|
final limit =
|
||||||
}
|
event.state.mouseReportMode == MouseReportMode.normal ? 223 : 2015;
|
||||||
}
|
final col = x > limit ? '\x00' : String.fromCharCode(32 + x);
|
||||||
|
final row = y > limit ? '\x00' : String.fromCharCode(32 + y);
|
||||||
class TerminalMouseInteraction extends StatefulWidget {
|
return '\x1b[M${String.fromCharCode(32 + button)}$col$row';
|
||||||
const TerminalMouseInteraction(
|
case MouseReportMode.sgr:
|
||||||
this.terminal, {
|
return '\x1b[<$button;$x;${y}M';
|
||||||
super.key,
|
case MouseReportMode.urxvt:
|
||||||
required this.controller,
|
return '\x1b[${32 + button};$x;${y}M';
|
||||||
this.focusNode,
|
}
|
||||||
this.backgroundOpacity = 1,
|
|
||||||
this.padding,
|
|
||||||
this.onSecondaryTapDown,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Terminal terminal;
|
|
||||||
final TerminalController controller;
|
|
||||||
final FocusNode? focusNode;
|
|
||||||
final double backgroundOpacity;
|
|
||||||
final EdgeInsets? padding;
|
|
||||||
final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TerminalMouseInteraction> createState() =>
|
|
||||||
_TerminalMouseInteractionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
|
|
||||||
static const _selectionScrollInterval = Duration(milliseconds: 50);
|
|
||||||
static const _noScroll = 0;
|
|
||||||
static const _scrollUp = -1;
|
|
||||||
static const _scrollDown = 1;
|
|
||||||
|
|
||||||
final _terminalViewKey = GlobalKey<TerminalViewState>();
|
|
||||||
final _scrollController = ScrollController();
|
|
||||||
final _mouseDrag = TerminalMouseDragReporter();
|
|
||||||
late final WheelButtonFixMouseHandler _mouseHandler;
|
|
||||||
TerminalMouseHandler? _previousMouseHandler;
|
|
||||||
Offset? _pointerPosition;
|
|
||||||
Offset? _selectionPointer;
|
|
||||||
CellAnchor? _selectionBase;
|
|
||||||
Buffer? _selectionBuffer;
|
|
||||||
int? _selectionPointerId;
|
|
||||||
Timer? _selectionScrollTimer;
|
|
||||||
var _selectionHasScrolled = false;
|
|
||||||
var _scrollDirection = _noScroll;
|
|
||||||
TerminalViewState? get _terminalView => _terminalViewKey.currentState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_mouseHandler = WheelButtonFixMouseHandler(
|
|
||||||
positionProvider: _cellAtPointer,
|
|
||||||
);
|
|
||||||
_installMouseHandler(widget.terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(TerminalMouseInteraction oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
final terminalChanged = !identical(oldWidget.terminal, widget.terminal);
|
|
||||||
final controllerChanged =
|
|
||||||
!identical(oldWidget.controller, widget.controller);
|
|
||||||
if (!terminalChanged && !controllerChanged) return;
|
|
||||||
if (controllerChanged && !terminalChanged) {
|
|
||||||
_mouseDrag.updateController(widget.controller);
|
|
||||||
} else {
|
|
||||||
_mouseDrag.cancel();
|
|
||||||
}
|
|
||||||
_clearSelectionDrag();
|
|
||||||
if (!terminalChanged) return;
|
|
||||||
_restoreMouseHandler(oldWidget.terminal);
|
|
||||||
_installMouseHandler(widget.terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _installMouseHandler(Terminal terminal) {
|
|
||||||
_previousMouseHandler = terminal.mouseHandler;
|
|
||||||
terminal.mouseHandler = _mouseHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _restoreMouseHandler(Terminal terminal) {
|
|
||||||
if (identical(terminal.mouseHandler, _mouseHandler)) {
|
|
||||||
terminal.mouseHandler = _previousMouseHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CellOffset? _cellAtPointer() {
|
|
||||||
final terminalView = _terminalView;
|
|
||||||
final pointerPosition = _pointerPosition;
|
|
||||||
if (terminalView == null || pointerPosition == null) return null;
|
|
||||||
final renderTerminal = terminalView.renderTerminal;
|
|
||||||
return renderTerminal.getCellOffset(
|
|
||||||
renderTerminal.globalToLocal(pointerPosition),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updatePointerPosition(PointerEvent event) =>
|
|
||||||
_pointerPosition = event.position;
|
|
||||||
|
|
||||||
void _handlePointerDown(PointerDownEvent event) {
|
|
||||||
_updatePointerPosition(event);
|
|
||||||
if (_mouseDrag.handleDown(event, widget.terminal, _terminalView)) {
|
|
||||||
_clearSelectionDrag();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.kind != PointerDeviceKind.mouse ||
|
|
||||||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_clearSelectionDrag();
|
|
||||||
final terminalView = _terminalView;
|
|
||||||
if (terminalView == null) return;
|
|
||||||
final renderTerminal = terminalView.renderTerminal;
|
|
||||||
final localPosition = renderTerminal.globalToLocal(event.position);
|
|
||||||
final selectionBuffer = widget.terminal.buffer;
|
|
||||||
_selectionPointerId = event.pointer;
|
|
||||||
_selectionBase = selectionBuffer.createAnchorFromOffset(
|
|
||||||
renderTerminal.getCellOffset(localPosition),
|
|
||||||
);
|
|
||||||
_selectionBuffer = selectionBuffer;
|
|
||||||
_selectionPointer = localPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePointerMove(PointerMoveEvent event) {
|
|
||||||
_updatePointerPosition(event);
|
|
||||||
if (_mouseDrag.handleMove(event, widget.terminal, _terminalView)) return;
|
|
||||||
if (event.pointer != _selectionPointerId) return;
|
|
||||||
if (event.kind != PointerDeviceKind.mouse ||
|
|
||||||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
|
|
||||||
_clearSelectionDrag();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final terminalView = _terminalView;
|
|
||||||
if (terminalView == null || _selectionBase == null) return;
|
|
||||||
final renderTerminal = terminalView.renderTerminal;
|
|
||||||
final localPosition = renderTerminal.globalToLocal(event.position);
|
|
||||||
_selectionPointer = localPosition;
|
|
||||||
_setScrollDirection(
|
|
||||||
_directionFor(localPosition, renderTerminal.paintBounds),
|
|
||||||
);
|
|
||||||
if (_selectionHasScrolled) {
|
|
||||||
scheduleMicrotask(() => _scrollSelection(scroll: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int _directionFor(Offset position, Rect bounds) {
|
|
||||||
if (position.dy < bounds.top) return _scrollUp;
|
|
||||||
if (position.dy >= bounds.bottom) return _scrollDown;
|
|
||||||
return _noScroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setScrollDirection(int direction) {
|
|
||||||
if (_scrollDirection == direction) return;
|
|
||||||
_stopAutoScroll();
|
|
||||||
_scrollDirection = direction;
|
|
||||||
if (direction == _noScroll) return;
|
|
||||||
_scrollSelection();
|
|
||||||
if (_scrollDirection != _noScroll) {
|
|
||||||
_selectionScrollTimer = Timer.periodic(
|
|
||||||
_selectionScrollInterval,
|
|
||||||
(_) => _scrollSelection(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scrollSelection({bool scroll = true}) {
|
|
||||||
final terminalView = _terminalView;
|
|
||||||
final selectionBase = _selectionBase;
|
|
||||||
final selectionBuffer = _selectionBuffer;
|
|
||||||
final selectionPointer = _selectionPointer;
|
|
||||||
if (terminalView == null ||
|
|
||||||
selectionBase == null ||
|
|
||||||
selectionBuffer == null ||
|
|
||||||
selectionPointer == null ||
|
|
||||||
!_scrollController.hasClients) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!identical(selectionBuffer, widget.terminal.buffer) ||
|
|
||||||
!selectionBase.attached) {
|
|
||||||
_clearSelectionDrag();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final renderTerminal = terminalView.renderTerminal;
|
|
||||||
if (scroll) {
|
|
||||||
final position = _scrollController.position;
|
|
||||||
final target =
|
|
||||||
(position.pixels + renderTerminal.lineHeight * _scrollDirection)
|
|
||||||
.clamp(position.minScrollExtent, position.maxScrollExtent)
|
|
||||||
.toDouble();
|
|
||||||
if (target == position.pixels) {
|
|
||||||
_stopAutoScroll();
|
|
||||||
} else {
|
|
||||||
position.jumpTo(target);
|
|
||||||
_selectionHasScrolled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderTerminal.selectCharacters(
|
|
||||||
renderTerminal.getOffset(selectionBase.offset),
|
|
||||||
selectionPointer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePointerEnd(PointerEvent event) {
|
|
||||||
_updatePointerPosition(event);
|
|
||||||
if (!_mouseDrag.handleEnd(event, widget.terminal, _terminalView) &&
|
|
||||||
event.pointer != _selectionPointerId) return;
|
|
||||||
if (_selectionHasScrolled) _scrollSelection(scroll: false);
|
|
||||||
_clearSelectionDrag();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _clearSelectionDrag() {
|
|
||||||
_selectionPointerId = null;
|
|
||||||
_selectionBase?.dispose();
|
|
||||||
_selectionBase = null;
|
|
||||||
_selectionBuffer = null;
|
|
||||||
_selectionPointer = null;
|
|
||||||
_selectionHasScrolled = false;
|
|
||||||
_stopAutoScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopAutoScroll() {
|
|
||||||
_selectionScrollTimer?.cancel();
|
|
||||||
_selectionScrollTimer = null;
|
|
||||||
_scrollDirection = _noScroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_mouseDrag.cancel();
|
|
||||||
_clearSelectionDrag();
|
|
||||||
_restoreMouseHandler(widget.terminal);
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Listener(
|
|
||||||
onPointerDown: _handlePointerDown,
|
|
||||||
onPointerMove: _handlePointerMove,
|
|
||||||
onPointerUp: _handlePointerEnd,
|
|
||||||
onPointerHover: _updatePointerPosition,
|
|
||||||
onPointerCancel: _handlePointerEnd,
|
|
||||||
onPointerSignal: _updatePointerPosition,
|
|
||||||
onPointerPanZoomStart: _updatePointerPosition,
|
|
||||||
onPointerPanZoomUpdate: _updatePointerPosition,
|
|
||||||
onPointerPanZoomEnd: _updatePointerPosition,
|
|
||||||
child: TerminalView(
|
|
||||||
widget.terminal,
|
|
||||||
key: _terminalViewKey,
|
|
||||||
controller: widget.controller,
|
|
||||||
scrollController: _scrollController,
|
|
||||||
focusNode: widget.focusNode,
|
|
||||||
backgroundOpacity: widget.backgroundOpacity,
|
|
||||||
padding: widget.padding,
|
|
||||||
shortcuts: platformTerminalShortcuts(),
|
|
||||||
onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller),
|
|
||||||
onSecondaryTapDown: widget.onSecondaryTapDown,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ class PlatformFFI {
|
|||||||
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
_ffiBind.sessionRegisterGpuTexture(
|
_ffiBind.sessionRegisterGpuTexture(
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
|
_ffiBind.sessionUnregisterPixelbufferTexture(
|
||||||
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
|
_ffiBind.sessionUnregisterGpuTexture(
|
||||||
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
|
||||||
Future<void> init(String appType) async {
|
Future<void> init(String appType) async {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
@@ -251,11 +257,6 @@ class PlatformFFI {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T?> invokeMethodWithResult<T>(String method,
|
|
||||||
[dynamic arguments]) async {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// just for compilation
|
// just for compilation
|
||||||
void syncAndroidServiceAppDirConfigPath() {}
|
void syncAndroidServiceAppDirConfigPath() {}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ final isWebDesktop_ = false;
|
|||||||
|
|
||||||
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||||
|
|
||||||
|
String get screenInfo_ => '';
|
||||||
|
|
||||||
final isWebOnWindows_ = false;
|
final isWebOnWindows_ = false;
|
||||||
final isWebOnLinux_ = false;
|
final isWebOnLinux_ = false;
|
||||||
final isWebOnMacOS_ = false;
|
final isWebOnMacOS_ = false;
|
||||||
|
|||||||
42
flutter/lib/plugin/common.dart
Normal file
42
flutter/lib/plugin/common.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
typedef PluginId = String;
|
||||||
|
|
||||||
|
// ui location
|
||||||
|
const String kLocationHostMainPlugin = 'host|main|settings|plugin';
|
||||||
|
const String kLocationClientRemoteToolbarDisplay =
|
||||||
|
'client|remote|toolbar|display';
|
||||||
|
|
||||||
|
class MsgFromUi {
|
||||||
|
String id;
|
||||||
|
String name;
|
||||||
|
String location;
|
||||||
|
String key;
|
||||||
|
String value;
|
||||||
|
String action;
|
||||||
|
|
||||||
|
MsgFromUi({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.location,
|
||||||
|
required this.key,
|
||||||
|
required this.value,
|
||||||
|
required this.action,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'location': location,
|
||||||
|
'key': key,
|
||||||
|
'value': value,
|
||||||
|
'action': action,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return jsonEncode(toJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
18
flutter/lib/plugin/event.dart
Normal file
18
flutter/lib/plugin/event.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void handlePluginEvent(
|
||||||
|
Map<String, dynamic> evt,
|
||||||
|
Function(Map<String, dynamic> e) handleMsgBox,
|
||||||
|
) {
|
||||||
|
Map<String, dynamic>? content;
|
||||||
|
try {
|
||||||
|
content = json.decode(evt['content']);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
'Json decode plugin event content failed: $e, ${evt['content']}');
|
||||||
|
}
|
||||||
|
if (content?['t'] == 'MsgBox') {
|
||||||
|
handleMsgBox(content?['c']);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
flutter/lib/plugin/handlers.dart
Normal file
79
flutter/lib/plugin/handlers.dart
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/utils/dialogs.dart';
|
||||||
|
|
||||||
|
abstract class NativeHandler {
|
||||||
|
bool onEvent(Map<String, dynamic> evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef OnSelectPeersCallback = Bool Function(Int returnCode,
|
||||||
|
Pointer<Void> data, Uint64 dataLength, Pointer<Void> userData);
|
||||||
|
typedef OnSelectPeersCallbackDart = bool Function(
|
||||||
|
int returnCode, Pointer<Void> data, int dataLength, Pointer<Void> userData);
|
||||||
|
|
||||||
|
class NativeUiHandler extends NativeHandler {
|
||||||
|
NativeUiHandler._();
|
||||||
|
|
||||||
|
static NativeUiHandler instance = NativeUiHandler._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onEvent(Map<String, dynamic> evt) {
|
||||||
|
final name = evt['name'];
|
||||||
|
final action = evt['action'];
|
||||||
|
if (name != "native_ui") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (action) {
|
||||||
|
case "select_peers":
|
||||||
|
int cb = evt['cb'];
|
||||||
|
int userData = evt['user_data'] ?? 0;
|
||||||
|
final cbFuncNative = Pointer.fromAddress(cb)
|
||||||
|
.cast<NativeFunction<OnSelectPeersCallback>>();
|
||||||
|
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
|
||||||
|
onSelectPeers(cbFuncDart, userData);
|
||||||
|
break;
|
||||||
|
case "register_ui_entry":
|
||||||
|
int cb = evt['on_tap_cb'];
|
||||||
|
int userData = evt['user_data'] ?? 0;
|
||||||
|
String title = evt['title'] ?? "";
|
||||||
|
final cbFuncNative = Pointer.fromAddress(cb)
|
||||||
|
.cast<NativeFunction<OnSelectPeersCallback>>();
|
||||||
|
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
|
||||||
|
onRegisterUiEntry(title, cbFuncDart, userData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSelectPeers(OnSelectPeersCallbackDart cb, int userData) async {
|
||||||
|
showPeerSelectionDialog(onPeersCallback: (peers) {
|
||||||
|
String json = jsonEncode(<String, dynamic> {
|
||||||
|
"peers": peers
|
||||||
|
});
|
||||||
|
final native = json.toNativeUtf8();
|
||||||
|
cb(0, native.cast(), native.length, Pointer.fromAddress(userData));
|
||||||
|
malloc.free(native);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRegisterUiEntry(String title, OnSelectPeersCallbackDart cbFuncDart, int userData) {
|
||||||
|
Widget widget = InkWell(
|
||||||
|
child: Container(
|
||||||
|
height: 25.0,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(title)),
|
||||||
|
Icon(Icons.chevron_right_rounded, size: 12.0,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
PluginUiManager.instance.registerEntry(title, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
319
flutter/lib/plugin/manager.dart
Normal file
319
flutter/lib/plugin/manager.dart
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
// The plugin manager is a singleton class that manages the plugins.
|
||||||
|
// 1. It merge metadata and the desc of plugins.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const String kValueTrue = '1';
|
||||||
|
const String kValueFalse = '0';
|
||||||
|
|
||||||
|
class ConfigItem {
|
||||||
|
String key;
|
||||||
|
String description;
|
||||||
|
String defaultValue;
|
||||||
|
|
||||||
|
ConfigItem(this.key, this.defaultValue, this.description);
|
||||||
|
ConfigItem.fromJson(Map<String, dynamic> json)
|
||||||
|
: key = json['key'] ?? '',
|
||||||
|
description = json['description'] ?? '',
|
||||||
|
defaultValue = json['default'] ?? '';
|
||||||
|
|
||||||
|
static String get trueValue => kValueTrue;
|
||||||
|
static String get falseValue => kValueFalse;
|
||||||
|
static bool isTrue(String value) => value == kValueTrue;
|
||||||
|
static bool isFalse(String value) => value == kValueFalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UiType {
|
||||||
|
String key;
|
||||||
|
String text;
|
||||||
|
String tooltip;
|
||||||
|
String action;
|
||||||
|
|
||||||
|
UiType(this.key, this.text, this.tooltip, this.action);
|
||||||
|
|
||||||
|
UiType.fromJson(Map<String, dynamic> json)
|
||||||
|
: key = json['key'] ?? '',
|
||||||
|
text = json['text'] ?? '',
|
||||||
|
tooltip = json['tooltip'] ?? '',
|
||||||
|
action = json['action'] ?? '';
|
||||||
|
|
||||||
|
static UiType? create(Map<String, dynamic> json) {
|
||||||
|
if (json['t'] == 'Button') {
|
||||||
|
return UiButton.fromJson(json['c']);
|
||||||
|
} else if (json['t'] == 'Checkbox') {
|
||||||
|
return UiCheckbox.fromJson(json['c']);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UiButton extends UiType {
|
||||||
|
String icon;
|
||||||
|
|
||||||
|
UiButton(
|
||||||
|
{required String key,
|
||||||
|
required String text,
|
||||||
|
required this.icon,
|
||||||
|
required String tooltip,
|
||||||
|
required String action})
|
||||||
|
: super(key, text, tooltip, action);
|
||||||
|
|
||||||
|
UiButton.fromJson(Map<String, dynamic> json)
|
||||||
|
: icon = json['icon'] ?? '',
|
||||||
|
super.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
class UiCheckbox extends UiType {
|
||||||
|
UiCheckbox(
|
||||||
|
{required String key,
|
||||||
|
required String text,
|
||||||
|
required String tooltip,
|
||||||
|
required String action})
|
||||||
|
: super(key, text, tooltip, action);
|
||||||
|
|
||||||
|
UiCheckbox.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Location {
|
||||||
|
// location key:
|
||||||
|
// host|main|settings|plugin
|
||||||
|
// client|remote|toolbar|display
|
||||||
|
HashMap<String, UiType> ui;
|
||||||
|
|
||||||
|
Location(this.ui);
|
||||||
|
Location.fromJson(Map<String, dynamic> json) : ui = HashMap() {
|
||||||
|
(json['ui'] as Map<String, dynamic>).forEach((key, value) {
|
||||||
|
var ui = UiType.create(value);
|
||||||
|
if (ui != null) {
|
||||||
|
this.ui[ui.key] = ui;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PublishInfo {
|
||||||
|
PublishInfo({
|
||||||
|
required this.lastReleased,
|
||||||
|
required this.published,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime lastReleased;
|
||||||
|
final DateTime published;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta {
|
||||||
|
Meta({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.version,
|
||||||
|
required this.description,
|
||||||
|
required this.author,
|
||||||
|
required this.home,
|
||||||
|
required this.license,
|
||||||
|
required this.publishInfo,
|
||||||
|
required this.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String version;
|
||||||
|
final String description;
|
||||||
|
final String author;
|
||||||
|
final String home;
|
||||||
|
final String license;
|
||||||
|
final PublishInfo publishInfo;
|
||||||
|
final String source;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceInfo {
|
||||||
|
String name; // 1. RustDesk github 2. Local
|
||||||
|
String url;
|
||||||
|
String description;
|
||||||
|
|
||||||
|
SourceInfo({
|
||||||
|
required this.name,
|
||||||
|
required this.url,
|
||||||
|
required this.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginInfo with ChangeNotifier {
|
||||||
|
SourceInfo sourceInfo;
|
||||||
|
Meta meta;
|
||||||
|
String installedVersion; // It is empty if not installed.
|
||||||
|
String failedMsg;
|
||||||
|
String invalidReason; // It is empty if valid.
|
||||||
|
|
||||||
|
PluginInfo({
|
||||||
|
required this.sourceInfo,
|
||||||
|
required this.meta,
|
||||||
|
required this.installedVersion,
|
||||||
|
required this.invalidReason,
|
||||||
|
this.failedMsg = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get installed => installedVersion.isNotEmpty;
|
||||||
|
bool get needUpdate => installed && installedVersion != meta.version;
|
||||||
|
|
||||||
|
void setInstall(String msg) {
|
||||||
|
if (msg == "finished") {
|
||||||
|
msg = '';
|
||||||
|
}
|
||||||
|
failedMsg = msg;
|
||||||
|
if (msg.isEmpty) {
|
||||||
|
installedVersion = meta.version;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUninstall(String msg) {
|
||||||
|
failedMsg = msg;
|
||||||
|
if (msg.isEmpty) {
|
||||||
|
installedVersion = '';
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginManager with ChangeNotifier {
|
||||||
|
String failedReason = ''; // The reason of failed to load plugins.
|
||||||
|
final List<PluginInfo> _plugins = [];
|
||||||
|
|
||||||
|
PluginManager._();
|
||||||
|
static final PluginManager _instance = PluginManager._();
|
||||||
|
static PluginManager get instance => _instance;
|
||||||
|
|
||||||
|
List<PluginInfo> get plugins => _plugins;
|
||||||
|
|
||||||
|
PluginInfo? getPlugin(String id) {
|
||||||
|
for (var p in _plugins) {
|
||||||
|
if (p.meta.id == id) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleEvent(Map<String, dynamic> evt) {
|
||||||
|
if (evt['plugin_list'] != null) {
|
||||||
|
_handlePluginList(evt['plugin_list']);
|
||||||
|
} else if (evt['plugin_install'] != null && evt['id'] != null) {
|
||||||
|
_handlePluginInstall(evt['id'], evt['plugin_install']);
|
||||||
|
} else if (evt['plugin_uninstall'] != null && evt['id'] != null) {
|
||||||
|
_handlePluginUninstall(evt['id'], evt['plugin_uninstall']);
|
||||||
|
} else {
|
||||||
|
debugPrint('Failed to handle manager event: $evt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sortPlugins() {
|
||||||
|
plugins.sort((a, b) {
|
||||||
|
if (a.installed) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.installed) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePluginList(String pluginList) {
|
||||||
|
_plugins.clear();
|
||||||
|
try {
|
||||||
|
for (var p in json.decode(pluginList) as List<dynamic>) {
|
||||||
|
final plugin = _getPluginFromEvent(p);
|
||||||
|
if (plugin == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_plugins.add(plugin);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to decode $e, plugin list \'$pluginList\'');
|
||||||
|
}
|
||||||
|
_sortPlugins();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePluginInstall(String id, String msg) {
|
||||||
|
debugPrint('Plugin \'$id\' install msg $msg');
|
||||||
|
for (var i = 0; i < _plugins.length; i++) {
|
||||||
|
if (_plugins[i].meta.id == id) {
|
||||||
|
_plugins[i].setInstall(msg);
|
||||||
|
_sortPlugins();
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePluginUninstall(String id, String msg) {
|
||||||
|
debugPrint('Plugin \'$id\' uninstall msg $msg');
|
||||||
|
for (var i = 0; i < _plugins.length; i++) {
|
||||||
|
if (_plugins[i].meta.id == id) {
|
||||||
|
_plugins[i].setUninstall(msg);
|
||||||
|
_sortPlugins();
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginInfo? _getPluginFromEvent(Map<String, dynamic> evt) {
|
||||||
|
final s = evt['source'];
|
||||||
|
assert(s != null, 'Source is null');
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final source = SourceInfo(
|
||||||
|
name: s['name'],
|
||||||
|
url: s['url'] ?? '',
|
||||||
|
description: s['description'] ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
final m = evt['meta'];
|
||||||
|
assert(m != null, 'Meta is null');
|
||||||
|
if (m == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
late DateTime lastReleased;
|
||||||
|
late DateTime published;
|
||||||
|
try {
|
||||||
|
lastReleased = DateTime.parse(
|
||||||
|
m['publish_info']?['last_released'] ?? '1970-01-01T00+00:00');
|
||||||
|
} catch (e) {
|
||||||
|
lastReleased = DateTime.utc(1970);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
published = DateTime.parse(
|
||||||
|
m['publish_info']?['published'] ?? '1970-01-01T00+00:00');
|
||||||
|
} catch (e) {
|
||||||
|
published = DateTime.utc(1970);
|
||||||
|
}
|
||||||
|
|
||||||
|
final meta = Meta(
|
||||||
|
id: m['id'],
|
||||||
|
name: m['name'],
|
||||||
|
version: m['version'],
|
||||||
|
description: m['description'] ?? '',
|
||||||
|
author: m['author'],
|
||||||
|
home: m['home'] ?? '',
|
||||||
|
license: m['license'] ?? '',
|
||||||
|
source: m['source'] ?? '',
|
||||||
|
publishInfo:
|
||||||
|
PublishInfo(lastReleased: lastReleased, published: published),
|
||||||
|
);
|
||||||
|
return PluginInfo(
|
||||||
|
sourceInfo: source,
|
||||||
|
meta: meta,
|
||||||
|
installedVersion: evt['installed_version'],
|
||||||
|
invalidReason: evt['invalid_reason'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginManager get pluginManager => PluginManager.instance;
|
||||||
110
flutter/lib/plugin/model.dart
Normal file
110
flutter/lib/plugin/model.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import './common.dart';
|
||||||
|
import './manager.dart';
|
||||||
|
|
||||||
|
final Map<String, LocationModel> _locationModels = {};
|
||||||
|
final Map<String, OptionModel> _optionModels = {};
|
||||||
|
|
||||||
|
class OptionModel with ChangeNotifier {
|
||||||
|
String? v;
|
||||||
|
|
||||||
|
String? get value => v;
|
||||||
|
set value(String? v) {
|
||||||
|
this.v = v;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String key(String location, PluginId id, String peer, String k) =>
|
||||||
|
'$location|$id|$peer|$k';
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginModel with ChangeNotifier {
|
||||||
|
final List<UiType> uiList = [];
|
||||||
|
final Map<String, String> opts = {};
|
||||||
|
|
||||||
|
void add(List<UiType> uiList) {
|
||||||
|
bool found = false;
|
||||||
|
for (var ui in uiList) {
|
||||||
|
for (int i = 0; i < this.uiList.length; i++) {
|
||||||
|
if (this.uiList[i].key == ui.key) {
|
||||||
|
this.uiList[i] = ui;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
this.uiList.add(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getOpt(String key) => opts.remove(key);
|
||||||
|
|
||||||
|
bool get isEmpty => uiList.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationModel with ChangeNotifier {
|
||||||
|
final Map<PluginId, PluginModel> pluginModels = {};
|
||||||
|
|
||||||
|
void add(PluginId id, List<UiType> uiList) {
|
||||||
|
if (pluginModels[id] != null) {
|
||||||
|
pluginModels[id]!.add(uiList);
|
||||||
|
} else {
|
||||||
|
var model = PluginModel();
|
||||||
|
model.add(uiList);
|
||||||
|
pluginModels[id] = model;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
pluginModels.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(PluginId id) {
|
||||||
|
pluginModels.remove(id);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isEmpty => pluginModels.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addLocationUi(String location, PluginId id, List<UiType> uiList) {
|
||||||
|
if (_locationModels[location] == null) {
|
||||||
|
_locationModels[location] = LocationModel();
|
||||||
|
}
|
||||||
|
_locationModels[location]?.add(id, uiList);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationModel? getLocationModel(String location) => _locationModels[location];
|
||||||
|
|
||||||
|
PluginModel? getPluginModel(String location, PluginId id) =>
|
||||||
|
_locationModels[location]?.pluginModels[id];
|
||||||
|
|
||||||
|
void clearPlugin(PluginId pluginId) {
|
||||||
|
for (var element in _locationModels.values) {
|
||||||
|
element.remove(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearLocations() {
|
||||||
|
for (var element in _locationModels.values) {
|
||||||
|
element.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionModel getOptionModel(
|
||||||
|
String location, PluginId pluginId, String peer, String key) {
|
||||||
|
final k = OptionModel.key(location, pluginId, peer, key);
|
||||||
|
if (_optionModels[k] == null) {
|
||||||
|
_optionModels[k] = OptionModel();
|
||||||
|
}
|
||||||
|
return _optionModels[k]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateOption(
|
||||||
|
String location, PluginId id, String peer, String key, String value) {
|
||||||
|
final k = OptionModel.key(location, id, peer, key);
|
||||||
|
_optionModels[k]?.value = value;
|
||||||
|
}
|
||||||
17
flutter/lib/plugin/ui_manager.dart
Normal file
17
flutter/lib/plugin/ui_manager.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class PluginUiManager {
|
||||||
|
PluginUiManager._();
|
||||||
|
|
||||||
|
static PluginUiManager instance = PluginUiManager._();
|
||||||
|
|
||||||
|
Map<String, Widget> entries = <String, Widget>{};
|
||||||
|
|
||||||
|
void registerEntry(String key, Widget widget) {
|
||||||
|
entries[key] = widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterEntry(String key) {
|
||||||
|
entries.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
flutter/lib/plugin/utils/dialogs.dart
Normal file
86
flutter/lib/plugin/utils/dialogs.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
|
||||||
|
void showPeerSelectionDialog(
|
||||||
|
{bool singleSelection = false,
|
||||||
|
required Function(List<String>) onPeersCallback}) async {
|
||||||
|
// load recent peers, we can directly use the peers in `gFFI.recentPeersModel`.
|
||||||
|
// The plugin is not used for now, so just left it empty here.
|
||||||
|
final peers = '';
|
||||||
|
if (peers.isEmpty) {
|
||||||
|
// debugPrint("load recent peers failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> map = jsonDecode(peers);
|
||||||
|
List<dynamic> peersList = map['peers'] ?? [];
|
||||||
|
final selected = List<String>.empty(growable: true);
|
||||||
|
|
||||||
|
submit() async {
|
||||||
|
onPeersCallback.call(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title:
|
||||||
|
Text(translate(singleSelection ? "Select peers" : "Select a peer")),
|
||||||
|
content: SizedBox(
|
||||||
|
height: 300.0,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final Map<String, dynamic> peer = peersList[index];
|
||||||
|
final String platform = peer['platform'] ?? "";
|
||||||
|
final String id = peer['id'] ?? "";
|
||||||
|
final String alias = peer['alias'] ?? "";
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (selected.contains(id)) {
|
||||||
|
selected.remove(id);
|
||||||
|
} else {
|
||||||
|
selected.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
key: ValueKey(index),
|
||||||
|
height: 50.0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).highlightColor,
|
||||||
|
borderRadius: BorderRadius.circular(12.0)),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||||
|
margin: EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
// platform
|
||||||
|
SizedBox(
|
||||||
|
width: 8.0,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
getPlatformImage(platform, size: 34.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 8.0,
|
||||||
|
),
|
||||||
|
// id/alias
|
||||||
|
Expanded(child: Text(alias.isEmpty ? id : alias)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: peersList.length,
|
||||||
|
itemExtent: 50.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmit: submit,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
301
flutter/lib/plugin/widgets/desc_ui.dart
Normal file
301
flutter/lib/plugin/widgets/desc_ui.dart
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
// to-do: do not depend on desktop
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
|
import '../manager.dart';
|
||||||
|
import '../model.dart';
|
||||||
|
import '../common.dart';
|
||||||
|
|
||||||
|
// dup to flutter\lib\desktop\pages\desktop_setting_page.dart
|
||||||
|
const double _kCheckBoxLeftMargin = 10;
|
||||||
|
|
||||||
|
class LocationItem extends StatelessWidget {
|
||||||
|
final String peerId;
|
||||||
|
final FFI ffi;
|
||||||
|
final String location;
|
||||||
|
final LocationModel locationModel;
|
||||||
|
final bool isMenu;
|
||||||
|
|
||||||
|
LocationItem({
|
||||||
|
Key? key,
|
||||||
|
required this.peerId,
|
||||||
|
required this.ffi,
|
||||||
|
required this.location,
|
||||||
|
required this.locationModel,
|
||||||
|
required this.isMenu,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
bool get isEmpty => locationModel.isEmpty;
|
||||||
|
|
||||||
|
static Widget createLocationItem(
|
||||||
|
String peerId, FFI ffi, String location, bool isMenu) {
|
||||||
|
final model = getLocationModel(location);
|
||||||
|
return model == null
|
||||||
|
? Container()
|
||||||
|
: LocationItem(
|
||||||
|
peerId: peerId,
|
||||||
|
ffi: ffi,
|
||||||
|
location: location,
|
||||||
|
locationModel: model,
|
||||||
|
isMenu: isMenu,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: locationModel,
|
||||||
|
child: Consumer<LocationModel>(builder: (context, model, child) {
|
||||||
|
return Column(
|
||||||
|
children: model.pluginModels.entries
|
||||||
|
.map((entry) => _buildPluginItem(entry.key, entry.value))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPluginItem(PluginId id, PluginModel model) => PluginItem(
|
||||||
|
pluginId: id,
|
||||||
|
peerId: peerId,
|
||||||
|
ffi: ffi,
|
||||||
|
location: location,
|
||||||
|
pluginModel: model,
|
||||||
|
isMenu: isMenu,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginItem extends StatelessWidget {
|
||||||
|
final PluginId pluginId;
|
||||||
|
final String peerId;
|
||||||
|
final FFI? ffi;
|
||||||
|
final String location;
|
||||||
|
final PluginModel pluginModel;
|
||||||
|
final bool isMenu;
|
||||||
|
|
||||||
|
PluginItem({
|
||||||
|
Key? key,
|
||||||
|
required this.pluginId,
|
||||||
|
required this.peerId,
|
||||||
|
this.ffi,
|
||||||
|
required this.location,
|
||||||
|
required this.pluginModel,
|
||||||
|
required this.isMenu,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
bool get isEmpty => pluginModel.isEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: pluginModel,
|
||||||
|
child: Consumer<PluginModel>(
|
||||||
|
builder: (context, pluginModel, child) {
|
||||||
|
return Column(
|
||||||
|
children: pluginModel.uiList.map((ui) => _buildItem(ui)).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(UiType ui) {
|
||||||
|
Widget? child;
|
||||||
|
switch (ui.runtimeType) {
|
||||||
|
case UiButton:
|
||||||
|
if (isMenu) {
|
||||||
|
if (ffi != null) {
|
||||||
|
child = _buildMenuButton(ui as UiButton, ffi!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
child = _buildButton(ui as UiButton);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UiCheckbox:
|
||||||
|
if (isMenu) {
|
||||||
|
if (ffi != null) {
|
||||||
|
child = _buildCheckboxMenuButton(ui as UiCheckbox, ffi!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
child = _buildCheckbox(ui as UiCheckbox);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// to-do: add plugin icon and tooltip
|
||||||
|
return child ?? Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButton(UiButton ui) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () => bind.pluginEvent(
|
||||||
|
id: pluginId,
|
||||||
|
peer: peerId,
|
||||||
|
event: _makeEvent(ui.key),
|
||||||
|
),
|
||||||
|
child: Text(ui.text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCheckbox(UiCheckbox ui) {
|
||||||
|
getChild(OptionModel model) {
|
||||||
|
final v = _getOption(model, ui.key);
|
||||||
|
if (v == null) {
|
||||||
|
// session or plugin not found
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChanged(bool value) {
|
||||||
|
bind.pluginEvent(
|
||||||
|
id: pluginId,
|
||||||
|
peer: peerId,
|
||||||
|
event: _makeEvent(ui.key, v: value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final value = ConfigItem.isTrue(v);
|
||||||
|
return GestureDetector(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: value,
|
||||||
|
onChanged: (_) => onChanged(!value),
|
||||||
|
).marginOnly(right: 5),
|
||||||
|
Expanded(
|
||||||
|
child: Text(translate(ui.text)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).marginOnly(left: _kCheckBoxLeftMargin),
|
||||||
|
onTap: () => onChanged(!value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: getOptionModel(location, pluginId, peerId, ui.key),
|
||||||
|
child: Consumer<OptionModel>(
|
||||||
|
builder: (context, model, child) => getChild(model),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCheckboxMenuButton(UiCheckbox ui, FFI ffi) {
|
||||||
|
getChild(OptionModel model) {
|
||||||
|
final v = _getOption(model, ui.key);
|
||||||
|
if (v == null) {
|
||||||
|
// session or plugin not found
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
return CkbMenuButton(
|
||||||
|
value: ConfigItem.isTrue(v),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) {
|
||||||
|
bind.pluginEvent(
|
||||||
|
id: pluginId,
|
||||||
|
peer: peerId,
|
||||||
|
event: _makeEvent(ui.key, v: v),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// to-do: RustDesk translate or plugin translate ?
|
||||||
|
child: Text(ui.text),
|
||||||
|
ffi: ffi,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: getOptionModel(location, pluginId, peerId, ui.key),
|
||||||
|
child: Consumer<OptionModel>(
|
||||||
|
builder: (context, model, child) => getChild(model),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuButton(UiButton ui, FFI ffi) {
|
||||||
|
return MenuButton(
|
||||||
|
onPressed: () => bind.pluginEvent(
|
||||||
|
id: pluginId,
|
||||||
|
peer: peerId,
|
||||||
|
event: _makeEvent(ui.key),
|
||||||
|
),
|
||||||
|
// to-do: support trailing icon, but it will cause tree shake error.
|
||||||
|
// ```
|
||||||
|
// This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations:
|
||||||
|
// Target release_macos_bundle_flutter_assets failed: Exception: Avoid non-constant invocations of IconData or try to build again with --no-tree-shake-icons.
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// trailingIcon: Icon(
|
||||||
|
// IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')),
|
||||||
|
//
|
||||||
|
// to-do: RustDesk translate or plugin translate ?
|
||||||
|
child: Text(ui.text),
|
||||||
|
ffi: ffi,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _makeEvent(
|
||||||
|
String key, {
|
||||||
|
bool? v,
|
||||||
|
}) {
|
||||||
|
final event = MsgFromUi(
|
||||||
|
id: pluginId,
|
||||||
|
name: pluginManager.getPlugin(pluginId)?.meta.name ?? '',
|
||||||
|
location: location,
|
||||||
|
key: key,
|
||||||
|
value:
|
||||||
|
v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '',
|
||||||
|
action: '',
|
||||||
|
);
|
||||||
|
return Uint8List.fromList(event.toString().codeUnits);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getOption(OptionModel model, String key) {
|
||||||
|
var v = model.value;
|
||||||
|
if (v == null) {
|
||||||
|
try {
|
||||||
|
if (peerId.isEmpty) {
|
||||||
|
v = bind.pluginGetSharedOption(id: pluginId, key: key);
|
||||||
|
} else {
|
||||||
|
v = bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to get option "$key", $e');
|
||||||
|
v = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleReloading(Map<String, dynamic> evt) {
|
||||||
|
if (evt['id'] == null || evt['location'] == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final uiList = <UiType>[];
|
||||||
|
for (var e in json.decode(evt['ui'] as String)) {
|
||||||
|
final ui = UiType.create(e);
|
||||||
|
if (ui != null) {
|
||||||
|
uiList.add(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uiList.isNotEmpty) {
|
||||||
|
addLocationUi(evt['location']!, evt['id']!, uiList);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed handleReloading, json decode of ui, $e ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleOption(Map<String, dynamic> evt) {
|
||||||
|
updateOption(
|
||||||
|
evt['location'], evt['id'], evt['peer'] ?? '', evt['key'], evt['value']);
|
||||||
|
}
|
||||||
202
flutter/lib/plugin/widgets/desktop_settings.dart
Normal file
202
flutter/lib/plugin/widgets/desktop_settings.dart
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/model.dart';
|
||||||
|
import 'package:flutter_hbb/plugin/common.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../manager.dart';
|
||||||
|
import './desc_ui.dart';
|
||||||
|
|
||||||
|
// to-do: use settings from desktop_setting_page.dart
|
||||||
|
const double _kCardFixedWidth = 540;
|
||||||
|
const double _kCardLeftMargin = 15;
|
||||||
|
const double _kContentHMargin = 15;
|
||||||
|
const double _kTitleFontSize = 20;
|
||||||
|
const double _kVersionFontSize = 12;
|
||||||
|
|
||||||
|
class DesktopSettingsCard extends StatefulWidget {
|
||||||
|
final PluginInfo plugin;
|
||||||
|
DesktopSettingsCard({
|
||||||
|
Key? key,
|
||||||
|
required this.plugin,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DesktopSettingsCard> createState() => _DesktopSettingsCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
|
||||||
|
PluginInfo get plugin => widget.plugin;
|
||||||
|
bool get installed => plugin.installed;
|
||||||
|
|
||||||
|
bool isEnabled = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
isEnabled = bind.pluginIsEnabled(id: plugin.meta.id);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: SizedBox(
|
||||||
|
width: _kCardFixedWidth,
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
header(),
|
||||||
|
body(),
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 10),
|
||||||
|
).marginOnly(left: _kCardLeftMargin, top: 15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget header() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
headerNameVersion(),
|
||||||
|
headerInstallEnable(),
|
||||||
|
],
|
||||||
|
).marginOnly(
|
||||||
|
left: _kContentHMargin,
|
||||||
|
top: 10,
|
||||||
|
bottom: 10,
|
||||||
|
right: _kContentHMargin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget headerNameVersion() {
|
||||||
|
return Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.plugin.meta.name,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: _kTitleFontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 5,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
plugin.meta.version,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: _kVersionFontSize,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget headerButton(String label, VoidCallback onPressed) {
|
||||||
|
return Container(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(translate(label)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget headerInstallEnable() {
|
||||||
|
final installButton = headerButton(
|
||||||
|
installed ? 'Uninstall' : 'Install',
|
||||||
|
() {
|
||||||
|
bind.pluginInstall(
|
||||||
|
id: plugin.meta.id,
|
||||||
|
b: !installed,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installed) {
|
||||||
|
final updateButton = plugin.needUpdate
|
||||||
|
? headerButton('Update', () {
|
||||||
|
bind.pluginInstall(
|
||||||
|
id: plugin.meta.id,
|
||||||
|
b: !installed,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: Container();
|
||||||
|
|
||||||
|
final enableButton = !installed
|
||||||
|
? Container()
|
||||||
|
: headerButton(isEnabled ? 'Disable' : 'Enable', () {
|
||||||
|
if (isEnabled) {
|
||||||
|
clearPlugin(plugin.meta.id);
|
||||||
|
}
|
||||||
|
bind.pluginEnable(id: plugin.meta.id, v: !isEnabled);
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
updateButton,
|
||||||
|
SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
installButton,
|
||||||
|
SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
enableButton,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return installButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget body() {
|
||||||
|
return Column(children: [
|
||||||
|
author(),
|
||||||
|
description(),
|
||||||
|
more(),
|
||||||
|
]).marginOnly(
|
||||||
|
left: _kCardLeftMargin,
|
||||||
|
top: 4,
|
||||||
|
right: _kContentHMargin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget author() {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(plugin.meta.author),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget description() {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(plugin.meta.description),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget more() {
|
||||||
|
if (!(installed && isEnabled)) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Widget> children = [];
|
||||||
|
final model = getPluginModel(kLocationHostMainPlugin, plugin.meta.id);
|
||||||
|
if (model != null) {
|
||||||
|
children.add(PluginItem(
|
||||||
|
pluginId: plugin.meta.id,
|
||||||
|
peerId: '',
|
||||||
|
location: kLocationHostMainPlugin,
|
||||||
|
pluginModel: model,
|
||||||
|
isMenu: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ExpansionTile(
|
||||||
|
title: Text('Options'),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -762,6 +762,10 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("mainGetError");
|
throw UnimplementedError("mainGetError");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mainShowOption({required String key, dynamic hint}) {
|
||||||
|
throw UnimplementedError("mainShowOption");
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> mainSetOption(
|
Future<void> mainSetOption(
|
||||||
{required String key, required String value, dynamic hint}) {
|
{required String key, required String value, dynamic hint}) {
|
||||||
js.context.callMethod('setByName', [
|
js.context.callMethod('setByName', [
|
||||||
@@ -1373,10 +1377,6 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("cmLoginRes");
|
throw UnimplementedError("cmLoginRes");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmCloseConnectionWindow({required int connId, dynamic hint}) {
|
|
||||||
throw UnimplementedError("cmCloseConnectionWindow");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cmCloseConnection({required int connId, dynamic hint}) {
|
Future<void> cmCloseConnection({required int connId, dynamic hint}) {
|
||||||
throw UnimplementedError("cmCloseConnection");
|
throw UnimplementedError("cmCloseConnection");
|
||||||
}
|
}
|
||||||
@@ -1450,6 +1450,31 @@ class RustdeskImpl {
|
|||||||
required int ptr,
|
required int ptr,
|
||||||
dynamic hint}) {}
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionUnregisterPixelbufferTexture(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required int display,
|
||||||
|
required int ptr,
|
||||||
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionUnregisterGpuTexture(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required int display,
|
||||||
|
required int ptr,
|
||||||
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionSetRenderVisible(
|
||||||
|
{required UuidValue sessionId, required bool visible, dynamic hint}) {}
|
||||||
|
|
||||||
|
bool mainTextureRenderProbeSupported({dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {}
|
||||||
|
|
||||||
|
int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
|
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
|
||||||
return Future(() =>
|
return Future(() =>
|
||||||
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
||||||
@@ -1644,6 +1669,78 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("sendUrlScheme");
|
throw UnimplementedError("sendUrlScheme");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> pluginEvent(
|
||||||
|
{required String id,
|
||||||
|
required String peer,
|
||||||
|
required Uint8List event,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginEvent");
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<EventToUI> pluginRegisterEventStream(
|
||||||
|
{required String id, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginRegisterEventStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
String? pluginGetSessionOption(
|
||||||
|
{required String id,
|
||||||
|
required String peer,
|
||||||
|
required String key,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginGetSessionOption");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pluginSetSessionOption(
|
||||||
|
{required String id,
|
||||||
|
required String peer,
|
||||||
|
required String key,
|
||||||
|
required String value,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginSetSessionOption");
|
||||||
|
}
|
||||||
|
|
||||||
|
String? pluginGetSharedOption(
|
||||||
|
{required String id, required String key, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginGetSharedOption");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pluginSetSharedOption(
|
||||||
|
{required String id,
|
||||||
|
required String key,
|
||||||
|
required String value,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginSetSharedOption");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pluginReload({required String id, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginReload");
|
||||||
|
}
|
||||||
|
|
||||||
|
void pluginEnable({required String id, required bool v, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginEnable");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pluginIsEnabled({required String id, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginIsEnabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pluginFeatureIsEnabled({dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginFeatureIsEnabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pluginSyncUi({required String syncTo, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginSyncUi");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pluginListReload({dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginListReload");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pluginInstall(
|
||||||
|
{required String id, required bool b, dynamic hint}) {
|
||||||
|
throw UnimplementedError("pluginInstall");
|
||||||
|
}
|
||||||
|
|
||||||
bool isSupportMultiUiSession({required String version, dynamic hint}) {
|
bool isSupportMultiUiSession({required String version, dynamic hint}) {
|
||||||
return versionToNumber(v: version) > versionToNumber(v: '1.2.4');
|
return versionToNumber(v: version) > versionToNumber(v: '1.2.4');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:js' as js;
|
import 'dart:js' as js;
|
||||||
|
import 'dart:html' as html;
|
||||||
// cycle imports, maybe we can improve this
|
// cycle imports, maybe we can improve this
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
|
|||||||
|
|
||||||
final isDesktop_ = false;
|
final isDesktop_ = false;
|
||||||
|
|
||||||
|
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
||||||
|
|
||||||
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
||||||
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
||||||
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
||||||
|
|||||||
14
flutter/lib/web/plugin/handlers.dart
Normal file
14
flutter/lib/web/plugin/handlers.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
abstract class NativeHandler {
|
||||||
|
bool onEvent(Map<String, dynamic> evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeUiHandler extends NativeHandler {
|
||||||
|
NativeUiHandler._();
|
||||||
|
|
||||||
|
static NativeUiHandler instance = NativeUiHandler._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onEvent(Map<String, dynamic> evt) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,28 +36,8 @@ class RelativeMouseState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MainFlutterWindow: NSWindow {
|
class MainFlutterWindow: NSWindow {
|
||||||
private static let fullscreenWorkAreaSizes = NSMapTable<NSWindow, NSValue>(
|
|
||||||
keyOptions: [.weakMemory, .objectPointerPersonality],
|
|
||||||
valueOptions: .strongMemory
|
|
||||||
)
|
|
||||||
private static let fullscreenObserver = NotificationCenter.default.addObserver(
|
|
||||||
forName: NSWindow.willEnterFullScreenNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { notification in
|
|
||||||
guard let window = notification.object as? NSWindow,
|
|
||||||
let screen = window.screen else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fullscreenWorkAreaSizes.setObject(
|
|
||||||
NSValue(size: screen.visibleFrame.size),
|
|
||||||
forKey: window
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
rustdesk_core_main();
|
rustdesk_core_main();
|
||||||
_ = MainFlutterWindow.fullscreenObserver
|
|
||||||
let flutterViewController = FlutterViewController.init()
|
let flutterViewController = FlutterViewController.init()
|
||||||
let windowFrame = self.frame
|
let windowFrame = self.frame
|
||||||
self.contentViewController = flutterViewController
|
self.contentViewController = flutterViewController
|
||||||
@@ -298,16 +278,6 @@ class MainFlutterWindow: NSWindow {
|
|||||||
self.disableNativeRelativeMouseMode()
|
self.disableNativeRelativeMouseMode()
|
||||||
result(true)
|
result(true)
|
||||||
|
|
||||||
case "getMacOSWorkAreaSize":
|
|
||||||
guard Thread.isMainThread,
|
|
||||||
let window = registrar.view?.window,
|
|
||||||
let size = MainFlutterWindow.fullscreenWorkAreaSizes
|
|
||||||
.object(forKey: window)?.sizeValue else {
|
|
||||||
result(nil)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result([Double(size.width), Double(size.height)])
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: 8b774a66671cbb9bcb2631af6ac28f9bdd469ce3
|
resolved-ref: 533883bcb0ffe91a9afdb13b8bac9b14b3e054ba
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -409,6 +409,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.1"
|
version: "12.0.1"
|
||||||
|
external_path:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: external_path
|
||||||
|
sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -521,18 +529,17 @@ packages:
|
|||||||
flutter_custom_cursor:
|
flutter_custom_cursor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: flutter_custom_cursor
|
||||||
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
|
sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d"
|
||||||
resolved-ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/rustdesk-org/flutter_custom_cursor"
|
source: hosted
|
||||||
source: git
|
version: "0.0.4"
|
||||||
version: "0.0.3"
|
|
||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
|
||||||
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
resolved-ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
|
||||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -1291,8 +1298,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||||
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
resolved-ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||||
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.16"
|
version: "0.0.16"
|
||||||
@@ -1590,9 +1597,9 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "plugins/window_size"
|
path: "plugins/window_size"
|
||||||
ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
|
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
||||||
resolved-ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
|
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
||||||
url: "https://github.com/21pages/flutter-desktop-embedding.git"
|
url: "https://github.com/google/flutter-desktop-embedding.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.5.0+68
|
version: 1.4.9+67
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -29,6 +29,7 @@ dependencies:
|
|||||||
|
|
||||||
ffi: ^2.1.0
|
ffi: ^2.1.0
|
||||||
path_provider: ^2.1.1
|
path_provider: ^2.1.1
|
||||||
|
external_path: ^1.0.3
|
||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
tuple: ^2.0.0
|
tuple: ^2.0.0
|
||||||
wakelock_plus: ^1.1.3
|
wakelock_plus: ^1.1.3
|
||||||
@@ -57,15 +58,12 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
|
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
|
||||||
freezed_annotation: ^2.0.3
|
freezed_annotation: ^2.0.3
|
||||||
flutter_custom_cursor:
|
flutter_custom_cursor: ^0.0.4
|
||||||
git:
|
|
||||||
url: https://github.com/rustdesk-org/flutter_custom_cursor
|
|
||||||
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
|
|
||||||
window_size:
|
window_size:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/21pages/flutter-desktop-embedding.git
|
url: https://github.com/google/flutter-desktop-embedding.git
|
||||||
path: plugins/window_size
|
path: plugins/window_size
|
||||||
ref: 51e67ce047c72b26810b99e8473ddb44612fe356
|
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
||||||
get: ^4.6.5
|
get: ^4.6.5
|
||||||
visibility_detector: ^0.4.0+2
|
visibility_detector: ^0.4.0+2
|
||||||
contextmenu: ^3.0.0
|
contextmenu: ^3.0.0
|
||||||
@@ -90,13 +88,13 @@ dependencies:
|
|||||||
texture_rgba_renderer:
|
texture_rgba_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
||||||
ref: 42797e0f03141dc2b585f76c64a13974508058b4
|
ref: 883326ddd4fb2af1484bf873b4ea856a0ac440bc
|
||||||
percent_indicator: ^4.2.2
|
percent_indicator: ^4.2.2
|
||||||
dropdown_button2: ^2.0.0
|
dropdown_button2: ^2.0.0
|
||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||||
ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87
|
ref: 208619e750a5fd904c689a9babd6ccf0f7c1ca88
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
auto_size_text_field: ^2.2.1
|
auto_size_text_field: ^2.2.1
|
||||||
flex_color_picker: ^3.3.0
|
flex_color_picker: ^3.3.0
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter_hbb/models/file_model.dart';
|
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
final _sessionId = UuidValue('00000000-0000-0000-0000-000000000000');
|
|
||||||
|
|
||||||
class _FakeFFI implements FFI {
|
|
||||||
@override
|
|
||||||
String id = 'test-peer';
|
|
||||||
@override
|
|
||||||
UuidValue get sessionId => _sessionId;
|
|
||||||
@override
|
|
||||||
late final FfiModel ffiModel = FfiModel(WeakReference(this));
|
|
||||||
|
|
||||||
@override
|
|
||||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileController _createController(FileFetcher fileFetcher) {
|
|
||||||
final ffi = _FakeFFI();
|
|
||||||
return FileController(
|
|
||||||
isLocal: false,
|
|
||||||
getSessionID: () => _sessionId,
|
|
||||||
rootState: WeakReference(ffi),
|
|
||||||
jobController: JobController(() => _sessionId, () => null),
|
|
||||||
fileFetcher: fileFetcher,
|
|
||||||
getOtherSideDirectoryData: () =>
|
|
||||||
DirectoryData(FileDirectory(), DirectoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileDirectory _directory(String path) => FileDirectory()..path = path;
|
|
||||||
|
|
||||||
String _directoryJson(String path) => jsonEncode({
|
|
||||||
'id': 0,
|
|
||||||
'path': path,
|
|
||||||
'entries': <Object>[],
|
|
||||||
});
|
|
||||||
|
|
||||||
class _SentRead {
|
|
||||||
final String path;
|
|
||||||
final bool includeHidden;
|
|
||||||
|
|
||||||
const _SentRead(this.path, this.includeHidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('a fast remote response is matched after registration', () async {
|
|
||||||
late final FileFetcher fileFetcher;
|
|
||||||
fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, path, __) {
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson(path), 'false');
|
|
||||||
return Future<void>.value();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final directory = await fileFetcher.fetchDirectory('/fast', false, false);
|
|
||||||
expect(directory.path, '/fast');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a send failure fails and removes its registered task', () async {
|
|
||||||
final failure = StateError('send failed');
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, __, ___) => Future<void>.error(failure),
|
|
||||||
);
|
|
||||||
await expectLater(
|
|
||||||
fileFetcher.fetchDirectory('/failed', false, false),
|
|
||||||
throwsA(same(failure)),
|
|
||||||
);
|
|
||||||
expect(fileFetcher.hasPendingRemoteRead('/failed'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a resolved Home path completes the sole empty-path request', () async {
|
|
||||||
final sent = <_SentRead>[];
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, path, includeHidden) async {
|
|
||||||
sent.add(_SentRead(path, includeHidden));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final controller = _createController(fileFetcher);
|
|
||||||
controller.directory.value = _directory('/initial');
|
|
||||||
|
|
||||||
final home = controller.openDirectory('');
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
final response = _directoryJson('/home/user');
|
|
||||||
controller.initDirAndHome({'value': response});
|
|
||||||
expect(controller.homePath, '/home/user');
|
|
||||||
expect(controller.directory.value.path, '/initial');
|
|
||||||
fileFetcher.tryCompleteTask(response, 'false');
|
|
||||||
expect(await home, isTrue);
|
|
||||||
expect(controller.directory.value.path, '/home/user');
|
|
||||||
expect(sent.single.path, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('an automatic response initializes Home without a pending request', () {
|
|
||||||
final controller = _createController(FileFetcher(() => _sessionId));
|
|
||||||
|
|
||||||
controller.initDirAndHome({'value': _directoryJson('/home/user')});
|
|
||||||
|
|
||||||
expect(controller.homePath, '/home/user');
|
|
||||||
expect(controller.directory.value.path, '/home/user');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('an exact path response is not taken by a pending Home request',
|
|
||||||
() async {
|
|
||||||
final sent = <_SentRead>[];
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, path, includeHidden) async {
|
|
||||||
sent.add(_SentRead(path, includeHidden));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final home = fileFetcher.fetchDirectory('', false, false);
|
|
||||||
final regular = fileFetcher.fetchDirectory('/regular', false, false);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
var homeCompleted = false;
|
|
||||||
home.then<void>((_) => homeCompleted = true);
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/unmatched'), 'false');
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(homeCompleted, isFalse);
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/regular'), 'false');
|
|
||||||
expect((await regular).path, '/regular');
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(homeCompleted, isFalse);
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/home/user'), 'false');
|
|
||||||
expect((await home).path, '/home/user');
|
|
||||||
expect(sent.map((request) => request.path), ['', '/regular']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a read error completes the sole pending request', () async {
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, __, ___) async {},
|
|
||||||
);
|
|
||||||
final request = fileFetcher.fetchDirectory('/denied', false, false);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
final expectation = expectLater(request, throwsA('permission denied'));
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteRemoteTaskWithError('permission denied');
|
|
||||||
|
|
||||||
await expectation;
|
|
||||||
expect(fileFetcher.hasPendingRemoteRead('/denied'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('same-path requests share the pending read', () async {
|
|
||||||
final sent = <_SentRead>[];
|
|
||||||
late final FileFetcher fileFetcher;
|
|
||||||
fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, path, includeHidden) async {
|
|
||||||
sent.add(_SentRead(path, includeHidden));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final controller = _createController(fileFetcher);
|
|
||||||
controller.directory.value = _directory('/initial');
|
|
||||||
|
|
||||||
final first = controller.openDirectory('/same');
|
|
||||||
final waiting = controller.openDirectory('/same');
|
|
||||||
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(sent.map((request) => request.path), ['/same']);
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
|
||||||
expect(await first, isTrue);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
expect(sent.map((request) => request.path), ['/same']);
|
|
||||||
expect(await waiting, isTrue);
|
|
||||||
expect(controller.directory.value.path, '/same');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('same-path requests with different hidden options are serialized',
|
|
||||||
() async {
|
|
||||||
final sent = <_SentRead>[];
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, path, includeHidden) async {
|
|
||||||
sent.add(_SentRead(path, includeHidden));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final first = fileFetcher.fetchDirectory('/same', false, false);
|
|
||||||
final second = fileFetcher.fetchDirectory('/same', false, true);
|
|
||||||
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(sent.map((request) => request.includeHidden), [false]);
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
|
||||||
expect((await first).path, '/same');
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(sent.map((request) => request.includeHidden), [false, true]);
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
|
||||||
expect((await second).path, '/same');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('session invalidation cancels active and waiting reads', () async {
|
|
||||||
final sent = <_SentRead>[];
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, path, includeHidden) async {
|
|
||||||
sent.add(_SentRead(path, includeHidden));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final first = fileFetcher.fetchDirectory('/same', false, false);
|
|
||||||
final waiting = fileFetcher.fetchDirectory('/same', false, true);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
final firstError = expectLater(first, throwsA(isA<StateError>()));
|
|
||||||
final waitingError = expectLater(waiting, throwsA(isA<StateError>()));
|
|
||||||
|
|
||||||
fileFetcher.beginRemoteSession();
|
|
||||||
|
|
||||||
await firstError;
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(sent.map((request) => request.includeHidden), [false]);
|
|
||||||
await waitingError;
|
|
||||||
expect(fileFetcher.hasPendingRemoteRead('/same'), isFalse);
|
|
||||||
final replacement = fileFetcher.fetchDirectory('/same', false, true);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(sent.map((request) => request.includeHidden), [false, true]);
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
|
||||||
expect((await replacement).path, '/same');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a late dispatch failure cannot remove a replacement task', () async {
|
|
||||||
final dispatches = <Completer<void>>[];
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, __, ___) {
|
|
||||||
final dispatch = Completer<void>();
|
|
||||||
dispatches.add(dispatch);
|
|
||||||
return dispatch.future;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final first = fileFetcher.fetchDirectory('/same', false, false);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
final firstError = expectLater(first, throwsA(isA<StateError>()));
|
|
||||||
fileFetcher.beginRemoteSession();
|
|
||||||
await firstError;
|
|
||||||
|
|
||||||
final replacement = fileFetcher.fetchDirectory('/same', false, false);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
expect(dispatches, hasLength(2));
|
|
||||||
dispatches.first.completeError(StateError('late dispatch failure'));
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
expect(fileFetcher.hasPendingRemoteRead('/same'), isTrue);
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
|
||||||
expect((await replacement).path, '/same');
|
|
||||||
dispatches.last.complete();
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('navigation ignores stale directory responses', () async {
|
|
||||||
final fileFetcher = FileFetcher(
|
|
||||||
() => _sessionId,
|
|
||||||
readRemoteDirectory: (_, __, ___) async {},
|
|
||||||
);
|
|
||||||
final controller = _createController(fileFetcher);
|
|
||||||
controller.directory.value = _directory('/initial');
|
|
||||||
|
|
||||||
final stale = controller.openDirectory('/stale');
|
|
||||||
final latest = controller.openDirectory('/latest');
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/latest'), 'false');
|
|
||||||
expect(await latest, isTrue);
|
|
||||||
fileFetcher.tryCompleteTask(_directoryJson('/stale'), 'false');
|
|
||||||
expect(await stale, isTrue);
|
|
||||||
|
|
||||||
expect(controller.directory.value.path, '/latest');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -342,43 +342,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('shouldHandleTerminalPasteShortcut', () {
|
group('shouldHandleTerminalPasteShortcut', () {
|
||||||
test('handles only Ctrl+Shift+V on Linux with a virtual lock', () {
|
|
||||||
expect(
|
|
||||||
shouldHandleTerminalPasteShortcut(
|
|
||||||
platform: TargetPlatform.linux,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
|
||||||
isKeyDown: true,
|
|
||||||
isKeyRepeat: false,
|
|
||||||
controlPressed: true,
|
|
||||||
metaPressed: false,
|
|
||||||
altPressed: false,
|
|
||||||
shiftPressed: true,
|
|
||||||
modifierLockActive: true,
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
shouldHandleTerminalPasteShortcut(
|
|
||||||
platform: TargetPlatform.linux,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
|
||||||
isKeyDown: true,
|
|
||||||
isKeyRepeat: false,
|
|
||||||
controlPressed: true,
|
|
||||||
metaPressed: false,
|
|
||||||
altPressed: false,
|
|
||||||
shiftPressed: false,
|
|
||||||
modifierLockActive: true,
|
|
||||||
),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
||||||
() {
|
() {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -396,7 +364,6 @@ void main() {
|
|||||||
() {
|
() {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -410,7 +377,6 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.macOS,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -427,7 +393,6 @@ void main() {
|
|||||||
test('handles paste shortcut repeats while a virtual lock is active', () {
|
test('handles paste shortcut repeats while a virtual lock is active', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: false,
|
isKeyDown: false,
|
||||||
isKeyRepeat: true,
|
isKeyRepeat: true,
|
||||||
@@ -444,7 +409,6 @@ void main() {
|
|||||||
test('ignores key-up and unmodified V events', () {
|
test('ignores key-up and unmodified V events', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: false,
|
isKeyDown: false,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -458,7 +422,6 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -481,7 +444,6 @@ void main() {
|
|||||||
]) {
|
]) {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -499,7 +461,6 @@ void main() {
|
|||||||
test('ignores non-V key events', () {
|
test('ignores non-V key events', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
platform: TargetPlatform.windows,
|
|
||||||
logicalKey: LogicalKeyboardKey.keyC,
|
logicalKey: LogicalKeyboardKey.keyC,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user