mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-21 09:08:35 +08:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99c540210 | ||
|
|
84dab0e96f | ||
|
|
458a88fb89 | ||
|
|
8a70932cd6 | ||
|
|
edfae98a01 | ||
|
|
d61c99b105 | ||
|
|
30a11bfe0a | ||
|
|
9d2bdfefb1 | ||
|
|
34b93c6f83 | ||
|
|
152d0ce74b | ||
|
|
32a3bcdc4f | ||
|
|
314c93b210 | ||
|
|
b64f6271e2 | ||
|
|
ac044c4049 | ||
|
|
0973f51df9 | ||
|
|
02b046bdbf | ||
|
|
02c274aeb6 | ||
|
|
ab6a6ca17d | ||
|
|
28d38cd71d | ||
|
|
b487f297b8 | ||
|
|
64654ee7cf | ||
|
|
1c99eb5500 | ||
|
|
74dd0c8fa0 | ||
|
|
d26fea41ee | ||
|
|
bc211c8031 | ||
|
|
d4cb7d68c5 | ||
|
|
608d7d55d5 | ||
|
|
4a49fbe4a6 | ||
|
|
f760e21ff8 | ||
|
|
251e1a3487 | ||
|
|
b990ff3782 | ||
|
|
c5426b0fbc | ||
|
|
8b710f62c8 | ||
|
|
0707e791e8 | ||
|
|
a07392e6b8 | ||
|
|
9125a68f81 | ||
|
|
9ee77a9b92 | ||
|
|
304e0e465d | ||
|
|
5d2bb9c995 | ||
|
|
ddc172bdfa | ||
|
|
06c7bc137f | ||
|
|
9e4cc91a14 | ||
|
|
0a28d09ff8 | ||
|
|
ab89d84a8f | ||
|
|
0aa98eac6d | ||
|
|
d4aa2b7ce4 | ||
|
|
35b4535ebc | ||
|
|
f0be80c253 | ||
|
|
a79a9f697b | ||
|
|
6082bb2754 | ||
|
|
4e6a43288e | ||
|
|
72a1f1161e | ||
|
|
912f5265f1 | ||
|
|
5eb2c31207 | ||
|
|
062c8d582c | ||
|
|
68b07505ab | ||
|
|
d3efcd4223 | ||
|
|
a277b022ff | ||
|
|
740c5358ab | ||
|
|
7978e0301d | ||
|
|
0f070b0108 | ||
|
|
6f0cb3b8c2 | ||
|
|
d0ef52e418 | ||
|
|
69277dd16b | ||
|
|
faf97c770c | ||
|
|
78088360ca | ||
|
|
5cfd1701fb | ||
|
|
a4bd23c9de | ||
|
|
12c1337b7b | ||
|
|
040253b319 | ||
|
|
44fa83d080 | ||
|
|
4f7e10bac6 | ||
|
|
d1fdcf1b16 | ||
|
|
4c12b83068 | ||
|
|
f86c88b3d8 | ||
|
|
697dd87383 | ||
|
|
0b8cccd8be | ||
|
|
32dbc0c8fb | ||
|
|
bae4a2c710 | ||
|
|
711ed28846 | ||
|
|
e6c5064ce5 | ||
|
|
0f5f9f6524 | ||
|
|
ce7867c1c0 | ||
|
|
415d2c5c60 | ||
|
|
a289eae07c | ||
|
|
1c9b456456 | ||
|
|
eba19e67ff | ||
|
|
adc5a7be51 | ||
|
|
26d23d588a | ||
|
|
f0450db203 | ||
|
|
3a75947553 | ||
|
|
c565849062 | ||
|
|
40e8f0d307 | ||
|
|
129f6c869b | ||
|
|
924aa515c6 | ||
|
|
c51771c854 | ||
|
|
c8b9031996 | ||
|
|
4da584055d | ||
|
|
bd22b01370 | ||
|
|
b35b48086a | ||
|
|
445e9ac285 | ||
|
|
7a3e1fe648 | ||
|
|
dfa9519d58 | ||
|
|
cc6f919080 | ||
|
|
2cdaca0fa3 | ||
|
|
6159449eba | ||
|
|
6088920f8d | ||
|
|
e8187588c1 | ||
|
|
289076aa70 | ||
|
|
547da31095 | ||
|
|
1bf4ef1f46 | ||
|
|
1212d9fa2d | ||
|
|
8c8a643cce | ||
|
|
675ffe0381 | ||
|
|
844caf8c15 | ||
|
|
0f6d28def7 | ||
|
|
0d3243e6dd | ||
|
|
53d11e99d7 | ||
|
|
defb3e6c73 | ||
|
|
ae8dfe84a0 | ||
|
|
5e920f0fd0 | ||
|
|
1a0814b201 | ||
|
|
ace98d98ad | ||
|
|
09083b3afa | ||
|
|
36e11c61a9 | ||
|
|
55187e9243 | ||
|
|
ae1c1a56e6 | ||
|
|
cdd58e77eb | ||
|
|
ce924cc0d3 | ||
|
|
498b8ba3d6 | ||
|
|
af610b2408 | ||
|
|
6cdbcfc082 | ||
|
|
9c7f51bc76 | ||
|
|
65683cc3e6 | ||
|
|
eb1ef0969c | ||
|
|
29b01e9cef | ||
|
|
cde7620eda | ||
|
|
844b853074 | ||
|
|
97f02ed25e | ||
|
|
22c84bbbd1 | ||
|
|
227f154ee7 | ||
|
|
59d7bf1e86 | ||
|
|
38fcf4e039 | ||
|
|
4b3b31147e | ||
|
|
e6d4067f48 | ||
|
|
507de628c9 | ||
|
|
2591d4f044 | ||
|
|
9bcd0d1b03 | ||
|
|
5555ba6b2f | ||
|
|
28b6bc186f | ||
|
|
00d38260e1 | ||
|
|
e06f456bbd | ||
|
|
cc860b2906 | ||
|
|
839e8180e0 | ||
|
|
83aba804d0 | ||
|
|
560c1effe8 | ||
|
|
e7353be0cd | ||
|
|
ba832362a7 | ||
|
|
9ea09c1515 | ||
|
|
3a97b63e95 | ||
|
|
dec3cde9b3 | ||
|
|
c4d0b02478 | ||
|
|
306dd77b81 | ||
|
|
fd62751cb8 | ||
|
|
b0edfb8f70 | ||
|
|
334526026c | ||
|
|
b5414ec002 | ||
|
|
4eca8b9447 | ||
|
|
1ebc726acd | ||
|
|
d563372a91 | ||
|
|
4a745d82f6 | ||
|
|
2f5f701dc7 | ||
|
|
60a0099ba0 | ||
|
|
30a7847100 | ||
|
|
1e822fa135 | ||
|
|
f6261883e8 | ||
|
|
3365844def | ||
|
|
769bbf1e1c | ||
|
|
81b999cfbe | ||
|
|
9959217cc3 | ||
|
|
3e6938bec6 | ||
|
|
4459406578 | ||
|
|
beb1084e87 | ||
|
|
d4184fd865 | ||
|
|
ffc73f86a0 | ||
|
|
c74bdcdfdb | ||
|
|
6d8b5b289f | ||
|
|
1d6873f622 | ||
|
|
7c55e3266b | ||
|
|
ce5151032e | ||
|
|
ba88bc9e8b | ||
|
|
e0095aebda | ||
|
|
664a3e186e | ||
|
|
e4f7e126e5 | ||
|
|
49989e34e4 | ||
|
|
75a14fea23 | ||
|
|
f535406962 | ||
|
|
f3f3bb538f | ||
|
|
8fefd34c15 | ||
|
|
d98f947824 | ||
|
|
5f52ce2c1b | ||
|
|
1d799483d7 | ||
|
|
3db55a718c |
@@ -12,3 +12,5 @@ rustflags = [
|
||||
#rustflags = [
|
||||
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
|
||||
#]
|
||||
[net]
|
||||
git-fetch-with-cli = true
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -26,8 +26,8 @@ body:
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system(s) on local side and remote side
|
||||
description: What operating system(s) do you see this bug on? local side -> remote side.
|
||||
label: Operating system(s) on local (controlling) side and remote (controlled) side
|
||||
description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
|
||||
placeholder: |
|
||||
Windows 10 -> osx
|
||||
validations:
|
||||
@@ -35,8 +35,8 @@ body:
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: RustDesk Version(s) on local side and remote side
|
||||
description: What RustDesk version(s) do you see this bug on? local side -> remote side.
|
||||
label: RustDesk Version(s) on local (controlling) side and remote (controlled) side
|
||||
description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
|
||||
placeholder: |
|
||||
1.1.9 -> 1.1.8
|
||||
validations:
|
||||
|
||||
42
.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
vendored
Normal file
42
.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||
index 7e634cd2aa..c1e9acc295 100644
|
||||
--- a/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||
+++ b/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||
@@ -475,7 +475,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
final GlobalKey _leadingKey = GlobalKey();
|
||||
late List<GlobalKey> buttonItemKeys;
|
||||
final MenuController _controller = MenuController();
|
||||
- late bool _enableFilter;
|
||||
+ bool _enableFilter = false;
|
||||
late List<DropdownMenuEntry<T>> filteredEntries;
|
||||
List<Widget>? _initialMenu;
|
||||
int? currentHighlight;
|
||||
@@ -524,6 +524,11 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
}
|
||||
_localTextEditingController = widget.controller ?? TextEditingController();
|
||||
}
|
||||
+ if (oldWidget.enableFilter != widget.enableFilter) {
|
||||
+ if (!widget.enableFilter) {
|
||||
+ _enableFilter = false;
|
||||
+ }
|
||||
+ }
|
||||
if (oldWidget.enableSearch != widget.enableSearch) {
|
||||
if (!widget.enableSearch) {
|
||||
currentHighlight = null;
|
||||
@@ -663,6 +668,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
);
|
||||
currentHighlight = widget.enableSearch ? i : null;
|
||||
widget.onSelected?.call(entry.value);
|
||||
+ _enableFilter = false;
|
||||
}
|
||||
: null,
|
||||
requestFocusOnHover: false,
|
||||
@@ -735,6 +741,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
if (_enableFilter) {
|
||||
filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
|
||||
?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
|
||||
+ } else {
|
||||
+ filteredEntries = widget.dropdownMenuEntries;
|
||||
}
|
||||
|
||||
if (widget.enableSearch) {
|
||||
9
.github/workflows/bridge.yml
vendored
9
.github/workflows/bridge.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.6"
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
|
||||
@@ -74,11 +74,12 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
pushd flutter && flutter pub get && popd
|
||||
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
|
||||
|
||||
- name: Run flutter rust bridge
|
||||
run: |
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@master
|
||||
@@ -89,3 +90,5 @@ jobs:
|
||||
./src/bridge_generated.io.rs
|
||||
./flutter/lib/generated_bridge.dart
|
||||
./flutter/lib/generated_bridge.freezed.dart
|
||||
./flutter/macos/Runner/bridge_generated.h
|
||||
./flutter/ios/Runner/bridge_generated.h
|
||||
|
||||
202
.github/workflows/flutter-build.yml
vendored
202
.github/workflows/flutter-build.yml
vendored
@@ -20,21 +20,21 @@ on:
|
||||
env:
|
||||
SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||
MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
|
||||
SCITER_NASM_DEBVERSION: "2.14-1"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.19.6"
|
||||
ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem).
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
ANDROID_FLUTTER_VERSION: "3.24.5"
|
||||
# for arm64 linux because official Dart SDK does not work
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.07.12
|
||||
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
|
||||
VERSION: "1.3.1"
|
||||
NDK_VERSION: "r27"
|
||||
VERSION: "1.3.3"
|
||||
NDK_VERSION: "r27b"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
@@ -46,6 +46,9 @@ env:
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||
|
||||
jobs:
|
||||
generate-bridge:
|
||||
uses: ./.github/workflows/bridge.yml
|
||||
|
||||
build-RustDeskTempTopMostWindow:
|
||||
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
|
||||
with:
|
||||
@@ -59,7 +62,7 @@ jobs:
|
||||
|
||||
build-for-windows-flutter:
|
||||
name: ${{ matrix.job.target }}
|
||||
needs: [build-RustDeskTempTopMostWindow]
|
||||
needs: [build-RustDeskTempTopMostWindow, generate-bridge]
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -85,6 +88,12 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
@@ -95,7 +104,23 @@ jobs:
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
# https://github.com/flutter/flutter/issues/155685
|
||||
- name: Replace engine with rustdesk custom flutter engine
|
||||
if: false
|
||||
run: |
|
||||
flutter doctor -v
|
||||
flutter precache --windows
|
||||
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
|
||||
Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
|
||||
mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||
|
||||
- name: Patch flutter
|
||||
shell: bash
|
||||
run: |
|
||||
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -108,13 +133,6 @@ jobs:
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
Push-Location flutter ; flutter pub get ; Pop-Location
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
@@ -374,6 +392,7 @@ jobs:
|
||||
# use build-for-macOS instead
|
||||
if: false
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
needs: [generate-bridge]
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
@@ -385,12 +404,11 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
pushd flutter && flutter pub get && popd
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
@@ -446,6 +464,7 @@ jobs:
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
name: build rustdesk ios ipa
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: [generate-bridge]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -475,6 +494,11 @@ jobs:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Patch flutter
|
||||
run: |
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
@@ -510,12 +534,11 @@ jobs:
|
||||
prefix-key: rustdesk-lib-cache-ios
|
||||
key: ${{ matrix.job.target }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
pushd flutter && flutter pub get && popd
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Build rustdesk lib
|
||||
run: |
|
||||
@@ -550,6 +573,7 @@ jobs:
|
||||
#if: ${{ inputs.upload-artifact }}
|
||||
if: false
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
needs: [generate-bridge]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
@@ -565,12 +589,11 @@ jobs:
|
||||
|
||||
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
pushd flutter && flutter pub get && popd
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Build rustdesk lib
|
||||
run: |
|
||||
@@ -605,6 +628,7 @@ jobs:
|
||||
build-for-macOS:
|
||||
name: ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: [generate-bridge]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -619,7 +643,7 @@ jobs:
|
||||
target: aarch64-apple-darwin,
|
||||
os: macos-latest,
|
||||
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
|
||||
extra-build-args: "",
|
||||
extra-build-args: "--screencapturekit",
|
||||
arch: aarch64,
|
||||
}
|
||||
steps:
|
||||
@@ -668,7 +692,13 @@ jobs:
|
||||
|
||||
- name: Install build runtime
|
||||
run: |
|
||||
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
|
||||
brew install llvm create-dmg nasm cmake gcc wget ninja
|
||||
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
|
||||
if command -v pkg-config &>/dev/null; then
|
||||
echo "pkg-config is already installed"
|
||||
else
|
||||
brew install pkg-config
|
||||
fi
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
@@ -676,6 +706,11 @@ jobs:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Patch flutter
|
||||
run: |
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
|
||||
- name: Workaround for flutter issue
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -687,7 +722,7 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
toolchain: ${{ env.MAC_RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: "rustfmt"
|
||||
|
||||
@@ -695,12 +730,11 @@ jobs:
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
pushd flutter && flutter pub get && popd
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
@@ -735,6 +769,13 @@ jobs:
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then
|
||||
MIN_MACOS_VERSION="12.3"
|
||||
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py
|
||||
sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile
|
||||
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
|
||||
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
|
||||
fi
|
||||
./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }}
|
||||
|
||||
- name: create unsigned dmg
|
||||
@@ -822,11 +863,8 @@ jobs:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz
|
||||
|
||||
generate-bridge-linux:
|
||||
uses: ./.github/workflows/bridge.yml
|
||||
|
||||
build-rustdesk-android:
|
||||
needs: [generate-bridge-linux]
|
||||
needs: [generate-bridge]
|
||||
name: build rustdesk android apk ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
@@ -903,7 +941,7 @@ jobs:
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-11-jdk-headless \
|
||||
openjdk-17-jdk-headless \
|
||||
pkg-config \
|
||||
tree \
|
||||
wget
|
||||
@@ -915,6 +953,12 @@ jobs:
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||
|
||||
- name: Patch flutter
|
||||
run: |
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
@@ -974,7 +1018,7 @@ jobs:
|
||||
key: ${{ matrix.job.target }}
|
||||
|
||||
- name: fix android for flutter 3.13
|
||||
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
run: |
|
||||
cd flutter
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
|
||||
@@ -1022,9 +1066,9 @@ jobs:
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
case ${{ matrix.job.target }} in
|
||||
@@ -1069,6 +1113,14 @@ jobs:
|
||||
mkdir -p signed-apk; pushd signed-apk
|
||||
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk .
|
||||
|
||||
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
|
||||
- name: Setup sign tool version variable
|
||||
shell: bash
|
||||
run: |
|
||||
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
|
||||
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||
|
||||
- uses: r0adkll/sign-android-release@v1
|
||||
name: Sign app APK
|
||||
if: env.ANDROID_SIGNING_KEY != null
|
||||
@@ -1080,8 +1132,8 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
# env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
|
||||
BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -1166,7 +1218,7 @@ jobs:
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-11-jdk-headless \
|
||||
openjdk-17-jdk-headless \
|
||||
pkg-config \
|
||||
tree \
|
||||
wget
|
||||
@@ -1179,6 +1231,11 @@ jobs:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||
|
||||
- name: Patch flutter
|
||||
run: |
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
@@ -1211,7 +1268,7 @@ jobs:
|
||||
path: ./flutter/android/app/src/main/jniLibs/x86
|
||||
|
||||
- name: fix android for flutter 3.13
|
||||
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
run: |
|
||||
cd flutter
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
|
||||
@@ -1223,9 +1280,9 @@ jobs:
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
@@ -1245,6 +1302,14 @@ jobs:
|
||||
mkdir -p signed-apk
|
||||
mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
|
||||
|
||||
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
|
||||
- name: Setup sign tool version variable
|
||||
shell: bash
|
||||
run: |
|
||||
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
|
||||
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||
|
||||
- uses: r0adkll/sign-android-release@v1
|
||||
name: Sign app APK
|
||||
if: env.ANDROID_SIGNING_KEY != null
|
||||
@@ -1256,8 +1321,8 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
# env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
|
||||
BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -1285,7 +1350,7 @@ jobs:
|
||||
signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
|
||||
|
||||
build-rustdesk-linux:
|
||||
needs: [generate-bridge-linux]
|
||||
needs: [generate-bridge]
|
||||
name: build rustdesk linux ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.on }}
|
||||
strategy:
|
||||
@@ -1376,6 +1441,7 @@ jobs:
|
||||
- name: Install vcpkg dependencies
|
||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
sudo apt install -y libva-dev libvdpau-dev
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet ${{ matrix.job.vcpkg-triplet }} \
|
||||
@@ -1520,6 +1586,19 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
|
||||
case ${{ matrix.job.arch }} in
|
||||
aarch64)
|
||||
pushd /opt/flutter-elinux/flutter
|
||||
;;
|
||||
x86_64)
|
||||
pushd /opt/flutter
|
||||
;;
|
||||
esac
|
||||
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
popd
|
||||
fi
|
||||
|
||||
# build flutter
|
||||
pushd /workspace
|
||||
export CARGO_INCREMENTAL=0
|
||||
@@ -1989,7 +2068,12 @@ jobs:
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Patch flutter
|
||||
shell: bash
|
||||
run: |
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
|
||||
# https://rustdesk.com/docs/en/dev/build/web/
|
||||
- name: Build web
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.06.15
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
VERSION: "1.3.1"
|
||||
VERSION: "1.3.3"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ examples/**/target/
|
||||
vcpkg_installed
|
||||
flutter/lib/generated_plugin_registrant.dart
|
||||
libsciter.dylib
|
||||
flutter/web/
|
||||
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -893,6 +893,20 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cidre"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
|
||||
dependencies = [
|
||||
"cidre-macros",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cidre-macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -1276,10 +1290,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#4d318ff778063ce14669fd4bd67a1673653fc6e5"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"cidre",
|
||||
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
@@ -1683,7 +1697,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.8.4",
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3051,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#da7dab48df19edb5a7138ff9e01bf9f148b523da"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -3505,7 +3519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-targets 0.52.5",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4149,7 +4163,7 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate 2.0.2",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
"syn 2.0.68",
|
||||
@@ -5205,7 +5219,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rdev"
|
||||
version = "0.5.0-2"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -5480,7 +5494,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5580,7 +5594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -6445,7 +6459,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tfc"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6"
|
||||
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"core-graphics 0.23.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -36,6 +36,7 @@ unix-file-copy-paste = [
|
||||
"dep:once_cell",
|
||||
"clipboard/unix-file-copy-paste",
|
||||
]
|
||||
screencapturekit = ["cpal/screencapturekit"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -77,9 +78,7 @@ fon = "0.6"
|
||||
zip = "0.6"
|
||||
shutdown_hooks = "0.1"
|
||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
|
||||
cpal = "0.15"
|
||||
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||
ringbuf = "0.3"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.1
|
||||
version: 1.3.3
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.1
|
||||
version: 1.3.3
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
11
build.py
11
build.py
@@ -143,6 +143,12 @@ def make_parser():
|
||||
"--package",
|
||||
type=str
|
||||
)
|
||||
if osx:
|
||||
parser.add_argument(
|
||||
'--screencapturekit',
|
||||
action='store_true',
|
||||
help='Enable feature screencapturekit'
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -274,6 +280,9 @@ def get_features(args):
|
||||
features.append('flutter')
|
||||
if args.unix_file_copy_paste:
|
||||
features.append('unix-file-copy-paste')
|
||||
if osx:
|
||||
if args.screencapturekit:
|
||||
features.append('screencapturekit')
|
||||
print("features:", features)
|
||||
return features
|
||||
|
||||
@@ -283,6 +292,8 @@ def generate_control_file(version):
|
||||
system2('/bin/rm -rf %s' % control_file_path)
|
||||
|
||||
content = """Package: rustdesk
|
||||
Section: net
|
||||
Priority: optional
|
||||
Version: %s
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
|
||||
9
build.rs
9
build.rs
@@ -1,7 +1,7 @@
|
||||
#[cfg(windows)]
|
||||
fn build_windows() {
|
||||
let file = "src/platform/windows.cc";
|
||||
let file2 = "src/platform/windows_delete_test_cert.cc";
|
||||
let file2 = "src/platform/windows_delete_test_cert.cc";
|
||||
cc::Build::new().file(file).file(file2).compile("windows");
|
||||
println!("cargo:rustc-link-lib=WtsApi32");
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
@@ -61,7 +61,11 @@ fn install_android_deps() {
|
||||
let target = format!("{}-android", target_arch);
|
||||
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
|
||||
let mut path: std::path::PathBuf = vcpkg_root.into();
|
||||
path.push("installed");
|
||||
if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
|
||||
path = vcpkg_root.into();
|
||||
} else {
|
||||
path.push("installed");
|
||||
}
|
||||
path.push(target);
|
||||
println!(
|
||||
"{}",
|
||||
@@ -72,7 +76,6 @@ fn install_android_deps() {
|
||||
);
|
||||
println!("cargo:rustc-link-lib=ndk_compat");
|
||||
println!("cargo:rustc-link-lib=oboe");
|
||||
println!("cargo:rustc-link-lib=oboe_wrapper");
|
||||
println!("cargo:rustc-link-lib=c++");
|
||||
println!("cargo:rustc-link-lib=OpenSLES");
|
||||
}
|
||||
|
||||
@@ -164,3 +164,7 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
|
||||

|
||||
|
||||

|
||||
|
||||
## [Serwery publiczne](#public-servers)
|
||||
|
||||
RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
||||
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
|
||||
<a href="#публічні-сервери">Сервери</a> •
|
||||
<a href="#кроки-для-збірки">Збирання</a> •
|
||||
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
||||
<a href="#структура-файлів">Структура</a> •
|
||||
<a href="#знімки">Знімки</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B>
|
||||
<a href="#знімки-екрана">Знімки екрана</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
|
||||
</p>
|
||||
|
||||
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
||||
```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
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
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)
|
||||
|
||||
```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
|
||||
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)
|
||||
@@ -158,18 +156,22 @@ target/release/rustdesk
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
|
||||
|
||||
## Знімки
|
||||
## Знімки екрана
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## [Публічні сервери](#публічні-сервери)
|
||||
|
||||
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
</p>
|
||||
|
||||
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-Z
|
||||
|
||||
## 依赖
|
||||
|
||||
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
|
||||
桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。
|
||||
|
||||
请自行下载Sciter动态库。
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
@@ -207,12 +209,13 @@ target/release/rustdesk
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||
- **[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代码
|
||||
|
||||
## 截图
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
plugins {
|
||||
id "com.google.protobuf" version "0.9.4"
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
@@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -32,10 +30,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
}
|
||||
@@ -57,7 +51,7 @@ protobuf {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
||||
@@ -105,7 +99,6 @@ flutter {
|
||||
dependencies {
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +304,13 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
val popupMenu = PopupMenu(this, floatingView)
|
||||
val idShowRustDesk = 0
|
||||
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
||||
val idStopService = 1
|
||||
// For host side, clipboard sync
|
||||
val idSyncClipboard = 1
|
||||
val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false
|
||||
if (isClipboardListenerEnabled) {
|
||||
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
||||
}
|
||||
val idStopService = 2
|
||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
@@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
openMainActivity()
|
||||
true
|
||||
}
|
||||
idSyncClipboard -> {
|
||||
syncClipboard()
|
||||
true
|
||||
}
|
||||
idStopService -> {
|
||||
stopMainService()
|
||||
true
|
||||
@@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncClipboard() {
|
||||
MainActivity.rdClipboardManager?.syncClipboard(false)
|
||||
}
|
||||
|
||||
private fun stopMainService() {
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
||||
}
|
||||
|
||||
@@ -280,20 +280,20 @@ class InputService : AccessibilityService() {
|
||||
|
||||
var textToCommit: String? = null
|
||||
|
||||
if (keyboardMode == KeyboardMode.Legacy) {
|
||||
if (keyEvent.hasChr() && keyEvent.getDown()) {
|
||||
// [down] indicates the key's state(down or up).
|
||||
// [press] indicates a click event(down and up).
|
||||
// https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688
|
||||
if (keyEvent.hasSeq()) {
|
||||
textToCommit = keyEvent.getSeq()
|
||||
} else if (keyboardMode == KeyboardMode.Legacy) {
|
||||
if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) {
|
||||
val chr = keyEvent.getChr()
|
||||
if (chr != null) {
|
||||
textToCommit = String(Character.toChars(chr))
|
||||
}
|
||||
}
|
||||
} else if (keyboardMode == KeyboardMode.Translate) {
|
||||
if (keyEvent.hasSeq() && keyEvent.getDown()) {
|
||||
val seq = keyEvent.getSeq()
|
||||
if (seq != null) {
|
||||
textToCommit = seq
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||
@@ -320,6 +320,10 @@ class InputService : AccessibilityService() {
|
||||
} else {
|
||||
ke?.let { event ->
|
||||
inputConnection.sendKeyEvent(event)
|
||||
if (keyEvent.getPress()) {
|
||||
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
|
||||
inputConnection.sendKeyEvent(actionUpEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +337,10 @@ class InputService : AccessibilityService() {
|
||||
for (item in possibleNodes) {
|
||||
val success = trySendKeyEvent(event, item, textToCommit)
|
||||
if (success) {
|
||||
if (keyEvent.getPress()) {
|
||||
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
|
||||
trySendKeyEvent(actionUpEvent, item, textToCommit)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,12 @@ object KeyEventConverter {
|
||||
}
|
||||
|
||||
var action = 0
|
||||
if (keyEventProto.getDown()) {
|
||||
if (keyEventProto.getDown() || keyEventProto.getPress()) {
|
||||
action = KeyEvent.ACTION_DOWN
|
||||
} else {
|
||||
action = KeyEvent.ACTION_UP
|
||||
}
|
||||
|
||||
// FIXME: The last parameter is the repeat count, not modifiers ?
|
||||
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
|
||||
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.ClipboardManager
|
||||
import android.os.Bundle
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
@@ -36,6 +38,9 @@ import kotlin.concurrent.thread
|
||||
class MainActivity : FlutterActivity() {
|
||||
companion object {
|
||||
var flutterMethodChannel: MethodChannel? = null
|
||||
private var _rdClipboardManager: RdClipboardManager? = null
|
||||
val rdClipboardManager: RdClipboardManager?
|
||||
get() = _rdClipboardManager;
|
||||
}
|
||||
|
||||
private val channelTag = "mChannel"
|
||||
@@ -85,11 +90,20 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (_rdClipboardManager == null) {
|
||||
_rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
|
||||
FFI.setClipboardManager(_rdClipboardManager!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.e(logTag, "onDestroy")
|
||||
mainService?.let {
|
||||
unbindService(serviceConnection)
|
||||
}
|
||||
rdClipboardManager?.rustEnableServiceClipboard(false)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -388,4 +402,13 @@ class MainActivity : FlutterActivity() {
|
||||
super.onStart()
|
||||
stopService(Intent(this, FloatingWindowService::class.java))
|
||||
}
|
||||
|
||||
// For client side
|
||||
// When swithing from other app to this app, try to sync clipboard.
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
rdClipboardManager?.syncClipboard(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,8 @@ class MainService : Service() {
|
||||
stopCapture()
|
||||
FFI.refreshScreen()
|
||||
startCapture()
|
||||
} else {
|
||||
FFI.refreshScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
|
||||
import hbb.MessageOuterClass.ClipboardFormat
|
||||
import hbb.MessageOuterClass.Clipboard
|
||||
import hbb.MessageOuterClass.MultiClipboards
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
class RdClipboardManager(private val clipboardManager: ClipboardManager) {
|
||||
private val logTag = "RdClipboardManager"
|
||||
private val supportedMimeTypes = arrayOf(
|
||||
ClipDescription.MIMETYPE_TEXT_PLAIN,
|
||||
ClipDescription.MIMETYPE_TEXT_HTML
|
||||
)
|
||||
|
||||
// 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`.
|
||||
// 2. Avoid sending the clipboard data before enabling client clipboard.
|
||||
// 1) Disable clipboard
|
||||
// 2) Copy text "a"
|
||||
// 3) Enable clipboard
|
||||
// 4) Switch to another app
|
||||
// 5) Switch back to the app
|
||||
// 6) "a" should not be sent to the client, because it's copied before enabling clipboard
|
||||
//
|
||||
// It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`,
|
||||
// though the `lastUpdatedClipData` will be set to null once.
|
||||
private var lastUpdatedClipData: ClipData? = null
|
||||
private var isClientEnabled = true;
|
||||
private var _isListening = false;
|
||||
val isListening: Boolean
|
||||
get() = _isListening
|
||||
|
||||
fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) {
|
||||
val clipData = clipboardManager.primaryClip
|
||||
if (clipData != null && clipData.itemCount > 0) {
|
||||
// Only handle the first item in the clipboard for now.
|
||||
val clip = clipData.getItemAt(0)
|
||||
val isHostSync = !isClient && isSync
|
||||
// Ignore the `isClipboardDataEqual()` check if it's a host sync operation.
|
||||
// Because it's a action manually triggered by the user.
|
||||
if (!isHostSync) {
|
||||
if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) {
|
||||
Log.d(logTag, "Clipboard data is the same as last update, ignore")
|
||||
return
|
||||
}
|
||||
}
|
||||
val mimeTypeCount = clipData.description.getMimeTypeCount()
|
||||
val mimeTypes = mutableListOf<String>()
|
||||
for (i in 0 until mimeTypeCount) {
|
||||
mimeTypes.add(clipData.description.getMimeType(i))
|
||||
}
|
||||
var text: CharSequence? = null;
|
||||
var html: String? = null;
|
||||
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
|
||||
text = clip?.text
|
||||
}
|
||||
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
|
||||
text = clip?.text
|
||||
html = clip?.htmlText
|
||||
}
|
||||
var count = 0
|
||||
val clips = MultiClipboards.newBuilder()
|
||||
if (text != null) {
|
||||
val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString())
|
||||
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build())
|
||||
count++
|
||||
}
|
||||
if (html != null) {
|
||||
val content = com.google.protobuf.ByteString.copyFromUtf8(html)
|
||||
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build())
|
||||
count++
|
||||
}
|
||||
if (count > 0) {
|
||||
val clipsBytes = clips.build().toByteArray()
|
||||
val isClientFlag = if (isClient) 1 else 0
|
||||
val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply {
|
||||
put(isClientFlag.toByte())
|
||||
put(clipsBytes)
|
||||
}
|
||||
clipsBuf.flip()
|
||||
lastUpdatedClipData = clipData
|
||||
Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote")
|
||||
FFI.onClipboardUpdate(clipsBuf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener {
|
||||
override fun onPrimaryClipChanged() {
|
||||
Log.d(logTag, "onPrimaryClipChanged")
|
||||
checkPrimaryClip(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSupportedMimeType(mimeType: String): Boolean {
|
||||
return supportedMimeTypes.contains(mimeType)
|
||||
}
|
||||
|
||||
private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean {
|
||||
if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) {
|
||||
return false
|
||||
}
|
||||
val mimeTypeCount = left.description.getMimeTypeCount()
|
||||
for (i in 0 until mimeTypeCount) {
|
||||
if (left.description.getMimeType(i) != right.description.getMimeType(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (left.itemCount != right.itemCount) {
|
||||
return false
|
||||
}
|
||||
for (i in 0 until left.itemCount) {
|
||||
val mimeType = left.description.getMimeType(i)
|
||||
if (!isSupportedMimeType(mimeType)) {
|
||||
continue
|
||||
}
|
||||
val leftItem = left.getItemAt(i)
|
||||
val rightItem = right.getItemAt(i)
|
||||
if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) {
|
||||
if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustEnableServiceClipboard(enable: Boolean) {
|
||||
Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening")
|
||||
if (enable) {
|
||||
if (!_isListening) {
|
||||
clipboardManager.addPrimaryClipChangedListener(clipboardListener)
|
||||
_isListening = true
|
||||
}
|
||||
} else {
|
||||
if (_isListening) {
|
||||
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
|
||||
_isListening = false
|
||||
lastUpdatedClipData = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustEnableClientClipboard(enable: Boolean) {
|
||||
Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
|
||||
isClientEnabled = enable
|
||||
if (enable) {
|
||||
lastUpdatedClipData = clipboardManager.primaryClip
|
||||
} else {
|
||||
lastUpdatedClipData = null
|
||||
}
|
||||
}
|
||||
|
||||
fun syncClipboard(isClient: Boolean) {
|
||||
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening")
|
||||
if (isClient && !isClientEnabled) {
|
||||
return
|
||||
}
|
||||
if (!isClient && !_isListening) {
|
||||
return
|
||||
}
|
||||
checkPrimaryClip(isClient, true)
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustUpdateClipboard(clips: ByteArray) {
|
||||
val clips = MultiClipboards.parseFrom(clips)
|
||||
var mimeTypes = mutableListOf<String>()
|
||||
var text: String? = null
|
||||
var html: String? = null
|
||||
for (clip in clips.getClipboardsList()) {
|
||||
when (clip.format) {
|
||||
ClipboardFormat.Text -> {
|
||||
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
|
||||
text = String(clip.content.toByteArray(), Charsets.UTF_8)
|
||||
}
|
||||
ClipboardFormat.Html -> {
|
||||
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML)
|
||||
html = String(clip.content.toByteArray(), Charsets.UTF_8)
|
||||
}
|
||||
ClipboardFormat.ImageRgba -> {
|
||||
}
|
||||
ClipboardFormat.ImagePng -> {
|
||||
}
|
||||
else -> {
|
||||
Log.e(logTag, "Unsupported clipboard format: ${clip.format}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray())
|
||||
var item: ClipData.Item? = null
|
||||
if (text == null) {
|
||||
Log.e(logTag, "No text content in clipboard")
|
||||
return
|
||||
} else {
|
||||
if (html == null) {
|
||||
item = ClipData.Item(text)
|
||||
} else {
|
||||
item = ClipData.Item(text, html)
|
||||
}
|
||||
}
|
||||
if (item == null) {
|
||||
Log.e(logTag, "No item in clipboard")
|
||||
return
|
||||
}
|
||||
val clipData = ClipData(clipDescription, item)
|
||||
lastUpdatedClipData = clipData
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,15 @@ package ffi
|
||||
import android.content.Context
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
import com.carriez.flutter_hbb.RdClipboardManager
|
||||
|
||||
object FFI {
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
@@ -20,4 +23,5 @@ object FFI {
|
||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
external fun setCodecInfo(info: String)
|
||||
external fun getLocalOption(key: String): String
|
||||
}
|
||||
external fun onClipboardUpdate(clips: ByteBuffer)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.3.14'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@@ -24,6 +9,8 @@ allprojects {
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
1
flutter/assets/message_24dp_5F6368.svg
Normal file
1
flutter/assets/message_24dp_5F6368.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -82,6 +82,21 @@ export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
|
||||
|
||||
export VCPKG_ROOT="${HOME}/vcpkg"
|
||||
|
||||
prepare_Flutter() {
|
||||
version="${1}"
|
||||
|
||||
pushd "${HOME}"
|
||||
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
|
||||
git clone https://github.com/flutter/flutter
|
||||
fi
|
||||
pushd flutter
|
||||
git restore .
|
||||
git checkout "${version}"
|
||||
flutter config --no-analytics
|
||||
popd # flutter
|
||||
popd # ${HOME}
|
||||
}
|
||||
|
||||
# Now act depending on build step
|
||||
|
||||
# NOTE: F-Droid maintainers require explicit declaration of dependencies
|
||||
@@ -110,9 +125,13 @@ prebuild)
|
||||
.github/workflows/flutter-build.yml)"
|
||||
fi
|
||||
|
||||
FLUTTER_BRIDGE_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
.github/workflows/bridge.yml)"
|
||||
|
||||
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
|
||||
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
.github/workflows/bridge.yml)"
|
||||
|
||||
NDK_VERSION="$(yq -r \
|
||||
.env.NDK_VERSION \
|
||||
@@ -127,6 +146,7 @@ prebuild)
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
||||
[ -z "${FLUTTER_BRIDGE_VERSION}" ] ||
|
||||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
||||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
||||
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
||||
@@ -163,24 +183,6 @@ prebuild)
|
||||
sdkmanager --install "ndk;${NDK_VERSION}"
|
||||
fi
|
||||
|
||||
# Install Flutter
|
||||
|
||||
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
|
||||
pushd "${HOME}"
|
||||
|
||||
git clone https://github.com/flutter/flutter
|
||||
|
||||
pushd flutter
|
||||
|
||||
git reset --hard "${FLUTTER_VERSION}"
|
||||
|
||||
flutter config --no-analytics
|
||||
|
||||
popd # flutter
|
||||
|
||||
popd # ${HOME}
|
||||
fi
|
||||
|
||||
# Install Rust
|
||||
|
||||
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
||||
@@ -275,12 +277,46 @@ prebuild)
|
||||
|
||||
git apply res/fdroid/patches/*.patch
|
||||
|
||||
# Backup .gclient file, for later restore
|
||||
|
||||
cp flutter-sdk/.gclient flutter-sdk/.gclient.bak
|
||||
|
||||
# For FLUTTER_BRIDGE_VERSION
|
||||
sed \
|
||||
-i \
|
||||
-e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \
|
||||
flutter/pubspec.yaml
|
||||
|
||||
# Install Flutter bridge version
|
||||
prepare_Flutter "${FLUTTER_BRIDGE_VERSION}"
|
||||
cp flutter-sdk/.gclient.bak flutter-sdk/.gclient
|
||||
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_BRIDGE_VERSION}/" flutter-sdk/.gclient
|
||||
|
||||
# Download Flutter dependencies
|
||||
pushd flutter
|
||||
flutter clean && flutter packages pub get
|
||||
popd # flutter
|
||||
|
||||
# Generate FFI bindings
|
||||
flutter_rust_bridge_codegen \
|
||||
--rust-input ./src/flutter_ffi.rs \
|
||||
--dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
git restore flutter/pubspec.*
|
||||
|
||||
# Install Flutter
|
||||
prepare_Flutter "${FLUTTER_VERSION}"
|
||||
cp flutter-sdk/.gclient.bak flutter-sdk/.gclient
|
||||
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
|
||||
|
||||
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
|
||||
sed \
|
||||
-i \
|
||||
-e '/gms/d' \
|
||||
flutter/android/build.gradle \
|
||||
flutter/android/app/build.gradle
|
||||
|
||||
# `firebase_analytics`` is not in thoes files now, but we still keep the following lines.
|
||||
sed \
|
||||
-i \
|
||||
-e '/firebase_analytics/d' \
|
||||
@@ -296,33 +332,6 @@ prebuild)
|
||||
-e '/firebase/Id' \
|
||||
flutter/lib/main.dart
|
||||
|
||||
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
|
||||
# Fix for android 3.13.9
|
||||
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
||||
flutter/pubspec.yaml
|
||||
|
||||
set --
|
||||
|
||||
while read -r _1; do
|
||||
set -- "$@" "${_1}"
|
||||
done 0<<.a
|
||||
$(find flutter/lib/ -type f -name "*dart*")
|
||||
.a
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
|
||||
"$@"
|
||||
|
||||
set --
|
||||
fi
|
||||
|
||||
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
|
||||
|
||||
;;
|
||||
build)
|
||||
# build: perform actual build of APK file
|
||||
@@ -372,16 +381,10 @@ build)
|
||||
|
||||
pushd flutter
|
||||
|
||||
flutter packages pub get
|
||||
flutter clean && flutter packages pub get
|
||||
|
||||
popd # flutter
|
||||
|
||||
# Generate FFI bindings
|
||||
|
||||
flutter_rust_bridge_codegen \
|
||||
--rust-input ./src/flutter_ffi.rs \
|
||||
--dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
# Build host android deps
|
||||
|
||||
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
platform :ios, '12.0'
|
||||
platform :ios, '13.0'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
|
||||
@@ -137,6 +137,6 @@ SPEC CHECKSUMS:
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
|
||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
||||
PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -491,7 +491,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -541,7 +541,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
|
||||
import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'models/model.dart';
|
||||
import 'models/platform_model.dart';
|
||||
@@ -63,6 +64,9 @@ int androidVersion = 0;
|
||||
// So we need to use this flag to enable/disable resizable.
|
||||
bool _linuxWindowResizable = true;
|
||||
|
||||
// Only used on Windows(window manager).
|
||||
bool _ignoreDevicePixelRatio = true;
|
||||
|
||||
/// only available for Windows target
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
@@ -554,7 +558,7 @@ class MyTheme {
|
||||
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
|
||||
}
|
||||
|
||||
static void changeDarkMode(ThemeMode mode) async {
|
||||
static Future<void> changeDarkMode(ThemeMode mode) async {
|
||||
Get.changeThemeMode(mode);
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
|
||||
if (mode == ThemeMode.system) {
|
||||
@@ -680,10 +684,12 @@ closeConnection({String? id}) {
|
||||
overlays: SystemUiOverlay.values);
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
stateGlobal.isInMainPage = true;
|
||||
}();
|
||||
} else {
|
||||
if (isWeb) {
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
stateGlobal.isInMainPage = true;
|
||||
} else {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
controller.closeBy(id);
|
||||
@@ -1171,33 +1177,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
if (reconnect != null && title == "Connection Error") {
|
||||
if (reconnect != null &&
|
||||
title == "Connection Error" &&
|
||||
reconnectTimeout != null) {
|
||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||
final enabled = true.obs;
|
||||
final button = reconnectTimeout != null
|
||||
? Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
))
|
||||
: Obx(
|
||||
() => dialogButton(
|
||||
'Reconnect',
|
||||
isOutline: true,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
final button = Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
buttons.insert(0, button);
|
||||
}
|
||||
if (link.isNotEmpty) {
|
||||
@@ -1620,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) {
|
||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||
}
|
||||
|
||||
class OffsetDevicePixelRatio {
|
||||
Offset offset;
|
||||
final double devicePixelRatio;
|
||||
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
|
||||
}
|
||||
|
||||
class LastWindowPosition {
|
||||
double? width;
|
||||
double? height;
|
||||
@@ -1708,8 +1696,10 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
if (isFullscreen || isMaximized) {
|
||||
setPreFrame();
|
||||
} else {
|
||||
position = await windowManager.getPosition();
|
||||
sz = await windowManager.getSize();
|
||||
position = await windowManager.getPosition(
|
||||
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||
sz = await windowManager.getSize(
|
||||
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -1827,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) {
|
||||
}
|
||||
|
||||
/// return null means center
|
||||
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||
Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
double? left,
|
||||
double? top,
|
||||
double? width,
|
||||
@@ -1841,13 +1831,9 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||
double? frameTop;
|
||||
double? frameRight;
|
||||
double? frameBottom;
|
||||
double devicePixelRatio = 1.0;
|
||||
|
||||
if (isDesktop || isWebDesktop) {
|
||||
for (final screen in await window_size.getScreenList()) {
|
||||
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
|
||||
devicePixelRatio = screen.scaleFactor;
|
||||
}
|
||||
frameLeft = frameLeft == null
|
||||
? screen.visibleFrame.left
|
||||
: min(screen.visibleFrame.left, frameLeft);
|
||||
@@ -1881,7 +1867,7 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||
top < frameTop!) {
|
||||
return null;
|
||||
} else {
|
||||
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
||||
return Offset(left, top);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1941,47 +1927,23 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
}
|
||||
|
||||
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
||||
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
|
||||
final offsetLeftTop = await _adjustRestoreMainWindowOffset(
|
||||
lpos.offsetWidth,
|
||||
lpos.offsetHeight,
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
debugPrint(
|
||||
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
||||
"restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
||||
|
||||
switch (type) {
|
||||
case WindowType.Main:
|
||||
// https://github.com/rustdesk/rustdesk/issues/8038
|
||||
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
|
||||
// So we need to adjust the offset by the scale factor.
|
||||
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
|
||||
if (isWindows) {
|
||||
double? curDevicePixelRatio;
|
||||
Offset curPos = await windowManager.getPosition();
|
||||
for (final screen in await window_size.getScreenList()) {
|
||||
if (isPointInRect(curPos, screen.visibleFrame)) {
|
||||
curDevicePixelRatio = screen.scaleFactor;
|
||||
}
|
||||
}
|
||||
if (curDevicePixelRatio != null &&
|
||||
curDevicePixelRatio != 0 &&
|
||||
offsetDevicePixelRatio != null) {
|
||||
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
|
||||
final scale =
|
||||
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
|
||||
offsetDevicePixelRatio.offset =
|
||||
offsetDevicePixelRatio.offset.scale(scale, scale);
|
||||
debugPrint(
|
||||
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
|
||||
}
|
||||
}
|
||||
}
|
||||
restorePos() async {
|
||||
if (offsetDevicePixelRatio == null) {
|
||||
if (offsetLeftTop == null) {
|
||||
await windowManager.center();
|
||||
} else {
|
||||
await windowManager.setPosition(offsetDevicePixelRatio.offset);
|
||||
await windowManager.setPosition(offsetLeftTop,
|
||||
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||
}
|
||||
}
|
||||
if (lpos.isMaximized == true) {
|
||||
@@ -1990,20 +1952,39 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
await windowManager.maximize();
|
||||
}
|
||||
} else {
|
||||
if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) {
|
||||
await windowManager.setSize(size);
|
||||
final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
|
||||
if (isWindows) {
|
||||
if (storeSize) {
|
||||
// We need to set the window size first to avoid the incorrect size in some special cases.
|
||||
// E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
|
||||
// The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
|
||||
// After restoring, the size will be incorrect.
|
||||
// See known issue in https://github.com/rustdesk/rustdesk/pull/9840
|
||||
await windowManager.setSize(size,
|
||||
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||
}
|
||||
await restorePos();
|
||||
if (storeSize) {
|
||||
await windowManager.setSize(size,
|
||||
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||
}
|
||||
} else {
|
||||
if (storeSize) {
|
||||
await windowManager.setSize(size,
|
||||
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||
}
|
||||
await restorePos();
|
||||
}
|
||||
await restorePos();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
restoreFrame() async {
|
||||
if (offsetDevicePixelRatio == null) {
|
||||
if (offsetLeftTop == null) {
|
||||
await wc.center();
|
||||
} else {
|
||||
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
|
||||
offsetDevicePixelRatio.offset.dy, size.width, size.height);
|
||||
final frame = Rect.fromLTWH(
|
||||
offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
|
||||
await wc.setFrame(frame);
|
||||
}
|
||||
}
|
||||
@@ -2035,6 +2016,8 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
return false;
|
||||
}
|
||||
|
||||
var webInitialLink = "";
|
||||
|
||||
/// Initialize uni links for macos/windows
|
||||
///
|
||||
/// [Availability]
|
||||
@@ -2051,7 +2034,12 @@ Future<bool> initUniLinks() async {
|
||||
if (initialLink == null || initialLink.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return handleUriLink(uriString: initialLink);
|
||||
if (isWeb) {
|
||||
webInitialLink = initialLink;
|
||||
return false;
|
||||
} else {
|
||||
return handleUriLink(uriString: initialLink);
|
||||
}
|
||||
} catch (err) {
|
||||
debugPrintStack(label: "$err");
|
||||
return false;
|
||||
@@ -2064,7 +2052,7 @@ Future<bool> initUniLinks() async {
|
||||
///
|
||||
/// Returns a [StreamSubscription] which can listen the uni links.
|
||||
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
||||
if (isLinux) {
|
||||
if (isLinux || isWeb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2257,7 +2245,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
}
|
||||
}
|
||||
|
||||
var key = uri.queryParameters["key"];
|
||||
var queryParameters =
|
||||
uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
|
||||
|
||||
var key = queryParameters["key"];
|
||||
if (id != null) {
|
||||
if (key != null) {
|
||||
id = "$id?key=$key";
|
||||
@@ -2266,7 +2257,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
|
||||
if (isMobile) {
|
||||
if (id != null) {
|
||||
final forceRelay = uri.queryParameters["relay"] != null;
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
connect(Get.context!, id, forceRelay: forceRelay);
|
||||
return null;
|
||||
}
|
||||
@@ -2276,7 +2267,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
if (command != null && id != null) {
|
||||
args.add(command);
|
||||
args.add(id);
|
||||
var param = uri.queryParameters;
|
||||
var param = queryParameters;
|
||||
String? password = param["password"];
|
||||
if (password != null) args.addAll(['--password', password]);
|
||||
String? switch_uuid = param["switch_uuid"];
|
||||
@@ -2294,16 +2285,19 @@ connectMainDesktop(String id,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
String? password,
|
||||
String? connToken,
|
||||
bool? isSharedPassword}) async {
|
||||
if (isFileTransfer) {
|
||||
await rustDeskWinManager.newFileTransfer(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTcpTunneling || isRDP) {
|
||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else {
|
||||
await rustDeskWinManager.newRemoteDesktop(id,
|
||||
@@ -2323,6 +2317,7 @@ connect(BuildContext context, String id,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
String? password,
|
||||
String? connToken,
|
||||
bool? isSharedPassword}) async {
|
||||
if (id == '') return;
|
||||
if (!isDesktop || desktopType == DesktopType.main) {
|
||||
@@ -2364,24 +2359,40 @@ connect(BuildContext context, String id,
|
||||
'password': password,
|
||||
'isSharedPassword': isSharedPassword,
|
||||
'forceRelay': forceRelay,
|
||||
'connToken': connToken,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (isFileTransfer) {
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||
return;
|
||||
if (isAndroid) {
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
desktop_file_manager.FileManagerPage(
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isWebDesktop) {
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -2405,6 +2416,7 @@ connect(BuildContext context, String id,
|
||||
);
|
||||
}
|
||||
}
|
||||
stateGlobal.isInMainPage = false;
|
||||
}
|
||||
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
@@ -2489,9 +2501,20 @@ Future<void> onActiveWindowChanged() async {
|
||||
// But the app will not close.
|
||||
//
|
||||
// No idea why we need to delay here, `terminate()` itself is also an async function.
|
||||
Future.delayed(Duration.zero, () {
|
||||
RdPlatformChannel.instance.terminate();
|
||||
});
|
||||
//
|
||||
// A quick workaround, use `Timer.periodic` to avoid the app not closing.
|
||||
// Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
|
||||
// may not work since `Flutter 3.24.4`, see the following logs.
|
||||
// A delay will allow the app to close.
|
||||
//
|
||||
//```
|
||||
// embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
|
||||
// 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
|
||||
// embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
|
||||
// 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
|
||||
// ```
|
||||
periodic_immediate(
|
||||
Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2584,7 +2607,7 @@ class ServerConfig {
|
||||
config['relay'] = relayServer.trim();
|
||||
config['api'] = apiServer.trim();
|
||||
config['key'] = key.trim();
|
||||
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
|
||||
return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
|
||||
.split('')
|
||||
.reversed
|
||||
.join();
|
||||
@@ -3158,6 +3181,9 @@ importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (isWeb || isIOS) {
|
||||
sc.relayServer = '';
|
||||
}
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||
success.then((value) {
|
||||
@@ -3597,3 +3623,7 @@ List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
||||
SubWindowResizeEdge.topRight,
|
||||
]
|
||||
: null;
|
||||
|
||||
void earlyAssert() {
|
||||
assert('\1' == '1');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -241,14 +242,15 @@ class _AddressBookState extends State<AddressBook> {
|
||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||
}
|
||||
},
|
||||
customButton: Obx(()=>Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
)),
|
||||
customButton: Obx(() => Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child:
|
||||
buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
)),
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
@@ -315,13 +317,14 @@ class _AddressBookState extends State<AddressBook> {
|
||||
|
||||
Widget _buildTags() {
|
||||
return Obx(() {
|
||||
final List tags;
|
||||
List tags;
|
||||
if (gFFI.abModel.sortTags.value) {
|
||||
tags = gFFI.abModel.currentAbTags.toList();
|
||||
tags.sort();
|
||||
} else {
|
||||
tags = gFFI.abModel.currentAbTags;
|
||||
tags = gFFI.abModel.currentAbTags.toList();
|
||||
}
|
||||
tags = [kUntagged, ...tags].toList();
|
||||
final editPermission = gFFI.abModel.current.canWrite();
|
||||
tagBuilder(String e) {
|
||||
return AddressBookTag(
|
||||
@@ -358,7 +361,6 @@ class _AddressBookState extends State<AddressBook> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: AddressBookPeersView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.abModel.currentAbPeers,
|
||||
)),
|
||||
);
|
||||
}
|
||||
@@ -509,19 +511,19 @@ class _AddressBookState extends State<AddressBook> {
|
||||
|
||||
row({required Widget lable, required Widget input}) {
|
||||
makeChild(bool isPortrait) => Row(
|
||||
children: [
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
children: [
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||
}
|
||||
|
||||
@@ -546,23 +548,28 @@ class _AddressBookState extends State<AddressBook> {
|
||||
],
|
||||
),
|
||||
input: Obx(() => TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
))),
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
))),
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Alias'),
|
||||
style: style,
|
||||
),
|
||||
input: Obx(() => TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'),
|
||||
),)),
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Alias'),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (isCurrentAbShared)
|
||||
row(
|
||||
@@ -570,25 +577,29 @@ class _AddressBookState extends State<AddressBook> {
|
||||
translate('Password'),
|
||||
style: style,
|
||||
),
|
||||
input: Obx(() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
input: Obx(
|
||||
() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),)),
|
||||
)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
@@ -660,6 +671,14 @@ class _AddressBookState extends State<AddressBook> {
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (var t in [kUntagged, translate(kUntagged)]) {
|
||||
if (tags.contains(t)) {
|
||||
BotToast.showText(
|
||||
contentColor: Colors.red, text: 'Tag name cannot be "$t"');
|
||||
isInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
gFFI.abModel.addTags(tags);
|
||||
// final currentPeers
|
||||
}
|
||||
@@ -732,12 +751,14 @@ class AddressBookTag extends StatelessWidget {
|
||||
}
|
||||
|
||||
const double radius = 8;
|
||||
final isUnTagged = name == kUntagged;
|
||||
final showAction = showActionMenu && !isUnTagged;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
onTapDown: showActionMenu ? setPosition : null,
|
||||
onSecondaryTapDown: showActionMenu ? setPosition : null,
|
||||
onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
|
||||
onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
|
||||
onTapDown: showAction ? setPosition : null,
|
||||
onSecondaryTapDown: showAction ? setPosition : null,
|
||||
onSecondaryTap: showAction ? () => _showMenu(context, pos) : null,
|
||||
onLongPress: showAction ? () => _showMenu(context, pos) : null,
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: tags.contains(name)
|
||||
@@ -749,17 +770,18 @@ class AddressBookTag extends StatelessWidget {
|
||||
child: IntrinsicWidth(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: tags.contains(name)
|
||||
? Colors.white
|
||||
: gFFI.abModel.getCurrentAbTagColor(name)),
|
||||
).marginOnly(right: radius / 2),
|
||||
if (!isUnTagged)
|
||||
Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: tags.contains(name)
|
||||
? Colors.white
|
||||
: gFFI.abModel.getCurrentAbTagColor(name)),
|
||||
).marginOnly(right: radius / 2),
|
||||
Expanded(
|
||||
child: Text(name,
|
||||
child: Text(isUnTagged ? translate(name) : name,
|
||||
style: TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: tags.contains(name) ? Colors.white : null)),
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
const _kWindowsSystemSound = 'System Sound';
|
||||
const _kSystemSound = 'System Sound';
|
||||
|
||||
typedef AudioINputSetDevice = void Function(String device);
|
||||
typedef AudioInputBuilder = Widget Function(
|
||||
@@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget {
|
||||
: super(key: key);
|
||||
|
||||
static String getDefault() {
|
||||
if (isWindows) return translate('System Sound');
|
||||
if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound);
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget {
|
||||
static Future<Map<String, Object>> getDevicesInfo(
|
||||
bool isCm, bool isVoiceCall) async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (isWindows) {
|
||||
devices.insert(0, translate(_kWindowsSystemSound));
|
||||
if (bind.mainAudioSupportLoopback()) {
|
||||
devices.insert(0, translate(_kSystemSound));
|
||||
}
|
||||
String current = await getValue(isCm, isVoiceCall);
|
||||
return {'devices': devices, 'current': current};
|
||||
|
||||
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
// end
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
debugPrint("TwoFingerState.pan onEnd");
|
||||
debugPrint("OneFingerState.pan onEnd");
|
||||
if (onOneFingerPanEnd != null) {
|
||||
onOneFingerPanEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
menuPadding: widget.menuPadding,
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -115,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
menuPadding: widget.menuPadding,
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
@@ -595,8 +595,7 @@ class QualityMonitor extends StatelessWidget {
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
_row(
|
||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||
if (!isWeb)
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -58,27 +58,33 @@ class _PeerCardState extends State<_PeerCard>
|
||||
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||
}
|
||||
|
||||
Widget gestureDetector({required Widget child}) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
return GestureDetector(
|
||||
onDoubleTap: peerTabModel.multiSelectionMode
|
||||
? null
|
||||
: () => widget.connect(context, peer.id),
|
||||
onTap: () {
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (isMobile) {
|
||||
widget.connect(context, peer.id);
|
||||
} else {
|
||||
peerTabModel.select(peer);
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () => peerTabModel.select(peer),
|
||||
child: child);
|
||||
}
|
||||
|
||||
Widget _buildPortrait() {
|
||||
final peer = super.widget.peer;
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (!isWebDesktop) {
|
||||
connectInPeerTab(context, peer, widget.tab);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDoubleTap: isWebDesktop
|
||||
? () => connectInPeerTab(context, peer, widget.tab)
|
||||
: null,
|
||||
onLongPress: () {
|
||||
peerTabModel.select(peer);
|
||||
},
|
||||
child: gestureDetector(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
||||
child: _buildPeerTile(context, peer, null)),
|
||||
@@ -86,7 +92,6 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
|
||||
Widget _buildLandscape() {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
var deco = Rx<BoxDecoration?>(
|
||||
BoxDecoration(
|
||||
@@ -115,30 +120,21 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onDoubleTap:
|
||||
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
|
||||
? null
|
||||
: () => widget.connect(context, peer.id),
|
||||
onTap: () => peerTabModel.select(peer),
|
||||
onLongPress: () => peerTabModel.select(peer),
|
||||
child: gestureDetector(
|
||||
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
||||
? _buildPeerCard(context, peer, deco)
|
||||
: _buildPeerTile(context, peer, deco))),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
makeChild(bool isPortrait, Peer peer) {
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
makeChild(bool isPortrait) => Row(
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
@@ -210,6 +206,12 @@ class _PeerCardState extends State<_PeerCard>
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
final colors = _frontN(peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
@@ -220,21 +222,22 @@ class _PeerCardState extends State<_PeerCard>
|
||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
Obx(() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue)
|
||||
: Container(
|
||||
Obx(
|
||||
() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue, peer)
|
||||
: Container(
|
||||
foregroundDecoration: deco.value,
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue),
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue, peer),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (colors.isNotEmpty)
|
||||
Obx(()=> Positioned(
|
||||
top: 2,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
))
|
||||
Obx(() => Positioned(
|
||||
top: 2,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1259,54 +1262,53 @@ void _rdpDialog(String id) async {
|
||||
],
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility))),
|
||||
controller: passwordController,
|
||||
)),
|
||||
),
|
||||
],
|
||||
))
|
||||
labelText:
|
||||
isDesktop ? null : translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
isDesktop ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () =>
|
||||
secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility))),
|
||||
controller: passwordController,
|
||||
)),
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -42,6 +44,14 @@ class LoadEvent {
|
||||
static const String group = 'load_group_peers';
|
||||
}
|
||||
|
||||
class PeersModelName {
|
||||
static const String recent = 'recent peer';
|
||||
static const String favorite = 'fav peer';
|
||||
static const String lan = 'discovered peer';
|
||||
static const String addressBook = 'address book peer';
|
||||
static const String group = 'group peer';
|
||||
}
|
||||
|
||||
/// for peer search text, global obs value
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
@@ -128,8 +138,9 @@ class _PeersViewState extends State<_PeersView>
|
||||
//
|
||||
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
|
||||
// we need the following comparison to ensure that `_isActive` is true in the end.
|
||||
if (isWindows && DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
if (isWindows &&
|
||||
DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
return;
|
||||
}
|
||||
_queryCount = _maxQueryCount;
|
||||
@@ -170,8 +181,8 @@ class _PeersViewState extends State<_PeersView>
|
||||
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => widget.peers,
|
||||
return ChangeNotifierProvider<Peers>.value(
|
||||
value: widget.peers,
|
||||
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||
if (peers.peers.isEmpty) {
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||
@@ -322,7 +333,12 @@ class _PeersViewState extends State<_PeersView>
|
||||
_queryOnlines(false);
|
||||
}
|
||||
} else {
|
||||
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
final skipIfIsWeb =
|
||||
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
|
||||
final skipIfMobile =
|
||||
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
|
||||
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
|
||||
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
@@ -403,28 +419,39 @@ class _PeersViewState extends State<_PeersView>
|
||||
}
|
||||
|
||||
abstract class BasePeersView extends StatelessWidget {
|
||||
final String name;
|
||||
final String loadEvent;
|
||||
final PeerTabIndex peerTabIndex;
|
||||
final PeerFilter? peerFilter;
|
||||
final PeerCardBuilder peerCardBuilder;
|
||||
final GetInitPeers? getInitPeers;
|
||||
|
||||
const BasePeersView({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.loadEvent,
|
||||
required this.peerTabIndex,
|
||||
this.peerFilter,
|
||||
required this.peerCardBuilder,
|
||||
required this.getInitPeers,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Peers peers;
|
||||
switch (peerTabIndex) {
|
||||
case PeerTabIndex.recent:
|
||||
peers = gFFI.recentPeersModel;
|
||||
break;
|
||||
case PeerTabIndex.fav:
|
||||
peers = gFFI.favoritePeersModel;
|
||||
break;
|
||||
case PeerTabIndex.lan:
|
||||
peers = gFFI.lanPeersModel;
|
||||
break;
|
||||
case PeerTabIndex.ab:
|
||||
peers = gFFI.abModel.peersModel;
|
||||
break;
|
||||
case PeerTabIndex.group:
|
||||
peers = gFFI.groupModel.peersModel;
|
||||
break;
|
||||
}
|
||||
return _PeersView(
|
||||
peers:
|
||||
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
|
||||
peerFilter: peerFilter,
|
||||
peerCardBuilder: peerCardBuilder);
|
||||
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,13 +460,11 @@ class RecentPeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'recent peer',
|
||||
loadEvent: LoadEvent.recent,
|
||||
peerTabIndex: PeerTabIndex.recent,
|
||||
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -455,13 +480,11 @@ class FavoritePeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'favorite peer',
|
||||
loadEvent: LoadEvent.favorite,
|
||||
peerTabIndex: PeerTabIndex.fav,
|
||||
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -477,13 +500,11 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'discovered peer',
|
||||
loadEvent: LoadEvent.lan,
|
||||
peerTabIndex: PeerTabIndex.lan,
|
||||
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -496,36 +517,38 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
|
||||
class AddressBookPeersView extends BasePeersView {
|
||||
AddressBookPeersView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required GetInitPeers getInitPeers})
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'address book peer',
|
||||
loadEvent: LoadEvent.addressBook,
|
||||
peerTabIndex: PeerTabIndex.ab,
|
||||
peerFilter: (Peer peer) =>
|
||||
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||
if (selectedTags.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
// The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations.
|
||||
final selectedNormalTags =
|
||||
selectedTags.where((tag) => tag != kUntagged).toList();
|
||||
if (selectedTags.contains(kUntagged)) {
|
||||
if (idents.isEmpty) return true;
|
||||
if (selectedNormalTags.isEmpty) return false;
|
||||
}
|
||||
if (gFFI.abModel.filterByIntersection.value) {
|
||||
for (final tag in selectedTags) {
|
||||
for (final tag in selectedNormalTags) {
|
||||
if (!idents.contains(tag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
for (final tag in selectedTags) {
|
||||
for (final tag in selectedNormalTags) {
|
||||
if (idents.contains(tag)) {
|
||||
return true;
|
||||
}
|
||||
@@ -537,20 +560,15 @@ class AddressBookPeersView extends BasePeersView {
|
||||
|
||||
class MyGroupPeerView extends BasePeersView {
|
||||
MyGroupPeerView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required GetInitPeers getInitPeers})
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'group peer',
|
||||
loadEvent: LoadEvent.group,
|
||||
peerTabIndex: PeerTabIndex.group,
|
||||
peerFilter: filter,
|
||||
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool filter(Peer peer) {
|
||||
|
||||
@@ -84,8 +84,17 @@ class _RawTouchGestureDetectorRegionState
|
||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||
double _scale = 1;
|
||||
|
||||
// Workaround tap down event when two fingers are used to scale(mobile)
|
||||
TapDownDetails? _lastTapDownDetails;
|
||||
|
||||
PointerDeviceKind? lastDeviceKind;
|
||||
|
||||
// For touch mode, onDoubleTap
|
||||
// `onDoubleTap()` does not provide the position of the tap event.
|
||||
Offset _lastPosOfDoubleTapDown = Offset.zero;
|
||||
bool _touchModePanStarted = false;
|
||||
Offset _doubleFinerTapPosition = Offset.zero;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
InputModel get inputModel => widget.inputModel;
|
||||
@@ -100,152 +109,190 @@ class _RawTouchGestureDetectorRegionState
|
||||
);
|
||||
}
|
||||
|
||||
onTapDown(TapDownDetails d) {
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
// Desktop or mobile "Touch mode"
|
||||
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||
inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
_lastTapDownDetails = d;
|
||||
}
|
||||
}
|
||||
|
||||
onTapUp(TapUpDetails d) {
|
||||
onTapUp(TapUpDetails d) async {
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||
inputModel.tapUp(MouseButtons.left);
|
||||
final isMoved =
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
if (isMoved) {
|
||||
if (lastTapDownDetails != null) {
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTap() {
|
||||
onTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Mobile, "Mouse mode"
|
||||
inputModel.tap(MouseButtons.left);
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleTapDown(TapDownDetails d) {
|
||||
onDoubleTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
onDoubleTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||
return;
|
||||
}
|
||||
inputModel.tap(MouseButtons.left);
|
||||
inputModel.tap(MouseButtons.left);
|
||||
if (handleTouch &&
|
||||
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
|
||||
return;
|
||||
}
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
|
||||
onLongPressDown(LongPressDownDetails d) {
|
||||
onLongPressDown(LongPressDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
_cacheLongPressPosition = d.localPosition;
|
||||
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||
return;
|
||||
}
|
||||
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressUp() {
|
||||
onLongPressUp() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
inputModel.tapUp(MouseButtons.left);
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
// for mobiles
|
||||
onLongPress() {
|
||||
onLongPress() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel
|
||||
final isMoved = await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
if (!isMoved) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!ffi.ffiModel.isPeerMobile) {
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
|
||||
onDoubleFinerTapDown(TapDownDetails d) {
|
||||
onDoubleFinerTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
_doubleFinerTapPosition = d.localPosition;
|
||||
// ignore for desktop and mobile
|
||||
}
|
||||
|
||||
onDoubleFinerTap(TapDownDetails d) {
|
||||
onDoubleFinerTap(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
|
||||
inputModel.tap(MouseButtons.right);
|
||||
|
||||
// mobile mouse mode or desktop touch screen
|
||||
final isMobileMouseMode = isMobile && !ffiModel.touchMode;
|
||||
// We can't use `d.localPosition` here because it's always (0, 0) on desktop.
|
||||
final isDesktopInRemoteRect = (isDesktop || isWebDesktop) &&
|
||||
ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition);
|
||||
if (isMobileMouseMode || isDesktopInRemoteRect) {
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragStart(DragStartDetails d) {
|
||||
onHoldDragStart(DragStartDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragUpdate(DragUpdateDetails d) {
|
||||
onHoldDragUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragEnd(DragEndDetails d) {
|
||||
onHoldDragEnd(DragEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanStart(BuildContext context, DragStartDetails d) {
|
||||
onOneFingerPanStart(BuildContext context, DragStartDetails d) async {
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
lastDeviceKind = d.kind ?? lastDeviceKind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
if (lastTapDownDetails != null) {
|
||||
await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx,
|
||||
lastTapDownDetails.localPosition.dy);
|
||||
}
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
return;
|
||||
}
|
||||
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_touchModePanStarted = true;
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||
}
|
||||
|
||||
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||
// If the time interval between the start event and the first pan event is less than 500ms,
|
||||
// we consider to use the long press position as the start position.
|
||||
@@ -253,11 +300,11 @@ class _RawTouchGestureDetectorRegionState
|
||||
// TODO: We should find a better way to send the first pan event as soon as possible.
|
||||
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
|
||||
500) {
|
||||
ffi.cursorModel
|
||||
await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
}
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
final offset = ffi.cursorModel.offset;
|
||||
final cursorX = offset.dx;
|
||||
@@ -266,39 +313,44 @@ class _RawTouchGestureDetectorRegionState
|
||||
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
|
||||
final size = MediaQueryData.fromView(View.of(context)).size;
|
||||
if (!visible.contains(Offset(cursorX, cursorY))) {
|
||||
ffi.cursorModel.move(size.width / 2, size.height / 2);
|
||||
await ffi.cursorModel.move(size.width / 2, size.height / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanUpdate(DragUpdateDetails d) {
|
||||
onOneFingerPanUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
return;
|
||||
}
|
||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
if (handleTouch && !_touchModePanStarted) {
|
||||
return;
|
||||
}
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) {
|
||||
onOneFingerPanEnd(DragEndDetails d) async {
|
||||
_touchModePanStarted = false;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
@@ -307,7 +359,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
_scale = d.scale;
|
||||
|
||||
if (scale != 0) {
|
||||
bind.sessionSendPointer(
|
||||
await bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(
|
||||
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
|
||||
@@ -322,21 +374,22 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) {
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
bind.sessionSendPointer(
|
||||
await bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(
|
||||
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
|
||||
} else {
|
||||
// mobile
|
||||
_scale = 1;
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
// No idea why we need to set the view style to "" here.
|
||||
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
|
||||
get onHoldDragCancel => null;
|
||||
|
||||
@@ -147,12 +147,23 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
child: Text(translate('Reset canvas')),
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
|
||||
connectWithToken(
|
||||
{required bool isFileTransfer, required bool isTcpTunneling}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
connToken: connToken);
|
||||
}
|
||||
|
||||
// transferFile
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
@@ -160,7 +171,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP tunneling')),
|
||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
// note
|
||||
@@ -183,7 +195,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
||||
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
||||
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
||||
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
|
||||
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||
const String kOptionAccessMode = "access-mode";
|
||||
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||
@@ -168,6 +169,13 @@ const int kWindowMainId = 0;
|
||||
const String kPointerEventKindTouch = "touch";
|
||||
const String kPointerEventKindMouse = "mouse";
|
||||
|
||||
const String kMouseEventTypeDefault = "";
|
||||
const String kMouseEventTypePanStart = "pan_start";
|
||||
const String kMouseEventTypePanUpdate = "pan_update";
|
||||
const String kMouseEventTypePanEnd = "pan_end";
|
||||
const String kMouseEventTypeDown = "down";
|
||||
const String kMouseEventTypeUp = "up";
|
||||
|
||||
const String kKeyFlutterKey = "flutter_key";
|
||||
|
||||
const String kKeyShowDisplaysAsIndividualWindows =
|
||||
@@ -570,3 +578,5 @@ enum WindowsTarget {
|
||||
extension WindowsTargetExt on int {
|
||||
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
||||
}
|
||||
|
||||
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||
@@ -3,8 +3,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -203,6 +203,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
// https://github.com/flutter/flutter/issues/157244
|
||||
Iterable<Peer> _autocompleteOpts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -323,43 +325,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
child: Ink(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate("id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
getConnectionPageTitle(context, false).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
@@ -375,7 +348,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
@@ -386,8 +359,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
);
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers
|
||||
_autocompleteOpts = peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
@@ -399,6 +371,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
@@ -459,6 +432,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
optionsViewBuilder: (BuildContext context,
|
||||
AutocompleteOnSelected<Peer> onSelected,
|
||||
Iterable<Peer> options) {
|
||||
options = _autocompleteOpts;
|
||||
double maxHeight = options.length * 50;
|
||||
if (options.length == 1) {
|
||||
maxHeight = 52;
|
||||
|
||||
@@ -272,10 +272,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
|
||||
buildPasswordBoard(BuildContext context) {
|
||||
final model = gFFI.serverModel;
|
||||
return ChangeNotifierProvider.value(
|
||||
value: gFFI.serverModel,
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, model, child) {
|
||||
return buildPasswordBoard2(context, model);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
buildPasswordBoard2(BuildContext context, ServerModel model) {
|
||||
RxBool refreshHover = false.obs;
|
||||
RxBool editHover = false.obs;
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
final showOneTime = model.approveMode != 'click' &&
|
||||
model.verificationMethod != kUsePermanentPassword;
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
|
||||
child: Row(
|
||||
@@ -304,8 +315,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () {
|
||||
if (model.verificationMethod !=
|
||||
kUsePermanentPassword) {
|
||||
if (showOneTime) {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: model.serverPasswd.text));
|
||||
showToast(translate("Copied"));
|
||||
@@ -323,22 +333,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotationWidget(
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
))),
|
||||
),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
if (showOneTime)
|
||||
AnimatedRotationWidget(
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
))),
|
||||
),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
if (!bind.isDisableSettings())
|
||||
InkWell(
|
||||
child: Tooltip(
|
||||
@@ -664,9 +675,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
(Map<String, dynamic> evt) async {
|
||||
if (evt['url'] is String) {
|
||||
setState(() {
|
||||
updateUrl = evt['url'];
|
||||
});
|
||||
}
|
||||
});
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (updateUrl.isNotEmpty) setState(() {});
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||
@@ -766,6 +785,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
final args = call.arguments.split(',');
|
||||
@@ -824,6 +844,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
_uniLinksSubscription?.cancel();
|
||||
Get.delete<RxBool>(tag: 'stop-service');
|
||||
_updateTimer?.cancel();
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -369,8 +369,8 @@ class _GeneralState extends State<_General> {
|
||||
|
||||
Widget theme() {
|
||||
final current = MyTheme.getThemeModePreference().toShortString();
|
||||
onChanged(String value) {
|
||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
|
||||
onChanged(String value) async {
|
||||
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -575,12 +575,18 @@ class _GeneralState extends State<_General> {
|
||||
bool root_dir_exists = map['root_dir_exists']!;
|
||||
bool user_dir_exists = map['user_dir_exists']!;
|
||||
return _Card(title: 'Recording', children: [
|
||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||
kOptionAllowAutoRecordIncoming),
|
||||
if (showRootDir)
|
||||
if (!bind.isOutgoingOnly())
|
||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||
kOptionAllowAutoRecordIncoming),
|
||||
if (!bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Automatically record outgoing sessions',
|
||||
kOptionAllowAutoRecordOutgoing,
|
||||
isServer: false),
|
||||
if (showRootDir && !bind.isOutgoingOnly())
|
||||
Row(
|
||||
children: [
|
||||
Text('${translate("Incoming")}:'),
|
||||
Text(
|
||||
'${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: root_dir_exists
|
||||
@@ -597,45 +603,49 @@ class _GeneralState extends State<_General> {
|
||||
),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
Row(
|
||||
children: [
|
||||
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: user_dir_exists
|
||||
? () => launchUrl(Uri.file(user_dir))
|
||||
: null,
|
||||
child: Text(
|
||||
user_dir,
|
||||
softWrap: true,
|
||||
style: user_dir_exists
|
||||
? const TextStyle(decoration: TextDecoration.underline)
|
||||
if (!(showRootDir && bind.isIncomingOnly()))
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: user_dir_exists
|
||||
? () => launchUrl(Uri.file(user_dir))
|
||||
: null,
|
||||
)).marginOnly(left: 10),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
||||
? null
|
||||
: () async {
|
||||
String? initialDirectory;
|
||||
if (await Directory.fromUri(Uri.directory(user_dir))
|
||||
.exists()) {
|
||||
initialDirectory = user_dir;
|
||||
}
|
||||
String? selectedDirectory =
|
||||
await FilePicker.platform.getDirectoryPath(
|
||||
initialDirectory: initialDirectory);
|
||||
if (selectedDirectory != null) {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionVideoSaveDirectory,
|
||||
value: selectedDirectory);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: Text(translate('Change')))
|
||||
.marginOnly(left: 5),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
child: Text(
|
||||
user_dir,
|
||||
softWrap: true,
|
||||
style: user_dir_exists
|
||||
? const TextStyle(
|
||||
decoration: TextDecoration.underline)
|
||||
: null,
|
||||
)).marginOnly(left: 10),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
||||
? null
|
||||
: () async {
|
||||
String? initialDirectory;
|
||||
if (await Directory.fromUri(
|
||||
Uri.directory(user_dir))
|
||||
.exists()) {
|
||||
initialDirectory = user_dir;
|
||||
}
|
||||
String? selectedDirectory =
|
||||
await FilePicker.platform.getDirectoryPath(
|
||||
initialDirectory: initialDirectory);
|
||||
if (selectedDirectory != null) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionVideoSaveDirectory,
|
||||
value: selectedDirectory);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: Text(translate('Change')))
|
||||
.marginOnly(left: 5),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -1019,7 +1029,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
translate('Accept sessions via both'),
|
||||
];
|
||||
var modeInitialKey = model.approveMode;
|
||||
if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
|
||||
if (!modeKeys.contains(modeInitialKey)) {
|
||||
modeInitialKey = defaultOptionApproveMode;
|
||||
}
|
||||
final usePassword = model.approveMode != 'click';
|
||||
|
||||
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||
@@ -1451,8 +1463,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
children: [
|
||||
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
||||
idErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||
relayController, relayErrMsg.value, enabled, secure)),
|
||||
if (!isWeb)
|
||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||
relayController, relayErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'API Server',
|
||||
apiController, apiErrMsg.value, enabled, secure)),
|
||||
_LabeledTextField(
|
||||
|
||||
@@ -17,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
@@ -55,14 +57,16 @@ class FileManagerPage extends StatefulWidget {
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.isSharedPassword,
|
||||
required this.tabController,
|
||||
this.tabController,
|
||||
this.connToken,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
final DesktopTabController tabController;
|
||||
final String? connToken;
|
||||
final DesktopTabController? tabController;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
@@ -88,6 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
connToken: widget.connToken,
|
||||
forceRelay: widget.forceRelay);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
@@ -97,11 +102,14 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
if (isWeb) {
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
@@ -140,10 +148,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
model.localController, _ffi, _mouseFocusScope))),
|
||||
if (!isWeb)
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
model.localController, _ffi, _mouseFocusScope))),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
@@ -192,7 +201,13 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return Icon(Icons.delete_outline, color: color);
|
||||
default:
|
||||
return Transform.rotate(
|
||||
angle: job.isRemoteToLocal ? pi : 0,
|
||||
angle: isWeb
|
||||
? job.isRemoteToLocal
|
||||
? pi / 2
|
||||
: pi / 2 * 3
|
||||
: job.isRemoteToLocal
|
||||
? pi
|
||||
: 0,
|
||||
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||
);
|
||||
}
|
||||
@@ -478,6 +493,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
|
||||
Widget headTools() {
|
||||
var uploadButtonTapPosition = RelativeRect.fill;
|
||||
RxBool isUploadFolder =
|
||||
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
|
||||
return Container(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -800,6 +818,66 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isWeb)
|
||||
Obx(() => ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||
isLocal
|
||||
? EdgeInsets.only(left: 10)
|
||||
: EdgeInsets.only(right: 10)),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
selectedItems.items.isEmpty
|
||||
? MyTheme.accent80
|
||||
: MyTheme.accent,
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
{webselectFiles(is_folder: isUploadFolder.value)},
|
||||
label: InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onTapDown: (e) {
|
||||
final x = e.globalPosition.dx;
|
||||
final y = e.globalPosition.dy;
|
||||
uploadButtonTapPosition =
|
||||
RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onTap: () async {
|
||||
final value = await showMenu<bool>(
|
||||
context: context,
|
||||
position: uploadButtonTapPosition,
|
||||
items: [
|
||||
PopupMenuItem<bool>(
|
||||
value: false,
|
||||
child: Text(translate('Upload files')),
|
||||
),
|
||||
PopupMenuItem<bool>(
|
||||
value: true,
|
||||
child: Text(translate('Upload folder')),
|
||||
),
|
||||
]);
|
||||
if (value != null) {
|
||||
isUploadFolder.value = value;
|
||||
bind.mainSetLocalOption(
|
||||
key: 'upload-folder-button',
|
||||
value: value ? 'Y' : '');
|
||||
webselectFiles(is_folder: value);
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
icon: Text(
|
||||
translate(isUploadFolder.isTrue
|
||||
? 'Upload folder'
|
||||
: 'Upload files'),
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
).marginOnly(left: 8),
|
||||
)).marginOnly(left: 16),
|
||||
Obx(() => ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||
@@ -833,19 +911,22 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: Colors.white,
|
||||
),
|
||||
)
|
||||
: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
: isWeb
|
||||
? Offstage()
|
||||
: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
label: isLocal
|
||||
? SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
@@ -857,7 +938,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: Colors.white),
|
||||
)
|
||||
: Text(
|
||||
translate('Receive'),
|
||||
translate(isWeb ? 'Download' : 'Receive'),
|
||||
style: TextStyle(
|
||||
color: selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
@@ -1020,7 +1101,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
if (!entry.isDrive &&
|
||||
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
|
||||
mod_menu.PopupMenuItem(
|
||||
child: Text("Rename"),
|
||||
child: Text(translate("Rename")),
|
||||
height: CustomPopupMenuTheme.height,
|
||||
onTap: () {
|
||||
controller.renameAction(entry, isLocal);
|
||||
|
||||
@@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
super.initState();
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
debugPrint(
|
||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewFileTransfer) {
|
||||
@@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
|
||||
@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
required this.isRDP,
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
final bool isRDP;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
@@ -62,6 +64,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
isRdp: widget.isRDP);
|
||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||
debugPrint("Port forward page init success with id ${widget.id}");
|
||||
|
||||
@@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
tabController: tabController,
|
||||
isRDP: isRDP,
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
isRDP: isRDP,
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
|
||||
@@ -115,6 +115,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
_ffi.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||
});
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
@@ -253,7 +255,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.imageModel.disposeImage();
|
||||
_ffi.cursorModel.disposeImages();
|
||||
_ffi.recordingModel.onClose();
|
||||
_rawKeyFocusNode.dispose();
|
||||
await _ffi.close(closeSession: closeSession);
|
||||
_timer?.cancel();
|
||||
|
||||
@@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
RemoteCountState.find().value = tabController.length;
|
||||
|
||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||
print(
|
||||
debugPrint(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
|
||||
@@ -305,7 +305,7 @@ class RemoteMenuEntry {
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
'${translate("Insert")} Ctrl + Alt + Del',
|
||||
translate("Insert Ctrl + Alt + Del"),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
shadowColor: MyTheme.color(context).shadow,
|
||||
borderRadius: borderRadius,
|
||||
child: _DraggableShowHide(
|
||||
id: widget.id,
|
||||
sessionId: widget.ffi.sessionId,
|
||||
dragging: _dragging,
|
||||
fractionX: _fractionX,
|
||||
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
setFullscreen: _setFullscreen,
|
||||
));
|
||||
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
if (!isWeb) {
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||
}
|
||||
if (!isWeb) toolbarItems.add(_RecordMenu());
|
||||
@@ -1780,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
if (isWeb) {
|
||||
return buildTextChatButton();
|
||||
} else {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
}
|
||||
}
|
||||
|
||||
buildTextChatButton() {
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/message_24dp_5F6368.svg',
|
||||
tooltip: 'Text chat',
|
||||
key: chatButtonKey,
|
||||
onPressed: _textChatOnPressed,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
);
|
||||
}
|
||||
|
||||
textChat() {
|
||||
return MenuButton(
|
||||
child: Text(translate('Text chat')),
|
||||
ffi: widget.ffi,
|
||||
onPressed: () {
|
||||
RenderBox? renderBox =
|
||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
onPressed: _textChatOnPressed);
|
||||
}
|
||||
|
||||
Offset? initPos;
|
||||
if (renderBox != null) {
|
||||
final pos = renderBox.localToGlobal(Offset.zero);
|
||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||
}
|
||||
|
||||
widget.ffi.chatModel.changeCurrentKey(
|
||||
MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||
});
|
||||
_textChatOnPressed() {
|
||||
RenderBox? renderBox =
|
||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
Offset? initPos;
|
||||
if (renderBox != null) {
|
||||
final pos = renderBox.localToGlobal(Offset.zero);
|
||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||
}
|
||||
widget.ffi.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||
}
|
||||
|
||||
voiceCall() {
|
||||
@@ -1908,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
|
||||
var ffi = Provider.of<FfiModel>(context);
|
||||
var recordingModel = Provider.of<RecordingModel>(context);
|
||||
final visible =
|
||||
(recordingModel.start || ffi.permissions['recording'] != false) &&
|
||||
ffi.pi.currentDisplay != kAllDisplayValue;
|
||||
(recordingModel.start || ffi.permissions['recording'] != false);
|
||||
if (!visible) return Offstage();
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/rec.svg',
|
||||
@@ -2218,6 +2233,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _DraggableShowHide extends StatefulWidget {
|
||||
final String id;
|
||||
final SessionID sessionId;
|
||||
final RxDouble fractionX;
|
||||
final RxBool dragging;
|
||||
@@ -2229,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget {
|
||||
|
||||
const _DraggableShowHide({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.sessionId,
|
||||
required this.fractionX,
|
||||
required this.dragging,
|
||||
@@ -2318,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
);
|
||||
final isFullscreen = stateGlobal.fullscreen;
|
||||
const double iconSize = 20;
|
||||
|
||||
buttonWrapper(VoidCallback? onPressed, Widget child,
|
||||
{Color hoverColor = _ToolbarTheme.blueColor}) {
|
||||
final bgColor = buttonStyle.backgroundColor?.resolve({});
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
style: buttonStyle.copyWith(
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return (bgColor ?? hoverColor).withOpacity(0.15);
|
||||
}
|
||||
return bgColor;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDraggable(context),
|
||||
Obx(() => TextButton(
|
||||
onPressed: () {
|
||||
Obx(() => buttonWrapper(
|
||||
() {
|
||||
widget.setFullscreen(!isFullscreen.value);
|
||||
},
|
||||
child: Tooltip(
|
||||
Tooltip(
|
||||
message: translate(
|
||||
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||
child: Icon(
|
||||
@@ -2337,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (!isMacOS)
|
||||
if (!isMacOS && !isWebDesktop)
|
||||
Obx(() => Offstage(
|
||||
offstage: isFullscreen.isFalse,
|
||||
child: TextButton(
|
||||
onPressed: () => widget.setMinimize(),
|
||||
child: Tooltip(
|
||||
child: buttonWrapper(
|
||||
widget.setMinimize,
|
||||
Tooltip(
|
||||
message: translate('Minimize'),
|
||||
child: Icon(
|
||||
Icons.remove,
|
||||
@@ -2351,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => setState(() {
|
||||
buttonWrapper(
|
||||
() => setState(() {
|
||||
widget.toolbarState.switchShow(widget.sessionId);
|
||||
}),
|
||||
child: Obx((() => Tooltip(
|
||||
Obx((() => Tooltip(
|
||||
message:
|
||||
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
child: Icon(
|
||||
@@ -2364,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
))),
|
||||
),
|
||||
if (isWebDesktop)
|
||||
Obx(() {
|
||||
if (show.isTrue) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return buttonWrapper(
|
||||
() => closeConnection(id: widget.id),
|
||||
Tooltip(
|
||||
message: translate('Close'),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: iconSize,
|
||||
color: _ToolbarTheme.redColor,
|
||||
),
|
||||
),
|
||||
hoverColor: _ToolbarTheme.redColor,
|
||||
).paddingOnly(left: iconSize / 2);
|
||||
}
|
||||
})
|
||||
],
|
||||
);
|
||||
return TextButtonTheme(
|
||||
|
||||
@@ -36,6 +36,7 @@ WindowType? kWindowType;
|
||||
late List<String> kBootArgs;
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
earlyAssert();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
debugPrint("launch args: $args");
|
||||
@@ -161,7 +162,7 @@ void runMobileApp() async {
|
||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
if (!isWeb) await initUniLinks();
|
||||
await initUniLinks();
|
||||
}
|
||||
|
||||
void runMultiWindow(
|
||||
@@ -444,7 +445,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
child: GetMaterialApp(
|
||||
navigatorKey: globalKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'RustDesk',
|
||||
title: isWeb
|
||||
? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
|
||||
: bind.mainGetAppNameSync(),
|
||||
theme: MyTheme.lightTheme,
|
||||
darkTheme: MyTheme.darkTheme,
|
||||
themeMode: MyTheme.currentThemeMode(),
|
||||
@@ -475,7 +478,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
: (context, child) {
|
||||
child = _keepScaleBuilder(context, child);
|
||||
child = botToastBuilder(context, child);
|
||||
if (isDesktop && desktopType == DesktopType.main) {
|
||||
if ((isDesktop && desktopType == DesktopType.main) ||
|
||||
isWebDesktop) {
|
||||
child = keyListenerBuilder(context, child);
|
||||
}
|
||||
if (isLinux) {
|
||||
@@ -503,7 +507,7 @@ _registerEventHandler() {
|
||||
platformFFI.registerEventHandler('theme', 'theme', (evt) async {
|
||||
String? dark = evt['dark'];
|
||||
if (dark != null) {
|
||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
||||
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
||||
}
|
||||
});
|
||||
platformFFI.registerEventHandler('language', 'language', (_) async {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:auto_size_text_field/auto_size_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -47,6 +48,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157244
|
||||
Iterable<Peer> _autocompleteOpts = [];
|
||||
|
||||
_ConnectionPageState() {
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
_idController.addListener(() {
|
||||
@@ -70,9 +74,17 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
if (isAndroid) {
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
(Map<String, dynamic> evt) async {
|
||||
if (evt['url'] is String) {
|
||||
setState(() {
|
||||
_updateUrl = evt['url'];
|
||||
});
|
||||
}
|
||||
});
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (_updateUrl.isNotEmpty) setState(() {});
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -157,7 +169,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
@@ -173,7 +185,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
@@ -185,7 +197,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers
|
||||
_autocompleteOpts = peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
@@ -197,12 +209,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
fieldViewBuilder: (BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(
|
||||
fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idEmpty.value =
|
||||
fieldTextEditingController.text.isEmpty;
|
||||
@@ -263,6 +278,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
optionsViewBuilder: (BuildContext context,
|
||||
AutocompleteOnSelected<Peer> onSelected,
|
||||
Iterable<Peer> options) {
|
||||
options = _autocompleteOpts;
|
||||
double maxHeight = options.length * 50;
|
||||
if (options.length == 1) {
|
||||
maxHeight = 52;
|
||||
@@ -341,9 +357,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
final child = Column(children: [
|
||||
if (isWebDesktop)
|
||||
getConnectionPageTitle(context, true)
|
||||
.marginOnly(bottom: 10, top: 15, left: 12),
|
||||
w
|
||||
]);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(constraints: kMobilePageConstraints, child: w));
|
||||
child: Container(constraints: kMobilePageConstraints, child: child));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -353,6 +375,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()) {
|
||||
Get.delete<TextEditingController>();
|
||||
}
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/state_model.dart';
|
||||
import 'connection_page.dart';
|
||||
|
||||
abstract class PageShape extends Widget {
|
||||
@@ -159,14 +160,75 @@ class WebHomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
stateGlobal.isInMainPage = true;
|
||||
handleUnilink(context);
|
||||
return Scaffold(
|
||||
// backgroundColor: MyTheme.grayBg,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(bind.mainGetAppNameSync()),
|
||||
title: Text("${bind.mainGetAppNameSync()} (Preview)"),
|
||||
actions: connectionPage.appBarActions,
|
||||
),
|
||||
body: connectionPage,
|
||||
);
|
||||
}
|
||||
|
||||
handleUnilink(BuildContext context) {
|
||||
if (webInitialLink.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final link = webInitialLink;
|
||||
webInitialLink = '';
|
||||
final splitter = ["/#/", "/#", "#/", "#"];
|
||||
var fakelink = '';
|
||||
for (var s in splitter) {
|
||||
if (link.contains(s)) {
|
||||
var list = link.split(s);
|
||||
if (list.length < 2 || list[1].isEmpty) {
|
||||
return;
|
||||
}
|
||||
list.removeAt(0);
|
||||
fakelink = "rustdesk://${list.join(s)}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fakelink.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final uri = Uri.tryParse(fakelink);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
final args = urlLinkToCmdArgs(uri);
|
||||
if (args == null || args.isEmpty) {
|
||||
return;
|
||||
}
|
||||
bool isFileTransfer = false;
|
||||
String? id;
|
||||
String? password;
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--connect':
|
||||
case '--play':
|
||||
isFileTransfer = false;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--file-transfer':
|
||||
isFileTransfer = true;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
connect(context, id, isFileTransfer: isFileTransfer, password: password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -25,6 +26,19 @@ import '../widgets/dialog.dart';
|
||||
|
||||
final initText = '1' * 1024;
|
||||
|
||||
// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
|
||||
// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
|
||||
// https://github.com/flutter/flutter/issues/159384
|
||||
// https://github.com/flutter/flutter/issues/159383
|
||||
void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||
if (isAndroid) {
|
||||
if (isKeyboardVisible != true) {
|
||||
// `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
: super(key: key);
|
||||
@@ -37,12 +51,15 @@ class RemotePage extends StatefulWidget {
|
||||
State<RemotePage> createState() => _RemotePageState(id);
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage> {
|
||||
class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
Timer? _timer;
|
||||
bool _showBar = !isWebDesktop;
|
||||
bool _showGestureHelp = false;
|
||||
String _value = '';
|
||||
Orientation? _currentOrientation;
|
||||
double _viewInsetsBottom = 0;
|
||||
|
||||
Timer? _timerDidChangeMetrics;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
@@ -89,10 +106,21 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
gFFI.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
|
||||
if (gFFI.recordingModel.start) {
|
||||
showToast(translate('Automatically record outgoing sessions'));
|
||||
}
|
||||
_disableAndroidSoftKeyboard(
|
||||
isKeyboardVisible: keyboardVisibilityController.isVisible);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
// https://github.com/flutter/flutter/issues/64935
|
||||
super.dispose();
|
||||
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
||||
@@ -104,6 +132,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_physicalFocusNode.dispose();
|
||||
await gFFI.close();
|
||||
_timer?.cancel();
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
@@ -118,6 +147,26 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
|
||||
// Don't try reset the view style and focus the cursor.
|
||||
if (gFFI.cursorModel.lastKeyboardIsVisible &&
|
||||
gFFI.canvasModel.isMobileCanvasChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
|
||||
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
|
||||
if (newBottom != _viewInsetsBottom) {
|
||||
gFFI.canvasModel.mobileFocusCanvasCursor();
|
||||
_viewInsetsBottom = newBottom;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||
// But for now, the transparent color will cause the canvas to be white.
|
||||
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
||||
@@ -155,9 +204,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
var oldValue = _value;
|
||||
_value = newValue;
|
||||
var i = newValue.length - 1;
|
||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||
for (; i >= 0 && newValue[i] != '1'; --i) {}
|
||||
var j = oldValue.length - 1;
|
||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||
for (; j >= 0 && oldValue[j] != '1'; --j) {}
|
||||
if (i < j) j = i;
|
||||
var subNewValue = newValue.substring(j + 1);
|
||||
var subOldValue = oldValue.substring(j + 1);
|
||||
@@ -206,8 +255,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_value = newValue;
|
||||
if (oldValue.isNotEmpty &&
|
||||
newValue.isNotEmpty &&
|
||||
oldValue[0] == '\1' &&
|
||||
newValue[0] != '\1') {
|
||||
oldValue[0] == '1' &&
|
||||
newValue[0] != '1') {
|
||||
// clipboard
|
||||
oldValue = '';
|
||||
}
|
||||
@@ -511,7 +560,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
right: 10,
|
||||
child: QualityMonitor(gFFI.qualityMonitorModel),
|
||||
),
|
||||
KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)),
|
||||
KeyHelpTools(
|
||||
keyboardIsVisible: keyboardIsVisible,
|
||||
showGestureHelp: _showGestureHelp),
|
||||
SizedBox(
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -531,6 +582,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
controller: _textController,
|
||||
// trick way to make backspace work always
|
||||
keyboardType: TextInputType.multiline,
|
||||
// `onChanged` may be called depending on the input method if this widget is wrapped in
|
||||
// `Focus(onKeyEvent: ..., child: ...)`
|
||||
// For `Backspace` button in the soft keyboard:
|
||||
// en/fr input method:
|
||||
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
|
||||
// 2. The button will trigger `onKeyEvent` if the text field is empty.
|
||||
// ko/zh/ja input method: the button will trigger `onKeyEvent`
|
||||
// and the event will not popup if `KeyEventResult.handled` is returned.
|
||||
onChanged: handleSoftKeyboardInput,
|
||||
),
|
||||
),
|
||||
@@ -740,10 +799,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
|
||||
class KeyHelpTools extends StatefulWidget {
|
||||
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
|
||||
final bool requestShow;
|
||||
final bool keyboardIsVisible;
|
||||
final bool showGestureHelp;
|
||||
|
||||
KeyHelpTools({required this.requestShow});
|
||||
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
|
||||
bool get requestShow => keyboardIsVisible || showGestureHelp;
|
||||
|
||||
KeyHelpTools(
|
||||
{required this.keyboardIsVisible, required this.showGestureHelp});
|
||||
|
||||
@override
|
||||
State<KeyHelpTools> createState() => _KeyHelpToolsState();
|
||||
@@ -788,7 +851,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
final size = renderObject.size;
|
||||
Offset pos = renderObject.localToGlobal(Offset.zero);
|
||||
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
|
||||
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
|
||||
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height),
|
||||
widget.keyboardIsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,7 +864,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
inputModel.command;
|
||||
|
||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
|
||||
gFFI.cursorModel
|
||||
.keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible);
|
||||
return Offstage();
|
||||
}
|
||||
final size = MediaQuery.of(context).size;
|
||||
@@ -937,11 +1002,11 @@ class ImagePaint extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||
var s = c.scale;
|
||||
final adjust = c.getAdjustY();
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s),
|
||||
image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -955,7 +1020,6 @@ class CursorPaint extends StatelessWidget {
|
||||
final m = Provider.of<CursorModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||
final s = c.scale;
|
||||
double hotx = m.hotx;
|
||||
double hoty = m.hoty;
|
||||
@@ -987,11 +1051,12 @@ class CursorPaint extends StatelessWidget {
|
||||
factor = s / mins;
|
||||
}
|
||||
final s2 = s < mins ? mins : s;
|
||||
final adjust = c.getAdjustY();
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
image: image,
|
||||
x: (m.x - hotx) * factor + c.x / s2,
|
||||
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
|
||||
y: (m.y - hoty) * factor + (c.y + adjust) / s2,
|
||||
scale: s2),
|
||||
);
|
||||
}
|
||||
@@ -1194,7 +1259,9 @@ void showOptions(
|
||||
toggles +
|
||||
[privacyModeWidget]),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||
_disableAndroidSoftKeyboard();
|
||||
});
|
||||
}
|
||||
|
||||
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||
@@ -1213,7 +1280,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||
_disableAndroidSoftKeyboard();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1255,7 +1324,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||
_disableAndroidSoftKeyboard();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
|
||||
class _ScanPageState extends State<ScanPage> {
|
||||
QRViewController? controller;
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
StreamSubscription? scanSubscription;
|
||||
|
||||
// In order to get hot reload to work we need to pause the camera if the platform
|
||||
// is android, or resume the camera if the platform is iOS.
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
if (isAndroid) {
|
||||
if (isAndroid && controller != null) {
|
||||
controller!.pauseCamera();
|
||||
} else if (controller != null) {
|
||||
controller!.resumeCamera();
|
||||
}
|
||||
controller!.resumeCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR'),
|
||||
actions: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.image_search),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? file =
|
||||
await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
var image = img.decodeNamedImage(
|
||||
file.path, File(file.path).readAsBytesSync())!;
|
||||
|
||||
LuminanceSource source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List());
|
||||
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||
|
||||
var reader = QRCodeReader();
|
||||
try {
|
||||
var result = reader.decode(bitmap);
|
||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||
handleUriLink(uriString: result.text);
|
||||
} else {
|
||||
showServerSettingFromQr(result.text);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('No QR code found');
|
||||
}
|
||||
}
|
||||
}),
|
||||
IconButton(
|
||||
color: Colors.yellow,
|
||||
icon: Icon(Icons.flash_on),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.toggleFlash();
|
||||
}),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.switch_camera),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.flipCamera();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildQrView(context));
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR'),
|
||||
actions: [
|
||||
_buildImagePickerButton(),
|
||||
_buildFlashToggleButton(),
|
||||
_buildCameraSwitchButton(),
|
||||
],
|
||||
),
|
||||
body: _buildQrView(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrView(BuildContext context) {
|
||||
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
|
||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
||||
MediaQuery.of(context).size.height < 400)
|
||||
var scanArea = MediaQuery.of(context).size.width < 400 ||
|
||||
MediaQuery.of(context).size.height < 400
|
||||
? 150.0
|
||||
: 300.0;
|
||||
// To ensure the Scanner view is properly sizes after rotation
|
||||
// we need to listen for Flutter SizeChanged notification and update controller
|
||||
return QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: _onQRViewCreated,
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: Colors.red,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: scanArea),
|
||||
borderColor: Colors.red,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: scanArea,
|
||||
),
|
||||
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
||||
);
|
||||
}
|
||||
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
setState(() {
|
||||
this.controller = controller;
|
||||
});
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
scanSubscription = controller.scannedDataStream.listen((scanData) {
|
||||
if (scanData.code != null) {
|
||||
showServerSettingFromQr(scanData.code!);
|
||||
}
|
||||
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
try {
|
||||
var image = img.decodeImage(await File(file.path).readAsBytes())!;
|
||||
LuminanceSource source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(),
|
||||
);
|
||||
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||
|
||||
var reader = QRCodeReader();
|
||||
var result = reader.decode(bitmap);
|
||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||
handleUriLink(uriString: result.text);
|
||||
} else {
|
||||
showServerSettingFromQr(result.text);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('No QR code found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImagePickerButton() {
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.image_search),
|
||||
iconSize: 32.0,
|
||||
onPressed: _pickImage,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFlashToggleButton() {
|
||||
return IconButton(
|
||||
color: Colors.yellow,
|
||||
icon: Icon(Icons.flash_on),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.toggleFlash();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraSwitchButton() {
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.switch_camera),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.flipCamera();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scanSubscription?.cancel();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -446,7 +446,6 @@ class ServerInfo extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPermanent = model.verificationMethod == kUsePermanentPassword;
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
|
||||
const Color colorPositive = Colors.green;
|
||||
@@ -486,6 +485,8 @@ class ServerInfo extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
final showOneTime = serverModel.approveMode != 'click' &&
|
||||
serverModel.verificationMethod != kUsePermanentPassword;
|
||||
return PaddingCard(
|
||||
title: translate('Your Device'),
|
||||
child: Column(
|
||||
@@ -523,10 +524,10 @@ class ServerInfo extends StatelessWidget {
|
||||
]),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(
|
||||
isPermanent ? '-' : model.serverPasswd.value.text,
|
||||
!showOneTime ? '-' : model.serverPasswd.value.text,
|
||||
style: textStyleValue,
|
||||
),
|
||||
isPermanent
|
||||
!showOneTime
|
||||
? SizedBox.shrink()
|
||||
: Row(children: [
|
||||
IconButton(
|
||||
@@ -595,7 +596,9 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
translate("android_version_audio_tip"),
|
||||
style: const TextStyle(color: MyTheme.darkGray),
|
||||
))
|
||||
])
|
||||
]),
|
||||
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
|
||||
serverModel.toggleClipboard),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _enableRecordSession = false;
|
||||
var _enableHardwareCodec = false;
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _autoRecordOutgoingSession = false;
|
||||
var _allowAutoDisconnect = false;
|
||||
var _localIP = "";
|
||||
var _directAccessPort = "";
|
||||
@@ -104,6 +105,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||
bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
|
||||
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||
@@ -231,6 +234,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final incommingOnly = bind.isIncomingOnly();
|
||||
final customClientSection = CustomSettingsSection(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -674,32 +678,55 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
]),
|
||||
if (isAndroid && !outgoingOnly)
|
||||
if (isAndroid)
|
||||
SettingsSection(
|
||||
title: Text(translate("Recording")),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record incoming sessions')),
|
||||
leading: Icon(Icons.videocam),
|
||||
description: Text(
|
||||
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionAllowAutoRecordIncoming,
|
||||
value:
|
||||
bool2option(kOptionAllowAutoRecordIncoming, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordIncoming,
|
||||
await bind.mainGetOption(
|
||||
key: kOptionAllowAutoRecordIncoming));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
if (!outgoingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record incoming sessions')),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionAllowAutoRecordIncoming,
|
||||
value: bool2option(
|
||||
kOptionAllowAutoRecordIncoming, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordIncoming,
|
||||
await bind.mainGetOption(
|
||||
key: kOptionAllowAutoRecordIncoming));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incommingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record outgoing sessions')),
|
||||
initialValue: _autoRecordOutgoingSession,
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionAllowAutoRecordOutgoing,
|
||||
value: bool2option(
|
||||
kOptionAllowAutoRecordOutgoing, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordOutgoing,
|
||||
bind.mainGetLocalOption(
|
||||
key: kOptionAllowAutoRecordOutgoing));
|
||||
setState(() {
|
||||
_autoRecordOutgoingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(translate("Directory")),
|
||||
description: Text(bind.mainVideoSaveDirectory(root: false)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -205,14 +205,15 @@ void showServerSettingsWithValue(
|
||||
)
|
||||
] +
|
||||
[
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg.value.isEmpty
|
||||
? null
|
||||
: relayServerMsg.value),
|
||||
)
|
||||
if (isAndroid)
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg.value.isEmpty
|
||||
? null
|
||||
: relayServerMsg.value),
|
||||
)
|
||||
] +
|
||||
[
|
||||
TextFormField(
|
||||
|
||||
@@ -33,6 +33,8 @@ bool filterAbTagByIntersection() {
|
||||
const _personalAddressBookName = "My address book";
|
||||
const _legacyAddressBookName = "Legacy address book";
|
||||
|
||||
const kUntagged = "Untagged";
|
||||
|
||||
enum ForcePullAb {
|
||||
listAndCurrent,
|
||||
current,
|
||||
@@ -66,10 +68,16 @@ class AbModel {
|
||||
var listInitialized = false;
|
||||
var _maxPeerOneAb = 0;
|
||||
|
||||
late final Peers peersModel;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
AbModel(this.parent) {
|
||||
addressbooks.clear();
|
||||
peersModel = Peers(
|
||||
name: PeersModelName.addressBook,
|
||||
getInitPeers: () => currentAbPeers,
|
||||
loadEvent: LoadEvent.addressBook);
|
||||
if (desktopType == DesktopType.main) {
|
||||
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
||||
if (_timerCounter++ % 6 == 0) {
|
||||
@@ -418,6 +426,7 @@ class AbModel {
|
||||
|
||||
// #region tags
|
||||
Future<bool> addTags(List<String> tagList) async {
|
||||
tagList.removeWhere((e) => e == kUntagged);
|
||||
final ret = await current.addTags(tagList, {});
|
||||
await pullNonLegacyAfterChange();
|
||||
_saveCache();
|
||||
|
||||
@@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
|
||||
chatWindowOverlayEntry == null);
|
||||
_isChatOverlayHide() =>
|
||||
((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
|
||||
chatWindowOverlayEntry == null);
|
||||
|
||||
toggleChatOverlay({Offset? chatInitPos}) {
|
||||
if (_isChatOverlayHide()) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
if (!isDesktop) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
showChatIconOverlay();
|
||||
}
|
||||
showChatWindowOverlay(chatInitPos: chatInitPos);
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/utils/event_loop.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||
|
||||
import '../consts.dart';
|
||||
import 'model.dart';
|
||||
@@ -74,7 +76,7 @@ class FileModel {
|
||||
|
||||
Future<void> onReady() async {
|
||||
await evtLoop.onReady();
|
||||
await localController.onReady();
|
||||
if (!isWeb) await localController.onReady();
|
||||
await remoteController.onReady();
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ class FileModel {
|
||||
}
|
||||
|
||||
Future<void> refreshAll() async {
|
||||
await localController.refresh();
|
||||
if (!isWeb) await localController.refresh();
|
||||
await remoteController.refresh();
|
||||
}
|
||||
|
||||
@@ -98,6 +100,10 @@ class FileModel {
|
||||
fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
||||
}
|
||||
|
||||
void receiveEmptyDirs(Map<String, dynamic> evt) {
|
||||
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
|
||||
}
|
||||
|
||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||
evtLoop.pushEvent(
|
||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||
@@ -228,6 +234,33 @@ class FileModel {
|
||||
);
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
void onSelectedFiles(dynamic obj) {
|
||||
localController.selectedItems.clear();
|
||||
|
||||
try {
|
||||
int handleIndex = int.parse(obj['handleIndex']);
|
||||
final file = jsonDecode(obj['file']);
|
||||
var entry = Entry.fromJson(file);
|
||||
entry.path = entry.name;
|
||||
final otherSideData = remoteController.directoryData();
|
||||
final toPath = otherSideData.directory.path;
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
final jobID = jobController.addTransferJob(entry, false);
|
||||
webSendLocalFiles(
|
||||
handleIndex: handleIndex,
|
||||
actId: jobID,
|
||||
path: entry.path,
|
||||
to: PathUtil.join(toPath, entry.name, isWindows),
|
||||
fileNum: 0,
|
||||
includeHidden: showHidden,
|
||||
isRemote: false,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to decode onSelectedFiles: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryData {
|
||||
@@ -441,7 +474,8 @@ class FileController {
|
||||
}
|
||||
|
||||
/// sendFiles from current side (FileController.isLocal) to other side (SelectedItems).
|
||||
void sendFiles(SelectedItems items, DirectoryData otherSideData) {
|
||||
Future<void> sendFiles(
|
||||
SelectedItems items, DirectoryData otherSideData) async {
|
||||
/// ignore wrong items side status
|
||||
if (items.isLocal != isLocal) {
|
||||
return;
|
||||
@@ -462,10 +496,47 @@ class FileController {
|
||||
to: PathUtil.join(toPath, from.name, isWindows),
|
||||
fileNum: 0,
|
||||
includeHidden: showHidden,
|
||||
isRemote: isRemoteToLocal);
|
||||
isRemote: isRemoteToLocal,
|
||||
isDir: from.isDirectory);
|
||||
debugPrint(
|
||||
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
|
||||
}
|
||||
|
||||
if (!isLocal &&
|
||||
versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<Entry> entrys = items.items.toList();
|
||||
var isRemote = isLocal == true ? true : false;
|
||||
|
||||
await Future.forEach(entrys, (Entry item) async {
|
||||
if (!item.isDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> paths = [];
|
||||
|
||||
final emptyDirs =
|
||||
await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden);
|
||||
|
||||
if (emptyDirs.isEmpty) {
|
||||
return;
|
||||
} else {
|
||||
for (var dir in emptyDirs) {
|
||||
paths.add(dir.path);
|
||||
}
|
||||
}
|
||||
|
||||
final dirs = paths.map((path) {
|
||||
return PathUtil.getOtherSidePath(directory.value.path, path,
|
||||
options.value.isWindows, toPath, isWindows);
|
||||
});
|
||||
|
||||
for (var dir in dirs) {
|
||||
createDirWithRemote(dir, isRemote);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _removeCheckboxRemember = false;
|
||||
@@ -489,7 +560,7 @@ class FileController {
|
||||
} else if (item.isDirectory) {
|
||||
title = translate("Not an empty directory");
|
||||
dialogManager?.showLoading(translate("Waiting"));
|
||||
final fd = await fileFetcher.fetchDirectoryRecursive(
|
||||
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
if (fd.path.isEmpty) {
|
||||
fd.path = item.path;
|
||||
@@ -659,12 +730,16 @@ class FileController {
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
Future<void> createDir(String path) async {
|
||||
Future<void> createDirWithRemote(String path, bool isRemote) async {
|
||||
bind.sessionCreateDir(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
path: path,
|
||||
isRemote: !isLocal);
|
||||
isRemote: isRemote);
|
||||
}
|
||||
|
||||
Future<void> createDir(String path) async {
|
||||
await createDirWithRemote(path, !isLocal);
|
||||
}
|
||||
|
||||
Future<void> renameAction(Entry item, bool isLocal) async {
|
||||
@@ -809,7 +884,6 @@ class JobController {
|
||||
job.speed = double.parse(evt['speed']);
|
||||
job.finishedSize = int.parse(evt['finished_size']);
|
||||
job.recvJobRes = true;
|
||||
debugPrint("update job $id with $evt");
|
||||
jobTable.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1035,6 +1109,7 @@ class JobResultListener<T> {
|
||||
class FileFetcher {
|
||||
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
|
||||
Map<String, Completer<FileDirectory>> remoteTasks = {};
|
||||
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
|
||||
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
|
||||
|
||||
final GetSessionID getSessionID;
|
||||
@@ -1042,6 +1117,24 @@ class FileFetcher {
|
||||
|
||||
FileFetcher(this.getSessionID);
|
||||
|
||||
Future<List<FileDirectory>> registerReadEmptyDirsTask(
|
||||
bool isLocal, String path) {
|
||||
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
||||
final tasks = remoteEmptyDirsTasks; // bypass now
|
||||
if (tasks.containsKey(path)) {
|
||||
throw "Failed to registerReadEmptyDirsTask, already have same read job";
|
||||
}
|
||||
final c = Completer<List<FileDirectory>>();
|
||||
tasks[path] = c;
|
||||
|
||||
Timer(Duration(seconds: 2), () {
|
||||
tasks.remove(path);
|
||||
if (c.isCompleted) return;
|
||||
c.completeError("Failed to read empty dirs, timeout");
|
||||
});
|
||||
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
|
||||
@@ -1075,6 +1168,25 @@ class FileFetcher {
|
||||
return c.future;
|
||||
}
|
||||
|
||||
tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) {
|
||||
if (msg == null || isLocalStr == null) return;
|
||||
late final Map<String, Completer<List<FileDirectory>>> tasks;
|
||||
try {
|
||||
final map = jsonDecode(msg);
|
||||
final String path = map["path"];
|
||||
final List<dynamic> fdJsons = map["empty_dirs"];
|
||||
final List<FileDirectory> fds =
|
||||
fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
|
||||
|
||||
tasks = remoteEmptyDirsTasks;
|
||||
final completer = tasks.remove(path);
|
||||
|
||||
completer?.complete(fds);
|
||||
} catch (e) {
|
||||
debugPrint("tryCompleteJob err: $e");
|
||||
}
|
||||
}
|
||||
|
||||
tryCompleteTask(String? msg, String? isLocalStr) {
|
||||
if (msg == null || isLocalStr == null) return;
|
||||
late final Map<Object, Completer<FileDirectory>> tasks;
|
||||
@@ -1098,6 +1210,28 @@ class FileFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FileDirectory>> readEmptyDirs(
|
||||
String path, bool isLocal, bool showHidden) async {
|
||||
try {
|
||||
if (isLocal) {
|
||||
final res = await bind.sessionReadLocalEmptyDirsRecursiveSync(
|
||||
sessionId: sessionId, path: path, includeHidden: showHidden);
|
||||
|
||||
final List<dynamic> fdJsons = jsonDecode(res);
|
||||
|
||||
final List<FileDirectory> fds =
|
||||
fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
|
||||
return fds;
|
||||
} else {
|
||||
await bind.sessionReadRemoteEmptyDirsRecursiveSync(
|
||||
sessionId: sessionId, path: path, includeHidden: showHidden);
|
||||
return registerReadEmptyDirsTask(isLocal, path);
|
||||
}
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileDirectory> fetchDirectory(
|
||||
String path, bool isLocal, bool showHidden) async {
|
||||
try {
|
||||
@@ -1116,11 +1250,11 @@ class FileFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileDirectory> fetchDirectoryRecursive(
|
||||
Future<FileDirectory> fetchDirectoryRecursiveToRemove(
|
||||
int actID, String path, bool isLocal, bool showHidden) async {
|
||||
// TODO test Recursive is show hidden default?
|
||||
try {
|
||||
await bind.sessionReadDirRecursive(
|
||||
await bind.sessionReadDirToRemoveRecursive(
|
||||
sessionId: sessionId,
|
||||
actId: actID,
|
||||
path: path,
|
||||
@@ -1344,6 +1478,24 @@ class PathUtil {
|
||||
static final windowsContext = path.Context(style: path.Style.windows);
|
||||
static final posixContext = path.Context(style: path.Style.posix);
|
||||
|
||||
static String getOtherSidePath(String mainRootPath, String mainPath,
|
||||
bool isMainWindows, String otherRootPath, bool isOtherWindows) {
|
||||
final mainPathUtil = isMainWindows ? windowsContext : posixContext;
|
||||
final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath);
|
||||
|
||||
final names = mainPathUtil.split(relativePath);
|
||||
|
||||
final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
|
||||
|
||||
String path = otherRootPath;
|
||||
|
||||
for (var name in names) {
|
||||
path = otherPathUtil.join(path, name);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
static String join(String path1, String path2, bool isWindows) {
|
||||
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||
return pathUtil.join(path1, path2);
|
||||
|
||||
@@ -23,7 +23,14 @@ class GroupModel {
|
||||
|
||||
bool get emtpy => users.isEmpty && peers.isEmpty;
|
||||
|
||||
GroupModel(this.parent);
|
||||
late final Peers peersModel;
|
||||
|
||||
GroupModel(this.parent) {
|
||||
peersModel = Peers(
|
||||
name: PeersModelName.group,
|
||||
getInitPeers: () => peers,
|
||||
loadEvent: LoadEvent.group);
|
||||
}
|
||||
|
||||
Future<void> pull({force = true, quiet = false}) async {
|
||||
if (bind.isDisableGroupPanel()) return;
|
||||
|
||||
@@ -469,8 +469,12 @@ class InputModel {
|
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
return KeyEventResult.handled;
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
} else if (isWeb) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
|
||||
final key = e.logicalKey;
|
||||
@@ -519,8 +523,12 @@ class InputModel {
|
||||
|
||||
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
return KeyEventResult.handled;
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
} else if (isWeb) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
if (isWindows || isLinux) {
|
||||
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
|
||||
@@ -536,8 +544,40 @@ class InputModel {
|
||||
handleKeyDownEventModifiers(e);
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
bool isMobileAndMapMode = false;
|
||||
if (isMobile) {
|
||||
// Do not use map mode if mobile -> Android. Android does not support map mode for now.
|
||||
// Because simulating the physical key events(uhid) which requires root permission is not supported.
|
||||
if (peerPlatform != kPeerPlatformAndroid) {
|
||||
if (isIOS) {
|
||||
isMobileAndMapMode = true;
|
||||
} else {
|
||||
// The physicalKey.usbHidUsage may be not correct for soft keyboard on Android.
|
||||
// iOS does not have this issue.
|
||||
// 1. Open the soft keyboard on Android
|
||||
// 2. Switch to input method like zh/ko/ja
|
||||
// 3. Click Backspace and Enter on the soft keyboard or physical keyboard
|
||||
// 4. The physicalKey.usbHidUsage is not correct.
|
||||
// PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042")
|
||||
// LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter")
|
||||
//
|
||||
// The correct PhysicalKeyboardKey should be
|
||||
// PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter")
|
||||
// https://github.com/flutter/flutter/issues/157771
|
||||
// We cannot use the debugName to determine the key is correct or not, because it's null in release mode.
|
||||
// The normal `usbHidUsage` for keyboard shoud be between [0x00000010, 0x000c029f]
|
||||
// https://github.com/flutter/flutter/blob/c051b69e2a2224300e20d93dbd15f4b91e8844d1/packages/flutter/lib/src/services/keyboard_key.g.dart#L5332 - 5600
|
||||
final isNormalHsbHidUsage = (e.physicalKey.usbHidUsage >> 20) == 0;
|
||||
isMobileAndMapMode = isNormalHsbHidUsage &&
|
||||
// No need to check `!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel)`
|
||||
// But we still add it for more reliability.
|
||||
!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
final isDesktopAndMapMode =
|
||||
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
|
||||
if (isMobileAndMapMode || isDesktopAndMapMode) {
|
||||
// FIXME: e.character is wrong for dead keys, eg: ^ in de
|
||||
newKeyboardMode(
|
||||
e.character ?? '',
|
||||
@@ -728,22 +768,22 @@ class InputModel {
|
||||
}
|
||||
|
||||
/// Send a mouse tap event(down and up).
|
||||
void tap(MouseButtons button) {
|
||||
sendMouse('down', button);
|
||||
sendMouse('up', button);
|
||||
Future<void> tap(MouseButtons button) async {
|
||||
await sendMouse('down', button);
|
||||
await sendMouse('up', button);
|
||||
}
|
||||
|
||||
void tapDown(MouseButtons button) {
|
||||
sendMouse('down', button);
|
||||
Future<void> tapDown(MouseButtons button) async {
|
||||
await sendMouse('down', button);
|
||||
}
|
||||
|
||||
void tapUp(MouseButtons button) {
|
||||
sendMouse('up', button);
|
||||
Future<void> tapUp(MouseButtons button) async {
|
||||
await sendMouse('up', button);
|
||||
}
|
||||
|
||||
/// Send scroll event with scroll distance [y].
|
||||
void scroll(int y) {
|
||||
bind.sessionSendMouse(
|
||||
Future<void> scroll(int y) async {
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json
|
||||
.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()})));
|
||||
@@ -764,9 +804,9 @@ class InputModel {
|
||||
}
|
||||
|
||||
/// Send mouse press event.
|
||||
void sendMouse(String type, MouseButtons button) {
|
||||
Future<void> sendMouse(String type, MouseButtons button) async {
|
||||
if (!keyboardPerm) return;
|
||||
bind.sessionSendMouse(
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
||||
}
|
||||
@@ -790,11 +830,11 @@ class InputModel {
|
||||
}
|
||||
|
||||
/// Send mouse movement event with distance in [x] and [y].
|
||||
void moveMouse(double x, double y) {
|
||||
Future<void> moveMouse(double x, double y) async {
|
||||
if (!keyboardPerm) return;
|
||||
var x2 = x.toInt();
|
||||
var y2 = y.toInt();
|
||||
bind.sessionSendMouse(
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
|
||||
}
|
||||
@@ -816,7 +856,7 @@ class InputModel {
|
||||
_stopFling = true;
|
||||
if (isViewOnly) return;
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent('touch', 'pan_start', e.position);
|
||||
handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,8 +899,8 @@ class InputModel {
|
||||
}
|
||||
if (x != 0 || y != 0) {
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent(
|
||||
'touch', 'pan_update', Offset(x.toDouble(), y.toDouble()));
|
||||
handlePointerEvent('touch', kMouseEventTypePanUpdate,
|
||||
Offset(x.toDouble(), y.toDouble()));
|
||||
} else {
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
@@ -922,7 +962,7 @@ class InputModel {
|
||||
|
||||
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent('touch', 'pan_end', e.position);
|
||||
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1040,7 +1080,7 @@ class InputModel {
|
||||
onExit: true,
|
||||
);
|
||||
|
||||
int trySetNearestRange(int v, int min, int max, int n) {
|
||||
static int tryGetNearestRange(int v, int min, int max, int n) {
|
||||
if (v < min && v >= min - n) {
|
||||
v = min;
|
||||
}
|
||||
@@ -1080,13 +1120,13 @@ class InputModel {
|
||||
// to-do: handle mouse events
|
||||
|
||||
late final dynamic evtValue;
|
||||
if (type == 'pan_update') {
|
||||
if (type == kMouseEventTypePanUpdate) {
|
||||
evtValue = {
|
||||
'x': x.toInt(),
|
||||
'y': y.toInt(),
|
||||
};
|
||||
} else {
|
||||
final isMoveTypes = ['pan_start', 'pan_end'];
|
||||
final isMoveTypes = [kMouseEventTypePanStart, kMouseEventTypePanEnd];
|
||||
final pos = handlePointerDevicePos(
|
||||
kPointerEventKindTouch,
|
||||
x,
|
||||
@@ -1141,14 +1181,14 @@ class InputModel {
|
||||
return;
|
||||
}
|
||||
|
||||
var type = '';
|
||||
var type = kMouseEventTypeDefault;
|
||||
var isMove = false;
|
||||
switch (evt['type']) {
|
||||
case _kMouseEventDown:
|
||||
type = 'down';
|
||||
type = kMouseEventTypeDown;
|
||||
break;
|
||||
case _kMouseEventUp:
|
||||
type = 'up';
|
||||
type = kMouseEventTypeUp;
|
||||
break;
|
||||
case _kMouseEventMove:
|
||||
_pointerMovedAfterEnter = true;
|
||||
@@ -1159,7 +1199,7 @@ class InputModel {
|
||||
}
|
||||
evt['type'] = type;
|
||||
|
||||
if (type == 'down' && !_pointerMovedAfterEnter) {
|
||||
if (type == kMouseEventTypeDown && !_pointerMovedAfterEnter) {
|
||||
// Move mouse to the position of the down event first.
|
||||
lastMousePos = ui.Offset(x, y);
|
||||
refreshMousePos();
|
||||
@@ -1332,6 +1372,14 @@ class InputModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
return InputModel.getPointInRemoteRect(
|
||||
true, peerPlatform, kind, evtType, evtX, evtY, rect,
|
||||
buttons: buttons);
|
||||
}
|
||||
|
||||
static Point? getPointInRemoteRect(bool isLocalDesktop, String? peerPlatform,
|
||||
String kind, String evtType, int evtX, int evtY, Rect rect,
|
||||
{int buttons = kPrimaryMouseButton}) {
|
||||
int minX = rect.left.toInt();
|
||||
// https://github.com/rustdesk/rustdesk/issues/6678
|
||||
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
|
||||
@@ -1340,15 +1388,34 @@ class InputModel {
|
||||
int minY = rect.top.toInt();
|
||||
int maxY = (rect.top + rect.height).toInt() -
|
||||
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
|
||||
evtX = trySetNearestRange(evtX, minX, maxX, 5);
|
||||
evtY = trySetNearestRange(evtY, minY, maxY, 5);
|
||||
if (kind == kPointerEventKindMouse) {
|
||||
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
|
||||
// If left mouse up, no early return.
|
||||
if (!(buttons == kPrimaryMouseButton && evtType == 'up')) {
|
||||
return null;
|
||||
evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5);
|
||||
evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5);
|
||||
if (isLocalDesktop) {
|
||||
if (kind == kPointerEventKindMouse) {
|
||||
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
|
||||
// If left mouse up, no early return.
|
||||
if (!(buttons == kPrimaryMouseButton &&
|
||||
evtType == kMouseEventTypeUp)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bool evtXInRange = evtX >= minX && evtX <= maxX;
|
||||
bool evtYInRange = evtY >= minY && evtY <= maxY;
|
||||
if (!(evtXInRange || evtYInRange)) {
|
||||
return null;
|
||||
}
|
||||
if (evtX < minX) {
|
||||
evtX = minX;
|
||||
} else if (evtX > maxX) {
|
||||
evtX = maxX;
|
||||
}
|
||||
if (evtY < minY) {
|
||||
evtY = minY;
|
||||
} else if (evtY > maxY) {
|
||||
evtY = maxY;
|
||||
}
|
||||
}
|
||||
|
||||
return Point(evtX, evtY);
|
||||
|
||||
@@ -4,15 +4,20 @@ import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/cm_file_model.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_hbb/models/group_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
@@ -139,6 +144,7 @@ class FfiModel with ChangeNotifier {
|
||||
bool get touchMode => _touchMode;
|
||||
|
||||
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
|
||||
bool get isPeerMobile => isPeerAndroid;
|
||||
|
||||
bool get viewOnly => _viewOnly;
|
||||
|
||||
@@ -267,6 +273,8 @@ class FfiModel with ChangeNotifier {
|
||||
var name = evt['name'];
|
||||
if (name == 'msgbox') {
|
||||
handleMsgBox(evt, sessionId, peerId);
|
||||
} else if (name == 'toast') {
|
||||
handleToast(evt, sessionId, peerId);
|
||||
} else if (name == 'set_multiple_windows_session') {
|
||||
handleMultipleWindowsSession(evt, sessionId, peerId);
|
||||
} else if (name == 'peer_info') {
|
||||
@@ -301,6 +309,8 @@ class FfiModel with ChangeNotifier {
|
||||
.receive(int.parse(evt['id'] as String), evt['text'] ?? '');
|
||||
} else if (name == 'file_dir') {
|
||||
parent.target?.fileModel.receiveFileDir(evt);
|
||||
} else if (name == 'empty_dirs') {
|
||||
parent.target?.fileModel.receiveEmptyDirs(evt);
|
||||
} else if (name == 'job_progress') {
|
||||
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
||||
} else if (name == 'job_done') {
|
||||
@@ -370,7 +380,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'plugin_option') {
|
||||
handleOption(evt);
|
||||
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
||||
if (desktopType == DesktopType.main) {
|
||||
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
||||
final id = evt['id'];
|
||||
final hash = evt['hash'];
|
||||
if (id != null && hash != null) {
|
||||
@@ -388,6 +398,14 @@ class FfiModel with ChangeNotifier {
|
||||
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||
} else if (name == 'use_texture_render') {
|
||||
_handleUseTextureRender(evt, sessionId, peerId);
|
||||
} else if (name == "selected_files") {
|
||||
if (isWeb) {
|
||||
parent.target?.fileModel.onSelectedFiles(evt);
|
||||
}
|
||||
} else if (name == "record_status") {
|
||||
if (desktopType == DesktopType.remote || isMobile) {
|
||||
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
|
||||
}
|
||||
} else {
|
||||
debugPrint('Event is not handled in the fixed branch: $name');
|
||||
}
|
||||
@@ -518,7 +536,6 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
|
||||
handleResolutions(peerId, evt['resolutions']);
|
||||
}
|
||||
@@ -589,13 +606,44 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
handleToast(Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
final type = evt['type'] ?? 'info';
|
||||
final text = evt['text'] ?? '';
|
||||
final durMsc = evt['dur_msec'] ?? 2000;
|
||||
final duration = Duration(milliseconds: durMsc);
|
||||
if ((text).isEmpty) {
|
||||
BotToast.showLoading(
|
||||
duration: duration,
|
||||
clickClose: true,
|
||||
allowClick: true,
|
||||
);
|
||||
} else {
|
||||
if (type.contains('error')) {
|
||||
BotToast.showText(
|
||||
contentColor: Colors.red,
|
||||
text: translate(text),
|
||||
duration: duration,
|
||||
clickClose: true,
|
||||
onlyOne: true,
|
||||
);
|
||||
} else {
|
||||
BotToast.showText(
|
||||
text: translate(text),
|
||||
duration: duration,
|
||||
clickClose: true,
|
||||
onlyOne: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a message box with [type], [title] and [text].
|
||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) {
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: reconnect,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
_timer?.cancel();
|
||||
if (hasRetry) {
|
||||
@@ -839,7 +887,7 @@ class FfiModel with ChangeNotifier {
|
||||
for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: sessionId, mode: mode)) {
|
||||
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
||||
await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1095,8 +1143,6 @@ class FfiModel with ChangeNotifier {
|
||||
// Directly switch to the new display without waiting for the response.
|
||||
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
||||
{bool updateCursorPos = false}) {
|
||||
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
|
||||
parent.target?.recordingModel.onClose();
|
||||
// no need to wait for the response
|
||||
pi.currentDisplay = display;
|
||||
updateCurDisplay(sessionId, updateCursorPos: updateCursorPos);
|
||||
@@ -1185,6 +1231,27 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
clearImage() => _image = null;
|
||||
|
||||
bool _webDecodingRgba = false;
|
||||
final List<Uint8List> _webRgbaList = List.empty(growable: true);
|
||||
webOnRgba(int display, Uint8List rgba) async {
|
||||
// deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer"
|
||||
_webRgbaList.add(Uint8List.fromList(rgba));
|
||||
if (_webDecodingRgba) {
|
||||
return;
|
||||
}
|
||||
_webDecodingRgba = true;
|
||||
try {
|
||||
while (_webRgbaList.isNotEmpty) {
|
||||
final rgba2 = _webRgbaList.last;
|
||||
_webRgbaList.clear();
|
||||
await decodeAndUpdate(display, rgba2);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('onRgba error: $e');
|
||||
}
|
||||
_webDecodingRgba = false;
|
||||
}
|
||||
|
||||
onRgba(int display, Uint8List rgba) async {
|
||||
try {
|
||||
await decodeAndUpdate(display, rgba);
|
||||
@@ -1212,13 +1279,6 @@ class ImageModel with ChangeNotifier {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
await parent.target?.canvasModel.updateViewStyle();
|
||||
await parent.target?.canvasModel.updateScrollStyle();
|
||||
} else {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final canvasWidth = size.width;
|
||||
final canvasHeight = size.height;
|
||||
final xscale = canvasWidth / image.width;
|
||||
final yscale = canvasHeight / image.height;
|
||||
parent.target?.canvasModel.scale = min(xscale, yscale);
|
||||
}
|
||||
if (parent.target != null) {
|
||||
await initializeCursorAndCanvas(parent.target!);
|
||||
@@ -1230,20 +1290,18 @@ class ImageModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
// mobile only
|
||||
// for desktop, height should minus tabbar height
|
||||
double get maxScale {
|
||||
if (_image == null) return 1.5;
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final size = parent.target!.canvasModel.getSize();
|
||||
final xscale = size.width / _image!.width;
|
||||
final yscale = size.height / _image!.height;
|
||||
return max(1.5, max(xscale, yscale));
|
||||
}
|
||||
|
||||
// mobile only
|
||||
// for desktop, height should minus tabbar height
|
||||
double get minScale {
|
||||
if (_image == null) return 1.5;
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final size = parent.target!.canvasModel.getSize();
|
||||
final xscale = size.width / _image!.width;
|
||||
final yscale = size.height / _image!.height;
|
||||
return min(xscale, yscale) / 1.5;
|
||||
@@ -1366,6 +1424,12 @@ class CanvasModel with ChangeNotifier {
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
|
||||
Timer? _timerMobileFocusCanvasCursor;
|
||||
|
||||
// `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method
|
||||
// after showing the soft keyboard.
|
||||
bool isMobileCanvasChanged = false;
|
||||
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
|
||||
@@ -1408,15 +1472,29 @@ class CanvasModel with ChangeNotifier {
|
||||
static double get bottomToEdge =>
|
||||
isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0;
|
||||
|
||||
updateViewStyle({refreshMousePos = true}) async {
|
||||
Size getSize() {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
// If minimized, w or h may be negative here.
|
||||
double w = size.width - leftToEdge - rightToEdge;
|
||||
double h = size.height - topToEdge - bottomToEdge;
|
||||
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
|
||||
Size getSize() {
|
||||
final mediaData = MediaQueryData.fromView(ui.window);
|
||||
final size = mediaData.size;
|
||||
// If minimized, w or h may be negative here.
|
||||
double w = size.width - leftToEdge - rightToEdge;
|
||||
double h = size.height - topToEdge - bottomToEdge;
|
||||
if (isMobile) {
|
||||
h = h -
|
||||
mediaData.viewInsets.bottom -
|
||||
(parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ??
|
||||
0);
|
||||
}
|
||||
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
|
||||
}
|
||||
|
||||
// mobile only
|
||||
double getAdjustY() {
|
||||
final bottom =
|
||||
parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? 0;
|
||||
return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0);
|
||||
}
|
||||
|
||||
updateViewStyle({refreshMousePos = true, notify = true}) async {
|
||||
final style = await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||
if (style == null) {
|
||||
return;
|
||||
@@ -1445,16 +1523,25 @@ class CanvasModel with ChangeNotifier {
|
||||
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / _devicePixelRatio;
|
||||
}
|
||||
_x = (size.width - displayWidth * _scale) / 2;
|
||||
_y = (size.height - displayHeight * _scale) / 2;
|
||||
_resetCanvasOffset(displayWidth, displayHeight);
|
||||
_imageOverflow.value = _x < 0 || y < 0;
|
||||
notifyListeners();
|
||||
if (refreshMousePos) {
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
if (!isMobile && refreshMousePos) {
|
||||
parent.target?.inputModel.refreshMousePos();
|
||||
}
|
||||
tryUpdateScrollStyle(Duration.zero, style);
|
||||
}
|
||||
|
||||
_resetCanvasOffset(int displayWidth, int displayHeight) {
|
||||
_x = (size.width - displayWidth * _scale) / 2;
|
||||
_y = (size.height - displayHeight * _scale) / 2;
|
||||
if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) {
|
||||
_moveToCenterCursor();
|
||||
}
|
||||
}
|
||||
|
||||
tryUpdateScrollStyle(Duration duration, String? style) async {
|
||||
if (_scrollStyle != ScrollStyle.scrollbar) return;
|
||||
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||
@@ -1558,6 +1645,9 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
panX(double dx) {
|
||||
_x += dx;
|
||||
if (isMobile) {
|
||||
isMobileCanvasChanged = true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1565,17 +1655,20 @@ class CanvasModel with ChangeNotifier {
|
||||
if (isWebDesktop) {
|
||||
updateViewStyle();
|
||||
} else {
|
||||
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||
_y = (size.height - getDisplayHeight() * _scale) / 2;
|
||||
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
panY(double dy) {
|
||||
_y += dy;
|
||||
if (isMobile) {
|
||||
isMobileCanvasChanged = true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// mobile only
|
||||
updateScale(double v, Offset focalPoint) {
|
||||
if (parent.target?.imageModel.image == null) return;
|
||||
final s = _scale;
|
||||
@@ -1587,21 +1680,34 @@ class CanvasModel with ChangeNotifier {
|
||||
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
|
||||
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
|
||||
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
|
||||
final adjustForKeyboard =
|
||||
parent.target?.cursorModel.adjustForKeyboard() ?? 0.0;
|
||||
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY
|
||||
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2
|
||||
_y = focalPoint.dy +
|
||||
adjustForKeyboard -
|
||||
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale;
|
||||
final adjust = getAdjustY();
|
||||
// (focalPoint.dy - _y_1 - adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 - adjust) / s2 + displayOriginY
|
||||
// _y_2 = focalPoint.dy - adjust - (focalPoint.dy - _y_1 - adjust) / s1 * s2
|
||||
_y = focalPoint.dy - adjust - (focalPoint.dy - _y - adjust) / s * _scale;
|
||||
if (isMobile) {
|
||||
isMobileCanvasChanged = true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
clear([bool notify = false]) {
|
||||
// For reset canvas to the last view style
|
||||
reset() {
|
||||
_scale = _lastViewStyle.scale;
|
||||
_devicePixelRatio = ui.window.devicePixelRatio;
|
||||
if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / _devicePixelRatio;
|
||||
}
|
||||
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
clear() {
|
||||
_x = 0;
|
||||
_y = 0;
|
||||
_scale = 1.0;
|
||||
if (notify) notifyListeners();
|
||||
_lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
_timerMobileFocusCanvasCursor?.cancel();
|
||||
}
|
||||
|
||||
updateScrollPercent() {
|
||||
@@ -1619,6 +1725,42 @@ class CanvasModel with ChangeNotifier {
|
||||
: 0.0;
|
||||
setScrollPercent(percentX, percentY);
|
||||
}
|
||||
|
||||
void mobileFocusCanvasCursor() {
|
||||
_timerMobileFocusCanvasCursor?.cancel();
|
||||
_timerMobileFocusCanvasCursor =
|
||||
Timer(Duration(milliseconds: 100), () async {
|
||||
await updateViewStyle(refreshMousePos: false, notify: false);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
// mobile only
|
||||
// Move the canvas to make the cursor visible(center) on the screen.
|
||||
void _moveToCenterCursor() {
|
||||
Rect? imageRect = parent.target?.ffiModel.rect;
|
||||
if (imageRect == null) {
|
||||
// unreachable
|
||||
return;
|
||||
}
|
||||
final maxX = 0.0;
|
||||
final minX = _size.width + (imageRect.left - imageRect.right) * _scale;
|
||||
final maxY = 0.0;
|
||||
final minY = _size.height + (imageRect.top - imageRect.bottom) * _scale;
|
||||
Offset offsetToCenter =
|
||||
parent.target?.cursorModel.getCanvasOffsetToCenterCursor() ??
|
||||
Offset.zero;
|
||||
if (minX < 0) {
|
||||
_x = min(max(offsetToCenter.dx, minX), maxX);
|
||||
} else {
|
||||
// _size.width > (imageRect.right, imageRect.left) * _scale, we should not change _x
|
||||
}
|
||||
if (minY < 0) {
|
||||
_y = min(max(offsetToCenter.dy, minY), maxY);
|
||||
} else {
|
||||
// _size.height > (imageRect.bottom - imageRect.top) * _scale, , we should not change _y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// data for cursor
|
||||
@@ -1812,9 +1954,13 @@ class CursorModel with ChangeNotifier {
|
||||
// `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap()
|
||||
// Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position.
|
||||
bool _lastIsBlocked = false;
|
||||
double _yForKeyboardAdjust = 0;
|
||||
bool _lastKeyboardIsVisible = false;
|
||||
|
||||
keyHelpToolsVisibilityChanged(Rect? r) {
|
||||
bool get lastKeyboardIsVisible => _lastKeyboardIsVisible;
|
||||
|
||||
Rect? get keyHelpToolsRectToAdjustCanvas =>
|
||||
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
|
||||
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
|
||||
_keyHelpToolsRect = r;
|
||||
if (r == null) {
|
||||
_lastIsBlocked = false;
|
||||
@@ -1824,7 +1970,11 @@ class CursorModel with ChangeNotifier {
|
||||
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
|
||||
_lastIsBlocked = true;
|
||||
}
|
||||
_yForKeyboardAdjust = _y;
|
||||
if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) {
|
||||
parent.target?.canvasModel.mobileFocusCanvasCursor();
|
||||
parent.target?.canvasModel.isMobileCanvasChanged = false;
|
||||
}
|
||||
_lastKeyboardIsVisible = keyboardIsVisible;
|
||||
}
|
||||
|
||||
get lastIsBlocked => _lastIsBlocked;
|
||||
@@ -1863,8 +2013,10 @@ class CursorModel with ChangeNotifier {
|
||||
addKey(String key) => _cacheKeys.add(key);
|
||||
|
||||
// remote physical display coordinate
|
||||
// For update pan (mobile), onOneFingerPanStart, onOneFingerPanUpdate, onHoldDragUpdate
|
||||
Rect getVisibleRect() {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final size = parent.target?.canvasModel.getSize() ??
|
||||
MediaQueryData.fromView(ui.window).size;
|
||||
final xoffset = parent.target?.canvasModel.x ?? 0;
|
||||
final yoffset = parent.target?.canvasModel.y ?? 0;
|
||||
final scale = parent.target?.canvasModel.scale ?? 1;
|
||||
@@ -1873,22 +2025,22 @@ class CursorModel with ChangeNotifier {
|
||||
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
||||
}
|
||||
|
||||
get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom;
|
||||
get scale => parent.target?.canvasModel.scale ?? 1.0;
|
||||
|
||||
double adjustForKeyboard() {
|
||||
if (keyboardHeight < 100) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
final m = MediaQueryData.fromWindow(ui.window);
|
||||
final size = m.size;
|
||||
final thresh = (size.height - keyboardHeight) / 2;
|
||||
final h = (_yForKeyboardAdjust - getVisibleRect().top) *
|
||||
scale; // local physical display height
|
||||
return h - thresh;
|
||||
Offset getCanvasOffsetToCenterCursor() {
|
||||
// Cursor should be at the center of the visible rect.
|
||||
// _x = rect.left + rect.width / 2
|
||||
// _y = rect.right + rect.height / 2
|
||||
// See `getVisibleRect()`
|
||||
// _x = _displayOriginX - xoffset / scale + size.width / scale * 0.5;
|
||||
// _y = _displayOriginY - yoffset / scale + size.height / scale * 0.5;
|
||||
final size = parent.target?.canvasModel.getSize() ??
|
||||
MediaQueryData.fromView(ui.window).size;
|
||||
final xoffset = (_displayOriginX - _x) * scale + size.width * 0.5;
|
||||
final yoffset = (_displayOriginY - _y) * scale + size.height * 0.5;
|
||||
return Offset(xoffset, yoffset);
|
||||
}
|
||||
|
||||
get scale => parent.target?.canvasModel.scale ?? 1.0;
|
||||
|
||||
// mobile Soft keyboard, block touch event from the KeyHelpTools
|
||||
shouldBlock(double x, double y) {
|
||||
if (!(parent.target?.ffiModel.touchMode ?? false)) {
|
||||
@@ -1903,22 +2055,63 @@ class CursorModel with ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
move(double x, double y) {
|
||||
// For touch mode
|
||||
Future<bool> move(double x, double y) async {
|
||||
if (shouldBlock(x, y)) {
|
||||
_lastIsBlocked = true;
|
||||
return false;
|
||||
}
|
||||
_lastIsBlocked = false;
|
||||
moveLocal(x, y, adjust: adjustForKeyboard());
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
if (!_moveLocalIfInRemoteRect(x, y)) {
|
||||
return false;
|
||||
}
|
||||
await parent.target?.inputModel.moveMouse(_x, _y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isInRemoteRect(Offset offset) {
|
||||
return getRemotePosInRect(offset) != null;
|
||||
}
|
||||
|
||||
Offset? getRemotePosInRect(Offset offset) {
|
||||
final adjust = parent.target?.canvasModel.getAdjustY() ?? 0;
|
||||
final newPos = _getNewPos(offset.dx, offset.dy, adjust);
|
||||
final visibleRect = getVisibleRect();
|
||||
if (!isPointInRect(newPos, visibleRect)) {
|
||||
return null;
|
||||
}
|
||||
final rect = parent.target?.ffiModel.rect;
|
||||
if (rect != null) {
|
||||
if (!isPointInRect(newPos, rect)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return newPos;
|
||||
}
|
||||
|
||||
Offset _getNewPos(double x, double y, double adjust) {
|
||||
final xoffset = parent.target?.canvasModel.x ?? 0;
|
||||
final yoffset = parent.target?.canvasModel.y ?? 0;
|
||||
final newX = (x - xoffset) / scale + _displayOriginX;
|
||||
final newY = (y - yoffset - adjust) / scale + _displayOriginY;
|
||||
return Offset(newX, newY);
|
||||
}
|
||||
|
||||
bool _moveLocalIfInRemoteRect(double x, double y) {
|
||||
final newPos = getRemotePosInRect(Offset(x, y));
|
||||
if (newPos == null) {
|
||||
return false;
|
||||
}
|
||||
_x = newPos.dx;
|
||||
_y = newPos.dy;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
moveLocal(double x, double y, {double adjust = 0}) {
|
||||
final xoffset = parent.target?.canvasModel.x ?? 0;
|
||||
final yoffset = parent.target?.canvasModel.y ?? 0;
|
||||
_x = (x - xoffset) / scale + _displayOriginX;
|
||||
_y = (y - yoffset + adjust) / scale + _displayOriginY;
|
||||
final newPos = _getNewPos(x, y, adjust);
|
||||
_x = newPos.dx;
|
||||
_y = newPos.dy;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1926,13 +2119,13 @@ class CursorModel with ChangeNotifier {
|
||||
_x = _displayOriginX;
|
||||
_y = _displayOriginY;
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
parent.target?.canvasModel.clear(true);
|
||||
parent.target?.canvasModel.reset();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
updatePan(Offset delta, Offset localPosition, bool touchMode) {
|
||||
updatePan(Offset delta, Offset localPosition, bool touchMode) async {
|
||||
if (touchMode) {
|
||||
_handleTouchMode(delta, localPosition);
|
||||
await _handleTouchMode(delta, localPosition);
|
||||
return;
|
||||
}
|
||||
double dx = delta.dx;
|
||||
@@ -1990,13 +2183,34 @@ class CursorModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (dx == 0 && dy == 0) return;
|
||||
_x += dx;
|
||||
_y += dy;
|
||||
|
||||
Point? newPos;
|
||||
final rect = parent.target?.ffiModel.rect;
|
||||
if (rect == null) {
|
||||
// unreachable
|
||||
return;
|
||||
}
|
||||
newPos = InputModel.getPointInRemoteRect(
|
||||
false,
|
||||
parent.target?.ffiModel.pi.platform,
|
||||
kPointerEventKindMouse,
|
||||
kMouseEventTypeDefault,
|
||||
(_x + dx).toInt(),
|
||||
(_y + dy).toInt(),
|
||||
rect,
|
||||
buttons: kPrimaryButton);
|
||||
if (newPos == null) {
|
||||
return;
|
||||
}
|
||||
dx = newPos.x - _x;
|
||||
dy = newPos.y - _y;
|
||||
_x = newPos.x.toDouble();
|
||||
_y = newPos.y.toDouble();
|
||||
if (tryMoveCanvasX && dx != 0) {
|
||||
parent.target?.canvasModel.panX(-dx);
|
||||
parent.target?.canvasModel.panX(-dx * scale);
|
||||
}
|
||||
if (tryMoveCanvasY && dy != 0) {
|
||||
parent.target?.canvasModel.panY(-dy);
|
||||
parent.target?.canvasModel.panY(-dy * scale);
|
||||
}
|
||||
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
@@ -2009,7 +2223,7 @@ class CursorModel with ChangeNotifier {
|
||||
return x >= 0 && y >= 0 && x <= w && y <= h;
|
||||
}
|
||||
|
||||
_handleTouchMode(Offset delta, Offset localPosition) {
|
||||
_handleTouchMode(Offset delta, Offset localPosition) async {
|
||||
bool isMoved = false;
|
||||
if (_remoteWindowCoords.isNotEmpty &&
|
||||
_windowRect != null &&
|
||||
@@ -2025,15 +2239,50 @@ class CursorModel with ChangeNotifier {
|
||||
coords.canvas.scale;
|
||||
x2 += coords.cursor.offset.dx;
|
||||
y2 += coords.cursor.offset.dy;
|
||||
parent.target?.inputModel.moveMouse(x2, y2);
|
||||
await parent.target?.inputModel.moveMouse(x2, y2);
|
||||
isMoved = true;
|
||||
}
|
||||
}
|
||||
if (!isMoved) {
|
||||
final rect = parent.target?.ffiModel.rect;
|
||||
if (rect == null) {
|
||||
// unreachable
|
||||
return;
|
||||
}
|
||||
|
||||
Offset? movementInRect(double x, double y, Rect r) {
|
||||
final isXInRect = x >= r.left && x <= r.right;
|
||||
final isYInRect = y >= r.top && y <= r.bottom;
|
||||
if (!(isXInRect || isYInRect)) {
|
||||
return null;
|
||||
}
|
||||
if (x < r.left) {
|
||||
x = r.left;
|
||||
} else if (x > r.right) {
|
||||
x = r.right;
|
||||
}
|
||||
if (y < r.top) {
|
||||
y = r.top;
|
||||
} else if (y > r.bottom) {
|
||||
y = r.bottom;
|
||||
}
|
||||
return Offset(x, y);
|
||||
}
|
||||
|
||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
||||
_x += delta.dx / scale;
|
||||
_y += delta.dy / scale;
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
var movement =
|
||||
movementInRect(_x + delta.dx / scale, _y + delta.dy / scale, rect);
|
||||
if (movement == null) {
|
||||
return;
|
||||
}
|
||||
movement = movementInRect(movement.dx, movement.dy, getVisibleRect());
|
||||
if (movement == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_x = movement.dx;
|
||||
_y = movement.dy;
|
||||
await parent.target?.inputModel.moveMouse(_x, _y);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -2281,25 +2530,7 @@ class RecordingModel with ChangeNotifier {
|
||||
WeakReference<FFI> parent;
|
||||
RecordingModel(this.parent);
|
||||
bool _start = false;
|
||||
get start => _start;
|
||||
|
||||
onSwitchDisplay() {
|
||||
if (isIOS || !_start) return;
|
||||
final sessionId = parent.target?.sessionId;
|
||||
int? width = parent.target?.canvasModel.getDisplayWidth();
|
||||
int? height = parent.target?.canvasModel.getDisplayHeight();
|
||||
if (sessionId == null || width == null || height == null) return;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi == null) return;
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay == kAllDisplayValue) return;
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: true,
|
||||
display: currentDisplay,
|
||||
width: width,
|
||||
height: height);
|
||||
}
|
||||
bool get start => _start;
|
||||
|
||||
toggle() async {
|
||||
if (isIOS) return;
|
||||
@@ -2307,48 +2538,16 @@ class RecordingModel with ChangeNotifier {
|
||||
if (sessionId == null) return;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi == null) return;
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay == kAllDisplayValue) return;
|
||||
_start = !_start;
|
||||
notifyListeners();
|
||||
await _sendStatusMessage(sessionId, pi, _start);
|
||||
if (_start) {
|
||||
sessionRefreshVideo(sessionId, pi);
|
||||
if (versionCmp(pi.version, '1.2.4') >= 0) {
|
||||
// will not receive SwitchDisplay since 1.2.4
|
||||
onSwitchDisplay();
|
||||
}
|
||||
} else {
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: false,
|
||||
display: currentDisplay,
|
||||
width: 0,
|
||||
height: 0);
|
||||
bool value = !_start;
|
||||
if (value) {
|
||||
await sessionRefreshVideo(sessionId, pi);
|
||||
}
|
||||
await bind.sessionRecordScreen(sessionId: sessionId, start: value);
|
||||
}
|
||||
|
||||
onClose() async {
|
||||
if (isIOS) return;
|
||||
final sessionId = parent.target?.sessionId;
|
||||
if (sessionId == null) return;
|
||||
if (!_start) return;
|
||||
_start = false;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi == null) return;
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay == kAllDisplayValue) return;
|
||||
await _sendStatusMessage(sessionId, pi, false);
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: false,
|
||||
display: currentDisplay,
|
||||
width: 0,
|
||||
height: 0);
|
||||
}
|
||||
|
||||
_sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async {
|
||||
await bind.sessionRecordStatus(sessionId: sessionId, status: status);
|
||||
updateStatus(bool status) {
|
||||
_start = status;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2397,6 +2596,9 @@ class FFI {
|
||||
late final ElevationModel elevationModel; // session
|
||||
late final CmFileModel cmFileModel; // cm
|
||||
late final TextureModel textureModel; //session
|
||||
late final Peers recentPeersModel; // global
|
||||
late final Peers favoritePeersModel; // global
|
||||
late final Peers lanPeersModel; // global
|
||||
|
||||
FFI(SessionID? sId) {
|
||||
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
||||
@@ -2417,6 +2619,16 @@ class FFI {
|
||||
elevationModel = ElevationModel(WeakReference(this));
|
||||
cmFileModel = CmFileModel(WeakReference(this));
|
||||
textureModel = TextureModel(WeakReference(this));
|
||||
recentPeersModel = Peers(
|
||||
name: PeersModelName.recent,
|
||||
loadEvent: LoadEvent.recent,
|
||||
getInitPeers: null);
|
||||
favoritePeersModel = Peers(
|
||||
name: PeersModelName.favorite,
|
||||
loadEvent: LoadEvent.favorite,
|
||||
getInitPeers: null);
|
||||
lanPeersModel = Peers(
|
||||
name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null);
|
||||
}
|
||||
|
||||
/// Mobile reuse FFI
|
||||
@@ -2437,6 +2649,7 @@ class FFI {
|
||||
String? switchUuid,
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
String? connToken,
|
||||
bool? forceRelay,
|
||||
int? tabWindowId,
|
||||
int? display,
|
||||
@@ -2473,6 +2686,7 @@ class FFI {
|
||||
forceRelay: forceRelay ?? false,
|
||||
password: password ?? '',
|
||||
isSharedPassword: isSharedPassword ?? false,
|
||||
connToken: connToken,
|
||||
);
|
||||
} else if (display != null) {
|
||||
if (displays == null) {
|
||||
@@ -2645,6 +2859,7 @@ class FFI {
|
||||
canvasModel.scale,
|
||||
ffiModel.pi.currentDisplay);
|
||||
}
|
||||
imageModel.callbacksOnFirstImage.clear();
|
||||
await imageModel.update(null);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
|
||||
@@ -48,6 +48,12 @@ class PlatformFFI {
|
||||
|
||||
static get isMain => instance._appType == kAppTypeMain;
|
||||
|
||||
static String getByName(String name, [String arg = '']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
static void setByName(String name, [String value = '']) {}
|
||||
|
||||
static Future<String> getVersion() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
return packageInfo.version;
|
||||
@@ -276,4 +282,6 @@ class PlatformFFI {
|
||||
void syncAndroidServiceAppDirConfigPath() {
|
||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||
}
|
||||
|
||||
void setFullscreenCallback(void Function(bool) fun) {}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
// https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700
|
||||
// After onTap, the shift key should be pressed for a while when not in multiselection mode,
|
||||
// because onTap is delayed when onDoubleTap is not null
|
||||
if (isDesktop && !_isShiftDown) return;
|
||||
if (isDesktop || isWebDesktop) return;
|
||||
_multiSelectionMode = true;
|
||||
}
|
||||
final cached = _currentTabCachedPeers.map((e) => e.id).toList();
|
||||
|
||||
@@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance;
|
||||
final localeName = PlatformFFI.localeName;
|
||||
|
||||
RustdeskImpl get bind => platformFFI.ffiBind;
|
||||
|
||||
String ffiGetByName(String name, [String arg = '']) {
|
||||
return PlatformFFI.getByName(name, arg);
|
||||
}
|
||||
|
||||
void ffiSetByName(String name, [String value = '']) {
|
||||
PlatformFFI.setByName(name, value);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class ServerModel with ChangeNotifier {
|
||||
bool _inputOk = false;
|
||||
bool _audioOk = false;
|
||||
bool _fileOk = false;
|
||||
bool _clipboardOk = false;
|
||||
bool _showElevation = false;
|
||||
bool hideCm = false;
|
||||
int _connectStatus = 0; // Rendezvous Server status
|
||||
@@ -59,6 +60,8 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
bool get fileOk => _fileOk;
|
||||
|
||||
bool get clipboardOk => _clipboardOk;
|
||||
|
||||
bool get showElevation => _showElevation;
|
||||
|
||||
int get connectStatus => _connectStatus;
|
||||
@@ -209,6 +212,10 @@ class ServerModel with ChangeNotifier {
|
||||
_fileOk = fileOption != 'N';
|
||||
}
|
||||
|
||||
// clipboard
|
||||
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
|
||||
_clipboardOk = clipOption != 'N';
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -315,6 +322,14 @@ class ServerModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
toggleClipboard() async {
|
||||
_clipboardOk = !_clipboardOk;
|
||||
bind.mainSetOption(
|
||||
key: kOptionEnableClipboard,
|
||||
value: _clipboardOk ? defaultOptionYes : 'N');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
toggleInput() async {
|
||||
if (clients.isNotEmpty) {
|
||||
await showClientsMayNotBeChangedAlert(parent.target);
|
||||
|
||||
@@ -19,6 +19,9 @@ class StateGlobal {
|
||||
final RxBool showRemoteToolBar = false.obs;
|
||||
final svcStatus = SvcStatus.notReady.obs;
|
||||
final RxBool isFocused = false.obs;
|
||||
// for mobile and web
|
||||
bool isInMainPage = true;
|
||||
bool isWebVisible = true;
|
||||
|
||||
final isPortrait = false.obs;
|
||||
|
||||
@@ -70,27 +73,40 @@ class StateGlobal {
|
||||
if (_fullscreen.value != v) {
|
||||
_fullscreen.value = v;
|
||||
_showTabBar.value = !_fullscreen.value;
|
||||
refreshResizeEdgeSize();
|
||||
print(
|
||||
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||
if (procWnd) {
|
||||
final wc = WindowController.fromWindowId(windowId);
|
||||
wc.setFullscreen(_fullscreen.isTrue).then((_) {
|
||||
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
|
||||
if (isWindows && !v) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final frame = await wc.getFrame();
|
||||
final newRect = Rect.fromLTWH(
|
||||
frame.left, frame.top, frame.width + 1, frame.height + 1);
|
||||
await wc.setFrame(newRect);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (isWebDesktop) {
|
||||
procFullscreenWeb();
|
||||
} else {
|
||||
procFullscreenNative(procWnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
procFullscreenWeb() {
|
||||
final isFullscreen = ffiGetByName('fullscreen') == 'Y';
|
||||
String fullscreenValue = '';
|
||||
if (isFullscreen && _fullscreen.isFalse) {
|
||||
fullscreenValue = 'N';
|
||||
} else if (!isFullscreen && fullscreen.isTrue) {
|
||||
fullscreenValue = 'Y';
|
||||
}
|
||||
if (fullscreenValue.isNotEmpty) {
|
||||
ffiSetByName('fullscreen', fullscreenValue);
|
||||
}
|
||||
}
|
||||
|
||||
procFullscreenNative(bool procWnd) {
|
||||
refreshResizeEdgeSize();
|
||||
print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||
if (procWnd) {
|
||||
final wc = WindowController.fromWindowId(windowId);
|
||||
wc.setFullscreen(_fullscreen.isTrue).then((_) {
|
||||
// We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced.
|
||||
// https://github.com/rustdesk/rustdesk/issues/9675
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
|
||||
? kFullScreenEdgeSize
|
||||
: isMaximized.isTrue
|
||||
@@ -109,7 +125,13 @@ class StateGlobal {
|
||||
_inputSource = bind.mainGetInputSource();
|
||||
}
|
||||
|
||||
StateGlobal._();
|
||||
StateGlobal._() {
|
||||
if (isWebDesktop) {
|
||||
platformFFI.setFullscreenCallback((v) {
|
||||
_fullscreen.value = v;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static final StateGlobal instance = StateGlobal._();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:js';
|
||||
import 'dart:html';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/web/bridge.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -28,7 +30,15 @@ class PlatformFFI {
|
||||
context.callMethod('setByName', [name, value]);
|
||||
}
|
||||
|
||||
PlatformFFI._();
|
||||
PlatformFFI._() {
|
||||
window.document.addEventListener(
|
||||
'visibilitychange',
|
||||
(event) => {
|
||||
stateGlobal.isWebVisible =
|
||||
window.document.visibilityState == 'visible'
|
||||
});
|
||||
}
|
||||
|
||||
static final PlatformFFI instance = PlatformFFI._();
|
||||
|
||||
static get localeName => window.navigator.language;
|
||||
@@ -98,6 +108,10 @@ class PlatformFFI {
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
|
||||
Future<void> init(String appType) async {
|
||||
Completer completer = Completer();
|
||||
context["onInitFinished"] = () {
|
||||
completer.complete();
|
||||
};
|
||||
context.callMethod('init');
|
||||
version = getByName('version');
|
||||
window.onContextMenu.listen((event) {
|
||||
@@ -112,6 +126,7 @@ class PlatformFFI {
|
||||
print('json.decode fail(): $e');
|
||||
}
|
||||
};
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void setEventCallback(void Function(Map<String, dynamic>) fun) {
|
||||
@@ -157,4 +172,10 @@ class PlatformFFI {
|
||||
|
||||
// just for compilation
|
||||
void syncAndroidServiceAppDirConfigPath() {}
|
||||
|
||||
void setFullscreenCallback(void Function(bool) fun) {
|
||||
context["onFullscreenChanged"] = (bool v) {
|
||||
fun(v);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ class RustDeskMultiWindowManager {
|
||||
bool withScreenRect,
|
||||
) async {
|
||||
final windowController = await DesktopMultiWindow.createWindow(msg);
|
||||
if (isWindows) {
|
||||
windowController.setInitBackgroundColor(Colors.black);
|
||||
}
|
||||
final windowId = windowController.windowId;
|
||||
if (!withScreenRect) {
|
||||
windowController
|
||||
@@ -198,6 +201,7 @@ class RustDeskMultiWindowManager {
|
||||
String? switchUuid,
|
||||
bool? isRDP,
|
||||
bool? isSharedPassword,
|
||||
String? connToken,
|
||||
}) async {
|
||||
var params = {
|
||||
"type": type.index,
|
||||
@@ -214,6 +218,9 @@ class RustDeskMultiWindowManager {
|
||||
if (isSharedPassword != null) {
|
||||
params['isSharedPassword'] = isSharedPassword;
|
||||
}
|
||||
if (connToken != null) {
|
||||
params['connToken'] = connToken;
|
||||
}
|
||||
final msg = jsonEncode(params);
|
||||
|
||||
// separate window for file transfer is not supported
|
||||
@@ -251,8 +258,13 @@ class RustDeskMultiWindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> newFileTransfer(String remoteId,
|
||||
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
|
||||
Future<MultiWindowCallResult> newFileTransfer(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) async {
|
||||
return await newSession(
|
||||
WindowType.FileTransfer,
|
||||
kWindowEventNewFileTransfer,
|
||||
@@ -261,11 +273,18 @@ class RustDeskMultiWindowManager {
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
|
||||
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
|
||||
Future<MultiWindowCallResult> newPortForward(
|
||||
String remoteId,
|
||||
bool isRDP, {
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) async {
|
||||
return await newSession(
|
||||
WindowType.PortForward,
|
||||
kWindowEventNewPortForward,
|
||||
@@ -275,6 +294,7 @@ class RustDeskMultiWindowManager {
|
||||
forceRelay: forceRelay,
|
||||
isRDP: isRDP,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import 'dart:js' as js;
|
||||
import 'dart:html' as html;
|
||||
// cycle imports, maybe we can improve this
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
|
||||
final isAndroid_ = false;
|
||||
final isIOS_ = false;
|
||||
@@ -13,8 +15,7 @@ final isDesktop_ = false;
|
||||
|
||||
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
||||
|
||||
final _userAgent = html.window.navigator.userAgent.toLowerCase();
|
||||
|
||||
final isWebOnWindows_ = _userAgent.contains('win');
|
||||
final isWebOnLinux_ = _userAgent.contains('linux');
|
||||
final isWebOnMacOS_ = _userAgent.contains('mac');
|
||||
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
||||
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
||||
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
||||
final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS;
|
||||
|
||||
14
flutter/lib/web/dummy.dart
Normal file
14
flutter/lib/web/dummy.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
Future<void> webselectFiles({required bool is_folder}) async {
|
||||
throw UnimplementedError("webselectFiles");
|
||||
}
|
||||
|
||||
Future<void> webSendLocalFiles(
|
||||
{required int handleIndex,
|
||||
required int actId,
|
||||
required String path,
|
||||
required String to,
|
||||
required int fileNum,
|
||||
required bool includeHidden,
|
||||
required bool isRemote}) {
|
||||
throw UnimplementedError("webSendLocalFiles");
|
||||
}
|
||||
@@ -1,23 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/scan_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
class WebSettingsPage extends StatelessWidget {
|
||||
const WebSettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isWebDesktop) {
|
||||
return _buildDesktopButton(context);
|
||||
} else {
|
||||
return _buildMobileMenu(context);
|
||||
}
|
||||
return _buildDesktopButton(context);
|
||||
}
|
||||
|
||||
Widget _buildDesktopButton(BuildContext context) {
|
||||
@@ -34,65 +23,4 @@ class WebSettingsPage extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileMenu(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return (isIOS
|
||||
? [
|
||||
const PopupMenuItem(
|
||||
value: "scan",
|
||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
||||
)
|
||||
]
|
||||
: <PopupMenuItem<String>>[]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "server",
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
child: Text(translate('About RustDesk')),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 'server') {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'about') {
|
||||
showAbout(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
}
|
||||
if (value == 'scan') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ScanPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
flutter/lib/web/web_unique.dart
Normal file
30
flutter/lib/web/web_unique.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:js' as js;
|
||||
|
||||
Future<void> webselectFiles({required bool is_folder}) async {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['select_files', is_folder]));
|
||||
}
|
||||
|
||||
Future<void> webSendLocalFiles(
|
||||
{required int handleIndex,
|
||||
required int actId,
|
||||
required String path,
|
||||
required String to,
|
||||
required int fileNum,
|
||||
required bool includeHidden,
|
||||
required bool isRemote}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'send_local_files',
|
||||
jsonEncode({
|
||||
'id': actId,
|
||||
'handle_index': handleIndex,
|
||||
'path': path,
|
||||
'to': to,
|
||||
'file_num': fileNum,
|
||||
'include_hidden': includeHidden,
|
||||
'is_remote': isRemote,
|
||||
})
|
||||
]));
|
||||
}
|
||||
@@ -37,10 +37,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.10"
|
||||
version: "3.6.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -335,7 +335,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
|
||||
resolved-ref: "519350f1f40746798299e94786197d058353bac9"
|
||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@@ -384,10 +384,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_text
|
||||
sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe"
|
||||
sha256: "38c1cac571d6eaf406f4b80040c1f88561e7617ad90795aac6a1be0a8d0bb676"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
version: "14.0.0"
|
||||
extended_text_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -408,10 +408,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.3"
|
||||
ffigen:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -745,10 +745,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.9"
|
||||
version: "1.1.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -793,10 +793,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b
|
||||
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.3"
|
||||
version: "2.10.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -865,18 +865,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.15.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1037,14 +1037,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1325,10 +1317,11 @@ packages:
|
||||
uni_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uni_links
|
||||
sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: uni_links
|
||||
ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f
|
||||
resolved-ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f
|
||||
url: "https://github.com/rustdesk-org/uni_links"
|
||||
source: git
|
||||
version: "0.5.1"
|
||||
uni_links_desktop:
|
||||
dependency: "direct main"
|
||||
@@ -1558,24 +1551,24 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: win32
|
||||
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
version: "5.5.4"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.5"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: f19acdb008645366339444a359a45c3257c8b32e
|
||||
resolved-ref: "85789bfe6e4cfaf4ecc00c52857467fdb7f26879"
|
||||
url: "https://github.com/rustdesk-org/window_manager"
|
||||
source: git
|
||||
version: "0.3.6"
|
||||
@@ -1583,8 +1576,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "plugins/window_size"
|
||||
ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
resolved-ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
||||
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
||||
url: "https://github.com/google/flutter-desktop-embedding.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@@ -1629,5 +1622,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# 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
|
||||
version: 1.3.1+47
|
||||
version: 1.3.3+52
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -46,7 +46,7 @@ dependencies:
|
||||
http: ^1.1.0
|
||||
qr_code_scanner: ^1.0.0
|
||||
zxing2: ^0.2.0
|
||||
image_picker: ^0.8.5
|
||||
image_picker: ^1.1.2
|
||||
image: ^4.0.17
|
||||
back_button_interceptor: ^6.0.1
|
||||
flutter_rust_bridge: "1.80.1"
|
||||
@@ -62,7 +62,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/google/flutter-desktop-embedding.git
|
||||
path: plugins/window_size
|
||||
ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
||||
get: ^4.6.5
|
||||
visibility_detector: ^0.4.0+2
|
||||
contextmenu: ^3.0.0
|
||||
@@ -78,7 +78,11 @@ dependencies:
|
||||
# if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below).
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
||||
uni_links: ^0.5.1
|
||||
uni_links:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/uni_links
|
||||
path: uni_links
|
||||
ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f
|
||||
uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
|
||||
path: ^1.8.1
|
||||
auto_size_text: ^3.0.0
|
||||
@@ -104,7 +108,7 @@ dependencies:
|
||||
pull_down_button: ^0.9.3
|
||||
device_info_plus: ^9.1.0
|
||||
qr_flutter: ^4.1.0
|
||||
extended_text: 13.0.0
|
||||
extended_text: 14.0.0
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
@@ -186,3 +190,4 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
|
||||
@@ -111,11 +111,17 @@ pub struct Enigo {
|
||||
double_click_interval: u32,
|
||||
last_click_time: Option<std::time::Instant>,
|
||||
multiple_click: i64,
|
||||
ignore_flags: bool,
|
||||
flags: CGEventFlags,
|
||||
char_to_vkey_map: Map<String, Map<char, CGKeyCode>>,
|
||||
}
|
||||
|
||||
impl Enigo {
|
||||
/// Set if ignore flags when posting events.
|
||||
pub fn set_ignore_flags(&mut self, ignore: bool) {
|
||||
self.ignore_flags = ignore;
|
||||
}
|
||||
|
||||
///
|
||||
pub fn reset_flag(&mut self) {
|
||||
self.flags = CGEventFlags::CGEventFlagNull;
|
||||
@@ -136,7 +142,9 @@ impl Enigo {
|
||||
}
|
||||
|
||||
fn post(&self, event: CGEvent) {
|
||||
event.set_flags(self.flags);
|
||||
if !self.ignore_flags {
|
||||
event.set_flags(self.flags);
|
||||
}
|
||||
event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE);
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
@@ -164,6 +172,7 @@ impl Default for Enigo {
|
||||
double_click_interval,
|
||||
multiple_click: 1,
|
||||
last_click_time: None,
|
||||
ignore_flags: false,
|
||||
flags: CGEventFlags::CGEventFlagNull,
|
||||
char_to_vkey_map: Default::default(),
|
||||
}
|
||||
|
||||
@@ -368,6 +368,16 @@ message ReadDir {
|
||||
bool include_hidden = 2;
|
||||
}
|
||||
|
||||
message ReadEmptyDirs {
|
||||
string path = 1;
|
||||
bool include_hidden = 2;
|
||||
}
|
||||
|
||||
message ReadEmptyDirsResponse {
|
||||
string path = 1;
|
||||
repeated FileDirectory empty_dirs = 2;
|
||||
}
|
||||
|
||||
message ReadAllFiles {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
@@ -392,6 +402,7 @@ message FileAction {
|
||||
FileTransferCancel cancel = 8;
|
||||
FileTransferSendConfirmRequest send_confirm = 9;
|
||||
FileRename rename = 10;
|
||||
ReadEmptyDirs read_empty_dirs = 11;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +415,7 @@ message FileResponse {
|
||||
FileTransferError error = 3;
|
||||
FileTransferDone done = 4;
|
||||
FileTransferDigest digest = 5;
|
||||
ReadEmptyDirsResponse empty_dirs = 6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -965,6 +965,10 @@ impl Config {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_bool_option(k: &str) -> bool {
|
||||
option2bool(k, &Self::get_option(k))
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
|
||||
return;
|
||||
@@ -1558,6 +1562,21 @@ impl LocalConfig {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// Usually get_option should be used.
|
||||
pub fn get_option_from_file(k: &str) -> String {
|
||||
get_or(
|
||||
&OVERWRITE_LOCAL_SETTINGS,
|
||||
&Self::load().options,
|
||||
&DEFAULT_LOCAL_SETTINGS,
|
||||
k,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_bool_option(k: &str) -> bool {
|
||||
option2bool(k, &Self::get_option(k))
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) {
|
||||
return;
|
||||
@@ -1691,6 +1710,9 @@ impl UserDefaultConfig {
|
||||
|
||||
pub fn get(&self, key: &str) -> String {
|
||||
match key {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
keys::OPTION_VIEW_STYLE => self.get_string(key, "adaptive", vec!["original"]),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]),
|
||||
keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||
keys::OPTION_IMAGE_QUALITY => {
|
||||
@@ -2198,6 +2220,7 @@ pub mod keys {
|
||||
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
||||
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
||||
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
||||
pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing";
|
||||
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
||||
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
||||
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
||||
@@ -2205,6 +2228,7 @@ pub mod keys {
|
||||
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
||||
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
|
||||
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
|
||||
pub const OPTION_VERIFICATION_METHOD: &str = "verification-method";
|
||||
pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server";
|
||||
pub const OPTION_API_SERVER: &str = "api-server";
|
||||
pub const OPTION_KEY: &str = "key";
|
||||
@@ -2214,6 +2238,7 @@ pub mod keys {
|
||||
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
|
||||
"enable-android-software-encoding-half-scale";
|
||||
pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices";
|
||||
pub const OPTION_AV1_TEST: &str = "av1-test";
|
||||
|
||||
// buildin options
|
||||
pub const OPTION_DISPLAY_NAME: &str = "display-name";
|
||||
@@ -2321,6 +2346,8 @@ pub mod keys {
|
||||
OPTION_DISABLE_GROUP_PANEL,
|
||||
OPTION_PRE_ELEVATE_SERVICE,
|
||||
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
|
||||
OPTION_ALLOW_AUTO_RECORD_OUTGOING,
|
||||
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||
];
|
||||
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||
pub const KEYS_SETTINGS: &[&str] = &[
|
||||
@@ -2342,13 +2369,13 @@ pub mod keys {
|
||||
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
||||
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
||||
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
||||
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||
OPTION_ENABLE_ABR,
|
||||
OPTION_ALLOW_REMOVE_WALLPAPER,
|
||||
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
||||
OPTION_ALLOW_LINUX_HEADLESS,
|
||||
OPTION_ENABLE_HWCODEC,
|
||||
OPTION_APPROVE_MODE,
|
||||
OPTION_VERIFICATION_METHOD,
|
||||
OPTION_PROXY_URL,
|
||||
OPTION_PROXY_USERNAME,
|
||||
OPTION_PROXY_PASSWORD,
|
||||
|
||||
@@ -185,6 +185,51 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<F
|
||||
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
|
||||
}
|
||||
|
||||
fn read_empty_dirs_recursive(
|
||||
path: &PathBuf,
|
||||
prefix: &Path,
|
||||
include_hidden: bool,
|
||||
) -> ResultType<Vec<FileDirectory>> {
|
||||
let mut dirs = Vec::new();
|
||||
if path.is_dir() {
|
||||
// to-do: symbol link handling, cp the link rather than the content
|
||||
// to-do: file mode, for unix
|
||||
let fd = read_dir(path, include_hidden)?;
|
||||
if fd.entries.is_empty() {
|
||||
dirs.push(fd);
|
||||
} else {
|
||||
for entry in fd.entries.iter() {
|
||||
match entry.entry_type.enum_value() {
|
||||
Ok(FileType::Dir) => {
|
||||
if let Ok(mut tmp) = read_empty_dirs_recursive(
|
||||
&path.join(&entry.name),
|
||||
&prefix.join(&entry.name),
|
||||
include_hidden,
|
||||
) {
|
||||
for entry in tmp.drain(0..) {
|
||||
dirs.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(dirs)
|
||||
} else if path.is_file() {
|
||||
Ok(dirs)
|
||||
} else {
|
||||
bail!("Not exists");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_empty_dirs_recursive(
|
||||
path: &str,
|
||||
include_hidden: bool,
|
||||
) -> ResultType<Vec<FileDirectory>> {
|
||||
read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_file_exists(file_path: &str) -> bool {
|
||||
return Path::new(file_path).exists();
|
||||
|
||||
@@ -27,6 +27,7 @@ pub use anyhow::{self, bail};
|
||||
pub use futures_util;
|
||||
pub mod config;
|
||||
pub mod fs;
|
||||
pub mod mem;
|
||||
pub use lazy_static;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use mac_address;
|
||||
|
||||
14
libs/hbb_common/src/mem.rs
Normal file
14
libs/hbb_common/src/mem.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
/// SAFETY: the returned Vec must not be resized or reserverd
|
||||
pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec<u8> {
|
||||
use std::alloc::*;
|
||||
|
||||
let layout =
|
||||
Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2");
|
||||
unsafe {
|
||||
let ptr = alloc(layout);
|
||||
if ptr.is_null() {
|
||||
panic!("failed to allocate {} bytes", cap);
|
||||
}
|
||||
Vec::from_raw_parts(ptr, 0, cap)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true }
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
optional = true
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,11 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
|
||||
target = target.replace("x64", "x86");
|
||||
}
|
||||
println!("cargo:info={}", target);
|
||||
path.push("installed");
|
||||
if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
|
||||
path = vcpkg_root.into();
|
||||
} else {
|
||||
path.push("installed");
|
||||
}
|
||||
path.push(target);
|
||||
println!(
|
||||
"{}",
|
||||
|
||||
@@ -5,16 +5,20 @@ use jni::sys::jboolean;
|
||||
use jni::JNIEnv;
|
||||
use jni::{
|
||||
objects::{GlobalRef, JClass, JObject},
|
||||
strings::JNIString,
|
||||
JavaVM,
|
||||
};
|
||||
|
||||
use hbb_common::{message_proto::MultiClipboards, protobuf::Message};
|
||||
use jni::errors::{Error as JniError, Result as JniResult};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Not;
|
||||
use std::os::raw::c_void;
|
||||
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
lazy_static! {
|
||||
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
|
||||
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
|
||||
@@ -22,6 +26,9 @@ lazy_static! {
|
||||
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
|
||||
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
|
||||
static ref MEDIA_CODEC_INFOS: RwLock<Option<MediaCodecInfos>> = RwLock::new(None);
|
||||
static ref CLIPBOARD_MANAGER: RwLock<Option<GlobalRef>> = RwLock::new(None);
|
||||
static ref CLIPBOARDS_HOST: Mutex<Option<MultiClipboards>> = Mutex::new(None);
|
||||
static ref CLIPBOARDS_CLIENT: Mutex<Option<MultiClipboards>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
@@ -104,6 +111,14 @@ pub fn get_audio_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||
AUDIO_RAW.lock().ok()?.take(dst, last)
|
||||
}
|
||||
|
||||
pub fn get_clipboards(client: bool) -> Option<MultiClipboards> {
|
||||
if client {
|
||||
CLIPBOARDS_CLIENT.lock().ok()?.take()
|
||||
} else {
|
||||
CLIPBOARDS_HOST.lock().ok()?.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate(
|
||||
env: JNIEnv,
|
||||
@@ -132,6 +147,27 @@ pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onClipboardUpdate(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
buffer: JByteBuffer,
|
||||
) {
|
||||
if let Ok(data) = env.get_direct_buffer_address(&buffer) {
|
||||
if let Ok(len) = env.get_direct_buffer_capacity(&buffer) {
|
||||
let data = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) {
|
||||
let is_client = data[0] == 1;
|
||||
if is_client {
|
||||
*CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips);
|
||||
} else {
|
||||
*CLIPBOARDS_HOST.lock().unwrap() = Some(clips);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
|
||||
env: JNIEnv,
|
||||
@@ -155,10 +191,36 @@ pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
|
||||
pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) {
|
||||
log::debug!("MainService init from java");
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
*JVM.write().unwrap() = Some(jvm);
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let mut jvm_lock = JVM.write().unwrap();
|
||||
if jvm_lock.is_none() {
|
||||
*jvm_lock = Some(jvm);
|
||||
}
|
||||
drop(jvm_lock);
|
||||
if let Ok(context) = env.new_global_ref(ctx) {
|
||||
let context_jobject = context.as_obj().as_raw() as *mut c_void;
|
||||
*MAIN_SERVICE_CTX.write().unwrap() = Some(context);
|
||||
init_ndk_context().ok();
|
||||
init_ndk_context(java_vm, context_jobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_setClipboardManager(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
clipboard_manager: JObject,
|
||||
) {
|
||||
log::debug!("ClipboardManager init from java");
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let mut jvm_lock = JVM.write().unwrap();
|
||||
if jvm_lock.is_none() {
|
||||
*jvm_lock = Some(jvm);
|
||||
}
|
||||
drop(jvm_lock);
|
||||
if let Ok(manager) = env.new_global_ref(clipboard_manager) {
|
||||
*CLIPBOARD_MANAGER.write().unwrap() = Some(manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +334,59 @@ pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn _call_clipboard_manager<S, T>(name: S, sig: T, args: &[JValue]) -> JniResult<()>
|
||||
where
|
||||
S: Into<JNIString>,
|
||||
T: Into<JNIString> + AsRef<str>,
|
||||
{
|
||||
if let (Some(jvm), Some(cm)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread()?;
|
||||
env.call_method(cm, name, sig, args)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> {
|
||||
if let (Some(jvm), Some(cm)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
|
||||
) {
|
||||
let mut env = jvm.attach_current_thread()?;
|
||||
let data = env.byte_array_from_slice(data)?;
|
||||
|
||||
env.call_method(
|
||||
cm,
|
||||
"rustUpdateClipboard",
|
||||
"([B)V",
|
||||
&[JValue::Object(&JObject::from(data))],
|
||||
)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(JniError::ThrowFailed(-1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_clipboard_manager_enable_service_clipboard(enable: bool) -> JniResult<()> {
|
||||
_call_clipboard_manager(
|
||||
"rustEnableServiceClipboard",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(jboolean::from(enable))],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> {
|
||||
_call_clipboard_manager(
|
||||
"rustEnableClientClipboard",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(jboolean::from(enable))],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
|
||||
if let (Some(jvm), Some(ctx)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
@@ -332,7 +447,14 @@ pub fn call_main_service_set_by_name(
|
||||
}
|
||||
}
|
||||
|
||||
fn init_ndk_context() -> JniResult<()> {
|
||||
// Difference between MainService, MainActivity, JNI_OnLoad:
|
||||
// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null.
|
||||
// cpal: all three works
|
||||
// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions
|
||||
// On app start: JNI_OnLoad or MainActivity init context
|
||||
// On service start first time: MainService replace the context
|
||||
|
||||
fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) {
|
||||
let mut lock = NDK_CONTEXT_INITED.lock().unwrap();
|
||||
if *lock {
|
||||
unsafe {
|
||||
@@ -340,22 +462,20 @@ fn init_ndk_context() -> JniResult<()> {
|
||||
}
|
||||
*lock = false;
|
||||
}
|
||||
if let (Some(jvm), Some(ctx)) = (
|
||||
JVM.read().unwrap().as_ref(),
|
||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||
) {
|
||||
unsafe {
|
||||
ndk_context::initialize_android_context(
|
||||
jvm.get_java_vm_pointer() as _,
|
||||
ctx.as_obj().as_raw() as _,
|
||||
);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hwcodec::android::ffmpeg_set_java_vm(
|
||||
jvm.get_java_vm_pointer() as _,
|
||||
);
|
||||
}
|
||||
*lock = true;
|
||||
return Ok(());
|
||||
unsafe {
|
||||
ndk_context::initialize_android_context(java_vm, context_jobject);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hwcodec::android::ffmpeg_set_java_vm(java_vm);
|
||||
}
|
||||
Err(JniError::ThrowFailed(-1))
|
||||
*lock = true;
|
||||
}
|
||||
|
||||
// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init
|
||||
#[no_mangle]
|
||||
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
|
||||
if let Ok(env) = vm.get_env() {
|
||||
let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void;
|
||||
init_ndk_context(vm, res);
|
||||
}
|
||||
jni::JNIVersion::V6.into()
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ mod webrtc {
|
||||
const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling.
|
||||
const kBitDepth: u32 = 8;
|
||||
const kLagInFrames: u32 = 0; // No look ahead.
|
||||
const kRtpTicksPerSecond: i32 = 90000;
|
||||
pub(super) const kTimeBaseDen: i64 = 1000;
|
||||
const kMinimumFrameRate: f64 = 1.0;
|
||||
|
||||
pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63
|
||||
@@ -108,7 +108,7 @@ mod webrtc {
|
||||
c.g_h = cfg.height;
|
||||
c.g_threads = codec_thread_num(64) as _;
|
||||
c.g_timebase.num = 1;
|
||||
c.g_timebase.den = kRtpTicksPerSecond;
|
||||
c.g_timebase.den = kTimeBaseDen as _;
|
||||
c.g_input_bit_depth = kBitDepth;
|
||||
if let Some(keyframe_interval) = cfg.keyframe_interval {
|
||||
c.kf_min_dist = 0;
|
||||
@@ -313,7 +313,7 @@ impl EncoderApi for AomEncoder {
|
||||
}
|
||||
|
||||
impl AomEncoder {
|
||||
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
pub fn encode(&mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
@@ -333,13 +333,14 @@ impl AomEncoder {
|
||||
stride_align as _,
|
||||
data.as_ptr() as _,
|
||||
));
|
||||
|
||||
let pts = webrtc::kTimeBaseDen / 1000 * ms;
|
||||
let duration = webrtc::kTimeBaseDen / 1000;
|
||||
call_aom!(aom_codec_encode(
|
||||
&mut self.ctx,
|
||||
&image,
|
||||
pts as _,
|
||||
1, // Duration
|
||||
0, // Flags
|
||||
duration as _, // Duration
|
||||
0, // Flags
|
||||
));
|
||||
|
||||
Ok(EncodeFrames {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
|
||||
common::GoogleImage,
|
||||
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
|
||||
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb,
|
||||
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture,
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
@@ -28,7 +28,6 @@ use hbb_common::{
|
||||
SupportedDecoding, SupportedEncoding, VideoFrame,
|
||||
},
|
||||
sysinfo::System,
|
||||
tokio::time::Instant,
|
||||
ResultType,
|
||||
};
|
||||
|
||||
@@ -264,15 +263,20 @@ impl Encoder {
|
||||
.unwrap_or((PreferCodec::Auto.into(), 0));
|
||||
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
|
||||
|
||||
// auto: h265 > h264 > vp9/vp8
|
||||
let mut auto_codec = CodecFormat::VP9;
|
||||
// auto: h265 > h264 > av1/vp9/vp8
|
||||
let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N";
|
||||
let mut auto_codec = if av1_useable && av1_test {
|
||||
CodecFormat::AV1
|
||||
} else {
|
||||
CodecFormat::VP9
|
||||
};
|
||||
if h264_useable {
|
||||
auto_codec = CodecFormat::H264;
|
||||
}
|
||||
if h265_useable {
|
||||
auto_codec = CodecFormat::H265;
|
||||
}
|
||||
if auto_codec == CodecFormat::VP9 {
|
||||
if auto_codec == CodecFormat::VP9 || auto_codec == CodecFormat::AV1 {
|
||||
let mut system = System::new();
|
||||
system.refresh_memory();
|
||||
if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 {
|
||||
@@ -623,7 +627,7 @@ impl Decoder {
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
rgb: &mut ImageRgb,
|
||||
_texture: &mut *mut c_void,
|
||||
_texture: &mut ImageTexture,
|
||||
_pixelbuffer: &mut bool,
|
||||
chroma: &mut Option<Chroma>,
|
||||
) -> ResultType<bool> {
|
||||
@@ -777,12 +781,16 @@ impl Decoder {
|
||||
fn handle_vram_video_frame(
|
||||
decoder: &mut VRamDecoder,
|
||||
frames: &EncodedVideoFrames,
|
||||
texture: &mut *mut c_void,
|
||||
texture: &mut ImageTexture,
|
||||
) -> ResultType<bool> {
|
||||
let mut ret = false;
|
||||
for h26x in frames.frames.iter() {
|
||||
for image in decoder.decode(&h26x.data)? {
|
||||
*texture = image.frame.texture;
|
||||
*texture = ImageTexture {
|
||||
texture: image.frame.texture,
|
||||
w: image.frame.width as _,
|
||||
h: image.frame.height as _,
|
||||
};
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
@@ -978,3 +986,124 @@ fn disable_av1() -> bool {
|
||||
// disable it for all 32 bit platforms
|
||||
std::mem::size_of::<usize>() == 4
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn test_av1() {
|
||||
use hbb_common::config::keys::OPTION_AV1_TEST;
|
||||
use hbb_common::rand::Rng;
|
||||
use std::{sync::Once, time::Duration};
|
||||
|
||||
if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() {
|
||||
log::info!("skip test av1");
|
||||
return;
|
||||
}
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let f = || {
|
||||
let (width, height, quality, keyframe_interval, i444) =
|
||||
(1920, 1080, Quality::Balanced, None, false);
|
||||
let frame_count = 10;
|
||||
let block_size = 300;
|
||||
let move_step = 50;
|
||||
let generate_fake_data =
|
||||
|frame_index: u32, dst_fmt: EncodeYuvFormat| -> ResultType<Vec<u8>> {
|
||||
let mut rng = hbb_common::rand::thread_rng();
|
||||
let mut bgra = vec![0u8; (width * height * 4) as usize];
|
||||
let gradient = frame_index as f32 / frame_count as f32;
|
||||
// floating block
|
||||
let x0 = (frame_index * move_step) % (width - block_size);
|
||||
let y0 = (frame_index * move_step) % (height - block_size);
|
||||
// Fill the block with random colors
|
||||
for y in 0..block_size {
|
||||
for x in 0..block_size {
|
||||
let index = (((y0 + y) * width + x0 + x) * 4) as usize;
|
||||
if index + 3 < bgra.len() {
|
||||
let noise = rng.gen_range(0..255) as f32 / 255.0;
|
||||
let value = (255.0 * gradient + noise * 50.0) as u8;
|
||||
bgra[index] = value;
|
||||
bgra[index + 1] = value;
|
||||
bgra[index + 2] = value;
|
||||
bgra[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_uv = dst_fmt.stride[1];
|
||||
let mut dst = vec![0u8; (dst_fmt.h * dst_stride_y * 2) as usize];
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||
let res = unsafe {
|
||||
crate::ARGBToI420(
|
||||
bgra.as_ptr(),
|
||||
(width * 4) as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_uv as _,
|
||||
dst_v,
|
||||
dst_stride_uv as _,
|
||||
width as _,
|
||||
height as _,
|
||||
)
|
||||
};
|
||||
if res != 0 {
|
||||
bail!("ARGBToI420 failed: {}", res);
|
||||
}
|
||||
Ok(dst)
|
||||
};
|
||||
let Ok(mut av1) = AomEncoder::new(
|
||||
EncoderCfg::AOM(AomEncoderConfig {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
keyframe_interval,
|
||||
}),
|
||||
i444,
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let mut key_frame_time = Duration::ZERO;
|
||||
let mut non_key_frame_time_sum = Duration::ZERO;
|
||||
let pts = Instant::now();
|
||||
let yuvfmt = av1.yuvfmt();
|
||||
for i in 0..frame_count {
|
||||
let Ok(yuv) = generate_fake_data(i, yuvfmt.clone()) else {
|
||||
return false;
|
||||
};
|
||||
let start = Instant::now();
|
||||
if av1
|
||||
.encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN)
|
||||
.is_err()
|
||||
{
|
||||
log::debug!("av1 encode failed");
|
||||
if i == 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
key_frame_time = start.elapsed();
|
||||
} else {
|
||||
non_key_frame_time_sum += start.elapsed();
|
||||
}
|
||||
}
|
||||
let non_key_frame_time = non_key_frame_time_sum / (frame_count - 1);
|
||||
log::info!(
|
||||
"av1 time: key: {:?}, non-key: {:?}, consume: {:?}",
|
||||
key_frame_time,
|
||||
non_key_frame_time,
|
||||
pts.elapsed()
|
||||
);
|
||||
key_frame_time < Duration::from_millis(90)
|
||||
&& non_key_frame_time < Duration::from_millis(30)
|
||||
};
|
||||
std::thread::spawn(move || {
|
||||
let v = f();
|
||||
Config::set_option(
|
||||
OPTION_AV1_TEST.to_string(),
|
||||
if v { "Y" } else { "N" }.to_string(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use hbb_common::{
|
||||
};
|
||||
use hwcodec::{
|
||||
common::{
|
||||
DataFormat,
|
||||
DataFormat, HwcodecErrno,
|
||||
Quality::{self, *},
|
||||
RateControl::{self, *},
|
||||
},
|
||||
@@ -31,6 +31,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12;
|
||||
pub const DEFAULT_FPS: i32 = 30;
|
||||
const DEFAULT_GOP: i32 = i32::MAX;
|
||||
const DEFAULT_HW_QUALITY: Quality = Quality_Default;
|
||||
pub const ERR_HEVC_POC: i32 = HwcodecErrno::HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC as i32;
|
||||
|
||||
crate::generate_call_macro!(call_yuv, false);
|
||||
|
||||
@@ -69,7 +70,7 @@ impl EncoderApi for HwRamEncoder {
|
||||
let b = Self::convert_quality(&config.name, config.quality);
|
||||
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
||||
let mut bitrate = base_bitrate * b / 100;
|
||||
if base_bitrate <= 0 {
|
||||
if bitrate <= 0 {
|
||||
bitrate = base_bitrate;
|
||||
}
|
||||
bitrate = Self::check_bitrate_range(&config, bitrate);
|
||||
@@ -179,7 +180,7 @@ impl EncoderApi for HwRamEncoder {
|
||||
let b = Self::convert_quality(&self.config.name, quality);
|
||||
let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100;
|
||||
if bitrate > 0 {
|
||||
bitrate = Self::check_bitrate_range(&self.config, self.bitrate);
|
||||
bitrate = Self::check_bitrate_range(&self.config, bitrate);
|
||||
self.encoder.set_bitrate(bitrate as _).ok();
|
||||
self.bitrate = bitrate;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,22 @@ impl ImageRgb {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageTexture {
|
||||
pub texture: *mut c_void,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
}
|
||||
|
||||
impl Default for ImageTexture {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
texture: std::ptr::null_mut(),
|
||||
w: 0,
|
||||
h: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
|
||||
// does this really help?
|
||||
@@ -156,7 +172,7 @@ pub trait TraitPixelBuffer {
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub enum Frame<'a> {
|
||||
PixelBuffer(PixelBuffer<'a>),
|
||||
Texture(*mut c_void),
|
||||
Texture((*mut c_void, usize)),
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
@@ -164,7 +180,7 @@ impl Frame<'_> {
|
||||
pub fn valid<'a>(&'a self) -> bool {
|
||||
match self {
|
||||
Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(),
|
||||
Frame::Texture(texture) => !texture.is_null(),
|
||||
Frame::Texture((texture, _)) => !texture.is_null(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +202,7 @@ impl Frame<'_> {
|
||||
|
||||
pub enum EncodeInput<'a> {
|
||||
YUV(&'a [u8]),
|
||||
Texture(*mut c_void),
|
||||
Texture((*mut c_void, usize)),
|
||||
}
|
||||
|
||||
impl<'a> EncodeInput<'a> {
|
||||
@@ -197,7 +213,7 @@ impl<'a> EncodeInput<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> ResultType<*mut c_void> {
|
||||
pub fn texture(&self) -> ResultType<(*mut c_void, usize)> {
|
||||
match self {
|
||||
Self::Texture(f) => Ok(*f),
|
||||
_ => bail!("not texture frame"),
|
||||
@@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&video_frame::Union> for CodecFormat {
|
||||
fn from(it: &video_frame::Union) -> Self {
|
||||
match it {
|
||||
video_frame::Union::Vp8s(_) => CodecFormat::VP8,
|
||||
video_frame::Union::Vp9s(_) => CodecFormat::VP9,
|
||||
video_frame::Union::Av1s(_) => CodecFormat::AV1,
|
||||
video_frame::Union::H264s(_) => CodecFormat::H264,
|
||||
video_frame::Union::H265s(_) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodecName> for CodecFormat {
|
||||
fn from(value: &CodecName) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -25,22 +25,28 @@ pub struct RecorderContext {
|
||||
pub server: bool,
|
||||
pub id: String,
|
||||
pub dir: String,
|
||||
pub display: usize,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecorderContext2 {
|
||||
pub filename: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub format: CodecFormat,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
impl RecorderContext {
|
||||
pub fn set_filename(&mut self) -> ResultType<()> {
|
||||
if !PathBuf::from(&self.dir).exists() {
|
||||
std::fs::create_dir_all(&self.dir)?;
|
||||
impl RecorderContext2 {
|
||||
pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> {
|
||||
if !PathBuf::from(&ctx.dir).exists() {
|
||||
std::fs::create_dir_all(&ctx.dir)?;
|
||||
}
|
||||
let file = if self.server { "incoming" } else { "outgoing" }.to_string()
|
||||
let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
|
||||
+ "_"
|
||||
+ &self.id.clone()
|
||||
+ &ctx.id.clone()
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
|
||||
+ &format!("display{}_", ctx.display)
|
||||
+ &self.format.to_string().to_lowercase()
|
||||
+ if self.format == CodecFormat::VP9
|
||||
|| self.format == CodecFormat::VP8
|
||||
@@ -50,11 +56,10 @@ impl RecorderContext {
|
||||
} else {
|
||||
".mp4"
|
||||
};
|
||||
self.filename = PathBuf::from(&self.dir)
|
||||
self.filename = PathBuf::from(&ctx.dir)
|
||||
.join(file)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
log::info!("video will save to {}", self.filename);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -63,7 +68,7 @@ unsafe impl Send for Recorder {}
|
||||
unsafe impl Sync for Recorder {}
|
||||
|
||||
pub trait RecorderApi {
|
||||
fn new(ctx: RecorderContext) -> ResultType<Self>
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
|
||||
@@ -78,13 +83,15 @@ pub enum RecordState {
|
||||
}
|
||||
|
||||
pub struct Recorder {
|
||||
pub inner: Box<dyn RecorderApi>,
|
||||
pub inner: Option<Box<dyn RecorderApi>>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: Option<RecorderContext2>,
|
||||
pts: Option<i64>,
|
||||
check_failed: bool,
|
||||
}
|
||||
|
||||
impl Deref for Recorder {
|
||||
type Target = Box<dyn RecorderApi>;
|
||||
type Target = Option<Box<dyn RecorderApi>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -98,114 +105,124 @@ impl DerefMut for Recorder {
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
||||
ctx.set_filename()?;
|
||||
let recorder = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder {
|
||||
inner: Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
ctx,
|
||||
pts: None,
|
||||
},
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Recorder {
|
||||
inner: Box::new(HwRecorder::new(ctx.clone())?),
|
||||
ctx,
|
||||
pts: None,
|
||||
},
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
};
|
||||
recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone()));
|
||||
Ok(recorder)
|
||||
pub fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
Ok(Self {
|
||||
inner: None,
|
||||
ctx,
|
||||
ctx2: None,
|
||||
pts: None,
|
||||
check_failed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
|
||||
ctx.set_filename()?;
|
||||
self.inner = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => {
|
||||
Box::new(WebmRecorder::new(ctx.clone())?)
|
||||
fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
||||
match self.ctx2 {
|
||||
Some(ref ctx2) => {
|
||||
if ctx2.width != w || ctx2.height != h || ctx2.format != format {
|
||||
let mut ctx2 = RecorderContext2 {
|
||||
width: w,
|
||||
height: h,
|
||||
format,
|
||||
filename: Default::default(),
|
||||
};
|
||||
ctx2.set_filename(&self.ctx)?;
|
||||
self.ctx2 = Some(ctx2);
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Box::new(HwRecorder::new(ctx.clone())?),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
None => {
|
||||
let mut ctx2 = RecorderContext2 {
|
||||
width: w,
|
||||
height: h,
|
||||
format,
|
||||
filename: Default::default(),
|
||||
};
|
||||
ctx2.set_filename(&self.ctx)?;
|
||||
self.ctx2 = Some(ctx2);
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
let Some(ctx2) = &self.ctx2 else {
|
||||
bail!("ctx2 is None");
|
||||
};
|
||||
self.ctx = ctx;
|
||||
self.pts = None;
|
||||
self.send_state(RecordState::NewFile(self.ctx.filename.clone()));
|
||||
if self.inner.is_none() {
|
||||
self.inner = match format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new(
|
||||
WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?,
|
||||
)),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Some(Box::new(HwRecorder::new(
|
||||
self.ctx.clone(),
|
||||
(*ctx2).clone(),
|
||||
)?)),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
};
|
||||
// pts is None when new inner is created
|
||||
self.pts = None;
|
||||
self.send_state(RecordState::NewFile(ctx2.filename.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_message(&mut self, msg: &Message) {
|
||||
pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) {
|
||||
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
|
||||
if let Some(frame) = &vf.union {
|
||||
self.write_frame(frame).ok();
|
||||
self.write_frame(frame, w, h).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
|
||||
pub fn write_frame(
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> ResultType<()> {
|
||||
if self.check_failed {
|
||||
bail!("check failed");
|
||||
}
|
||||
let format = CodecFormat::from(frame);
|
||||
if format == CodecFormat::Unknown {
|
||||
bail!("unsupported frame type");
|
||||
}
|
||||
let res = self.check(w, h, format);
|
||||
if res.is_err() {
|
||||
self.check_failed = true;
|
||||
log::error!("check failed: {:?}", res);
|
||||
res?;
|
||||
}
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
if self.ctx.format != CodecFormat::VP8 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::VP8,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in vp8s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if self.ctx.format != CodecFormat::VP9 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::VP9,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in vp9s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Av1s(av1s) => {
|
||||
if self.ctx.format != CodecFormat::AV1 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::AV1,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in av1s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
if self.ctx.format != CodecFormat::H264 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::H264,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in h264s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
if self.ctx.format != CodecFormat::H265 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::H265,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in h265s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
_ => bail!("unsupported frame type"),
|
||||
@@ -214,13 +231,31 @@ impl Recorder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_pts(&mut self, pts: i64) -> ResultType<()> {
|
||||
fn check_pts(
|
||||
&mut self,
|
||||
pts: i64,
|
||||
key: bool,
|
||||
w: usize,
|
||||
h: usize,
|
||||
format: CodecFormat,
|
||||
) -> ResultType<()> {
|
||||
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
|
||||
if self.pts.is_none() && !key {
|
||||
bail!("first frame is not key frame");
|
||||
}
|
||||
let old_pts = self.pts;
|
||||
self.pts = Some(pts);
|
||||
if old_pts.clone().unwrap_or_default() > pts {
|
||||
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
|
||||
self.change(self.ctx.clone())?;
|
||||
self.inner = None;
|
||||
self.ctx2 = None;
|
||||
let res = self.check(w, h, format);
|
||||
if res.is_err() {
|
||||
self.check_failed = true;
|
||||
log::error!("check failed: {:?}", res);
|
||||
res?;
|
||||
}
|
||||
self.pts = Some(pts);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -234,21 +269,22 @@ struct WebmRecorder {
|
||||
vt: VideoTrack,
|
||||
webm: Option<Segment<Writer<File>>>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
key: bool,
|
||||
written: bool,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl RecorderApi for WebmRecorder {
|
||||
fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||
let out = match {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&ctx.filename)
|
||||
.open(&ctx2.filename)
|
||||
} {
|
||||
Ok(file) => file,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
|
||||
@@ -256,18 +292,18 @@ impl RecorderApi for WebmRecorder {
|
||||
None => bail!("Failed to create webm mux"),
|
||||
};
|
||||
let vt = webm.add_video_track(
|
||||
ctx.width as _,
|
||||
ctx.height as _,
|
||||
ctx2.width as _,
|
||||
ctx2.height as _,
|
||||
None,
|
||||
if ctx.format == CodecFormat::VP9 {
|
||||
if ctx2.format == CodecFormat::VP9 {
|
||||
mux::VideoCodecId::VP9
|
||||
} else if ctx.format == CodecFormat::VP8 {
|
||||
} else if ctx2.format == CodecFormat::VP8 {
|
||||
mux::VideoCodecId::VP8
|
||||
} else {
|
||||
mux::VideoCodecId::AV1
|
||||
},
|
||||
);
|
||||
if ctx.format == CodecFormat::AV1 {
|
||||
if ctx2.format == CodecFormat::AV1 {
|
||||
// [129, 8, 12, 0] in 3.6.0, but zero works
|
||||
let codec_private = vec![0, 0, 0, 0];
|
||||
if !webm.set_codec_private(vt.track_number(), &codec_private) {
|
||||
@@ -278,6 +314,7 @@ impl RecorderApi for WebmRecorder {
|
||||
vt,
|
||||
webm: Some(webm),
|
||||
ctx,
|
||||
ctx2,
|
||||
key: false,
|
||||
written: false,
|
||||
start: Instant::now(),
|
||||
@@ -307,7 +344,7 @@ impl Drop for WebmRecorder {
|
||||
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
|
||||
let mut state = RecordState::WriteTail;
|
||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||
std::fs::remove_file(&self.ctx.filename).ok();
|
||||
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||
state = RecordState::RemoveFile;
|
||||
}
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
@@ -316,8 +353,9 @@ impl Drop for WebmRecorder {
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
struct HwRecorder {
|
||||
muxer: Muxer,
|
||||
muxer: Option<Muxer>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
written: bool,
|
||||
key: bool,
|
||||
start: Instant,
|
||||
@@ -325,18 +363,19 @@ struct HwRecorder {
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
impl RecorderApi for HwRecorder {
|
||||
fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||
let muxer = Muxer::new(MuxContext {
|
||||
filename: ctx.filename.clone(),
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
is265: ctx.format == CodecFormat::H265,
|
||||
filename: ctx2.filename.clone(),
|
||||
width: ctx2.width,
|
||||
height: ctx2.height,
|
||||
is265: ctx2.format == CodecFormat::H265,
|
||||
framerate: crate::hwcodec::DEFAULT_FPS as _,
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||
Ok(HwRecorder {
|
||||
muxer,
|
||||
muxer: Some(muxer),
|
||||
ctx,
|
||||
ctx2,
|
||||
written: false,
|
||||
key: false,
|
||||
start: Instant::now(),
|
||||
@@ -348,7 +387,11 @@ impl RecorderApi for HwRecorder {
|
||||
self.key = true;
|
||||
}
|
||||
if self.key {
|
||||
let ok = self.muxer.write_video(&frame.data, frame.key).is_ok();
|
||||
let ok = self
|
||||
.muxer
|
||||
.as_mut()
|
||||
.map(|m| m.write_video(&frame.data, frame.key).is_ok())
|
||||
.unwrap_or_default();
|
||||
if ok {
|
||||
self.written = true;
|
||||
}
|
||||
@@ -362,10 +405,12 @@ impl RecorderApi for HwRecorder {
|
||||
#[cfg(feature = "hwcodec")]
|
||||
impl Drop for HwRecorder {
|
||||
fn drop(&mut self) {
|
||||
self.muxer.write_tail().ok();
|
||||
self.muxer.as_mut().map(|m| m.write_tail().ok());
|
||||
let mut state = RecordState::WriteTail;
|
||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||
std::fs::remove_file(&self.ctx.filename).ok();
|
||||
// The process cannot access the file because it is being used by another process
|
||||
self.muxer = None;
|
||||
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||
state = RecordState::RemoveFile;
|
||||
}
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
|
||||
@@ -64,7 +64,7 @@ impl EncoderApi for VRamEncoder {
|
||||
let b = Self::convert_quality(config.quality, &config.feature);
|
||||
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
||||
let mut bitrate = base_bitrate * b / 100;
|
||||
if base_bitrate <= 0 {
|
||||
if bitrate <= 0 {
|
||||
bitrate = base_bitrate;
|
||||
}
|
||||
let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32;
|
||||
@@ -101,7 +101,12 @@ impl EncoderApi for VRamEncoder {
|
||||
frame: EncodeInput,
|
||||
ms: i64,
|
||||
) -> ResultType<hbb_common::message_proto::VideoFrame> {
|
||||
let texture = frame.texture()?;
|
||||
let (texture, rotation) = frame.texture()?;
|
||||
if rotation != 0 {
|
||||
// to-do: support rotation
|
||||
// Both the encoder and display(w,h) information need to be changed.
|
||||
bail!("rotation not supported");
|
||||
}
|
||||
let mut vf = VideoFrame::new();
|
||||
let mut frames = Vec::new();
|
||||
for frame in self
|
||||
|
||||
@@ -7,10 +7,11 @@ use winapi::{
|
||||
shared::{
|
||||
dxgi::*,
|
||||
dxgi1_2::*,
|
||||
dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
dxgitype::*,
|
||||
minwindef::{DWORD, FALSE, TRUE, UINT},
|
||||
ntdef::LONG,
|
||||
windef::HMONITOR,
|
||||
windef::{HMONITOR, RECT},
|
||||
winerror::*,
|
||||
// dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
|
||||
},
|
||||
@@ -57,6 +58,7 @@ pub struct Capturer {
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
output_texture: bool,
|
||||
adapter_desc1: DXGI_ADAPTER_DESC1,
|
||||
rotate: Rotate,
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
@@ -151,6 +153,7 @@ impl Capturer {
|
||||
(*duplication).GetDesc(&mut desc);
|
||||
}
|
||||
}
|
||||
let rotate = Self::create_rotations(device.0, context.0, &display);
|
||||
|
||||
Ok(Capturer {
|
||||
device,
|
||||
@@ -168,9 +171,143 @@ impl Capturer {
|
||||
saved_raw_data: Vec::new(),
|
||||
output_texture: false,
|
||||
adapter_desc1,
|
||||
rotate,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_rotations(
|
||||
device: *mut ID3D11Device,
|
||||
context: *mut ID3D11DeviceContext,
|
||||
display: &Display,
|
||||
) -> Rotate {
|
||||
let mut video_context: *mut ID3D11VideoContext = ptr::null_mut();
|
||||
let mut video_device: *mut ID3D11VideoDevice = ptr::null_mut();
|
||||
let mut video_processor_enum: *mut ID3D11VideoProcessorEnumerator = ptr::null_mut();
|
||||
let mut video_processor: *mut ID3D11VideoProcessor = ptr::null_mut();
|
||||
let processor_rotation = match display.rotation() {
|
||||
DXGI_MODE_ROTATION_ROTATE90 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_90),
|
||||
DXGI_MODE_ROTATION_ROTATE180 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_180),
|
||||
DXGI_MODE_ROTATION_ROTATE270 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_270),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(processor_rotation) = processor_rotation {
|
||||
println!("create rotations");
|
||||
if !device.is_null() && !context.is_null() {
|
||||
unsafe {
|
||||
(*context).QueryInterface(
|
||||
&IID_ID3D11VideoContext,
|
||||
&mut video_context as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
if !video_context.is_null() {
|
||||
(*device).QueryInterface(
|
||||
&IID_ID3D11VideoDevice,
|
||||
&mut video_device as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
if !video_device.is_null() {
|
||||
let (input_width, input_height) = match display.rotation() {
|
||||
DXGI_MODE_ROTATION_ROTATE90 | DXGI_MODE_ROTATION_ROTATE270 => {
|
||||
(display.height(), display.width())
|
||||
}
|
||||
_ => (display.width(), display.height()),
|
||||
};
|
||||
let (output_width, output_height) = (display.width(), display.height());
|
||||
let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
|
||||
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
||||
InputFrameRate: DXGI_RATIONAL {
|
||||
Numerator: 30,
|
||||
Denominator: 1,
|
||||
},
|
||||
InputWidth: input_width as _,
|
||||
InputHeight: input_height as _,
|
||||
OutputFrameRate: DXGI_RATIONAL {
|
||||
Numerator: 30,
|
||||
Denominator: 1,
|
||||
},
|
||||
OutputWidth: output_width as _,
|
||||
OutputHeight: output_height as _,
|
||||
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
||||
};
|
||||
(*video_device).CreateVideoProcessorEnumerator(
|
||||
&content_desc,
|
||||
&mut video_processor_enum,
|
||||
);
|
||||
if !video_processor_enum.is_null() {
|
||||
let mut caps: D3D11_VIDEO_PROCESSOR_CAPS = mem::zeroed();
|
||||
if S_OK == (*video_processor_enum).GetVideoProcessorCaps(&mut caps)
|
||||
{
|
||||
if caps.FeatureCaps
|
||||
& D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_ROTATION
|
||||
!= 0
|
||||
{
|
||||
(*video_device).CreateVideoProcessor(
|
||||
video_processor_enum,
|
||||
0,
|
||||
&mut video_processor,
|
||||
);
|
||||
if !video_processor.is_null() {
|
||||
(*video_context).VideoProcessorSetStreamRotation(
|
||||
video_processor,
|
||||
0,
|
||||
TRUE,
|
||||
processor_rotation,
|
||||
);
|
||||
(*video_context)
|
||||
.VideoProcessorSetStreamAutoProcessingMode(
|
||||
video_processor,
|
||||
0,
|
||||
FALSE,
|
||||
);
|
||||
(*video_context).VideoProcessorSetStreamFrameFormat(
|
||||
video_processor,
|
||||
0,
|
||||
D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
||||
);
|
||||
(*video_context).VideoProcessorSetStreamSourceRect(
|
||||
video_processor,
|
||||
0,
|
||||
TRUE,
|
||||
&RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: input_width as _,
|
||||
bottom: input_height as _,
|
||||
},
|
||||
);
|
||||
(*video_context).VideoProcessorSetStreamDestRect(
|
||||
video_processor,
|
||||
0,
|
||||
TRUE,
|
||||
&RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: output_width as _,
|
||||
bottom: output_height as _,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let video_context = ComPtr(video_context);
|
||||
let video_device = ComPtr(video_device);
|
||||
let video_processor_enum = ComPtr(video_processor_enum);
|
||||
let video_processor = ComPtr(video_processor);
|
||||
let rotated_texture = ComPtr(ptr::null_mut());
|
||||
Rotate {
|
||||
video_context,
|
||||
video_device,
|
||||
video_processor_enum,
|
||||
video_processor,
|
||||
texture: (rotated_texture, false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_gdi(&self) -> bool {
|
||||
self.gdi_capturer.is_some()
|
||||
}
|
||||
@@ -328,7 +465,7 @@ impl Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_texture(&mut self, timeout: UINT) -> io::Result<*mut c_void> {
|
||||
fn get_texture(&mut self, timeout: UINT) -> io::Result<(*mut c_void, usize)> {
|
||||
unsafe {
|
||||
if self.duplication.0.is_null() {
|
||||
return Err(std::io::ErrorKind::AddrNotAvailable.into());
|
||||
@@ -352,7 +489,86 @@ impl Capturer {
|
||||
);
|
||||
let texture = ComPtr(texture);
|
||||
self.texture = texture;
|
||||
Ok(self.texture.0 as *mut c_void)
|
||||
|
||||
let mut final_texture = self.texture.0 as *mut c_void;
|
||||
let mut rotation = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||
_ => 0,
|
||||
};
|
||||
if rotation != 0
|
||||
&& !self.texture.is_null()
|
||||
&& !self.rotate.video_context.is_null()
|
||||
&& !self.rotate.video_device.is_null()
|
||||
&& !self.rotate.video_processor_enum.is_null()
|
||||
&& !self.rotate.video_processor.is_null()
|
||||
{
|
||||
let mut desc: D3D11_TEXTURE2D_DESC = mem::zeroed();
|
||||
(*self.texture.0).GetDesc(&mut desc);
|
||||
if rotation == 90 || rotation == 270 {
|
||||
let tmp = desc.Width;
|
||||
desc.Width = desc.Height;
|
||||
desc.Height = tmp;
|
||||
}
|
||||
if !self.rotate.texture.1 {
|
||||
self.rotate.texture.1 = true;
|
||||
let mut rotated_texture: *mut ID3D11Texture2D = ptr::null_mut();
|
||||
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
|
||||
(*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut rotated_texture);
|
||||
self.rotate.texture.0 = ComPtr(rotated_texture);
|
||||
}
|
||||
if !self.rotate.texture.0.is_null()
|
||||
&& desc.Width == self.width as u32
|
||||
&& desc.Height == self.height as u32
|
||||
{
|
||||
let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
|
||||
FourCC: 0,
|
||||
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
|
||||
Texture2D: D3D11_TEX2D_VPIV {
|
||||
ArraySlice: 0,
|
||||
MipSlice: 0,
|
||||
},
|
||||
};
|
||||
let mut input_view = ptr::null_mut();
|
||||
(*self.rotate.video_device.0).CreateVideoProcessorInputView(
|
||||
self.texture.0 as *mut _,
|
||||
self.rotate.video_processor_enum.0 as *mut _,
|
||||
&input_view_desc,
|
||||
&mut input_view,
|
||||
);
|
||||
if !input_view.is_null() {
|
||||
let input_view = ComPtr(input_view);
|
||||
let mut output_view_desc: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC =
|
||||
mem::zeroed();
|
||||
output_view_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D;
|
||||
output_view_desc.u.Texture2D_mut().MipSlice = 0;
|
||||
let mut output_view = ptr::null_mut();
|
||||
(*self.rotate.video_device.0).CreateVideoProcessorOutputView(
|
||||
self.rotate.texture.0 .0 as *mut _,
|
||||
self.rotate.video_processor_enum.0 as *mut _,
|
||||
&output_view_desc,
|
||||
&mut output_view,
|
||||
);
|
||||
if !output_view.is_null() {
|
||||
let output_view = ComPtr(output_view);
|
||||
let mut stream_data: D3D11_VIDEO_PROCESSOR_STREAM = mem::zeroed();
|
||||
stream_data.Enable = TRUE;
|
||||
stream_data.pInputSurface = input_view.0;
|
||||
(*self.rotate.video_context.0).VideoProcessorBlt(
|
||||
self.rotate.video_processor.0,
|
||||
output_view.0,
|
||||
0,
|
||||
1,
|
||||
&stream_data,
|
||||
);
|
||||
final_texture = self.rotate.texture.0 .0 as *mut c_void;
|
||||
rotation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((final_texture, rotation))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,3 +872,11 @@ fn wrap_hresult(x: HRESULT) -> io::Result<()> {
|
||||
})
|
||||
.into())
|
||||
}
|
||||
|
||||
struct Rotate {
|
||||
video_context: ComPtr<ID3D11VideoContext>,
|
||||
video_device: ComPtr<ID3D11VideoDevice>,
|
||||
video_processor_enum: ComPtr<ID3D11VideoProcessorEnumerator>,
|
||||
video_processor: ComPtr<ID3D11VideoProcessor>,
|
||||
texture: (ComPtr<ID3D11Texture2D>, bool),
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user