mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-18 14:39:08 +08:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a23822074e | ||
|
|
3d17bf4990 | ||
|
|
fd67be4a16 | ||
|
|
e6edf39305 | ||
|
|
34d2c62781 | ||
|
|
bd0a33e467 | ||
|
|
b8d36b6558 | ||
|
|
f38d89aaee | ||
|
|
773b9d6645 | ||
|
|
dea99ffb3a | ||
|
|
3251045e22 | ||
|
|
dc58c85e30 | ||
|
|
5a2a94d2cc | ||
|
|
f330953f4f | ||
|
|
8d4c86fe7f | ||
|
|
f8c2713c5b | ||
|
|
082a66b282 | ||
|
|
743b0ce8ce | ||
|
|
9d9b67aca5 | ||
|
|
d60b5a6ca0 | ||
|
|
e0ed6ee986 | ||
|
|
d3f0c80e94 | ||
|
|
b32ff87c6e | ||
|
|
b91b49229a | ||
|
|
afc8bb71dc | ||
|
|
4f86169f7f | ||
|
|
2cf43042e6 | ||
|
|
734fb8d6f7 | ||
|
|
3c7f6d3127 | ||
|
|
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 |
@@ -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) {
|
||||
11
.github/workflows/bridge.yml
vendored
11
.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
|
||||
|
||||
@@ -73,12 +73,13 @@ jobs:
|
||||
- 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 install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||
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
|
||||
|
||||
217
.github/workflows/flutter-build.yml
vendored
217
.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.2"
|
||||
NDK_VERSION: "r27b"
|
||||
VERSION: "1.3.4"
|
||||
NDK_VERSION: "r27c"
|
||||
#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,22 @@ 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
|
||||
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 +132,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:
|
||||
@@ -139,6 +156,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
@@ -299,6 +317,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
@@ -374,6 +393,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 +405,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 +465,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 +495,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:
|
||||
@@ -496,6 +521,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||
shell: bash
|
||||
|
||||
- name: Install Rust toolchain
|
||||
@@ -510,12 +536,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 +575,7 @@ jobs:
|
||||
#if: ${{ inputs.upload-artifact }}
|
||||
if: false
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
needs: [generate-bridge]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
@@ -565,12 +591,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 +630,7 @@ jobs:
|
||||
build-for-macOS:
|
||||
name: ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: [generate-bridge]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -614,13 +640,15 @@ jobs:
|
||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
extra-build-args: "",
|
||||
arch: x86_64,
|
||||
vcpkg-triplet: x64-osx,
|
||||
}
|
||||
- {
|
||||
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,
|
||||
vcpkg-triplet: arm64-osx,
|
||||
}
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
@@ -668,7 +696,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,18 +710,23 @@ 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: |
|
||||
cd "$(dirname "$(which flutter)")"
|
||||
# https://github.com/flutter/flutter/issues/133533
|
||||
# https://github.com/flutter/flutter/issues/1.3.43
|
||||
sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||
|
||||
- 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 +734,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
|
||||
@@ -722,6 +760,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||
|
||||
- name: Show version information (Rust, cargo, Clang)
|
||||
shell: bash
|
||||
@@ -735,6 +774,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 +868,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:
|
||||
@@ -894,7 +937,6 @@ jobs:
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
@@ -903,7 +945,7 @@ jobs:
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-11-jdk-headless \
|
||||
openjdk-17-jdk-headless \
|
||||
pkg-config \
|
||||
tree \
|
||||
wget
|
||||
@@ -915,6 +957,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 +1022,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
|
||||
@@ -989,7 +1037,7 @@ jobs:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
|
||||
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-linux-android)
|
||||
./flutter/ndk_arm64.sh
|
||||
@@ -1022,9 +1070,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 +1117,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 +1136,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'
|
||||
@@ -1157,7 +1213,6 @@ jobs:
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
@@ -1166,7 +1221,7 @@ jobs:
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-11-jdk-headless \
|
||||
openjdk-17-jdk-headless \
|
||||
pkg-config \
|
||||
tree \
|
||||
wget
|
||||
@@ -1179,6 +1234,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 +1271,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 +1283,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 +1305,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 +1324,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 +1353,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 +1444,7 @@ jobs:
|
||||
- name: Install vcpkg dependencies
|
||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
sudo apt install -y libva-dev && apt show libva-dev
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet ${{ matrix.job.vcpkg-triplet }} \
|
||||
@@ -1389,6 +1458,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||
shell: bash
|
||||
|
||||
- name: Restore bridge files
|
||||
@@ -1433,7 +1503,6 @@ jobs:
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
@@ -1520,6 +1589,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
|
||||
@@ -1693,7 +1775,6 @@ jobs:
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
@@ -1781,6 +1862,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||
# build rustdesk
|
||||
python3 ./res/inline-sciter.py
|
||||
export CARGO_INCREMENTAL=0
|
||||
@@ -1989,7 +2071,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
|
||||
|
||||
8
.github/workflows/playground.yml
vendored
8
.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.2"
|
||||
VERSION: "1.3.4"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked
|
||||
# below works for mac to make buildable on 3.13.9
|
||||
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
|
||||
pushd flutter && flutter pub get && popd
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
- 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"
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
|
||||
pushd flutter ; flutter pub get ; popd
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
|
||||
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-linux-android)
|
||||
./flutter/ndk_arm64.sh
|
||||
|
||||
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]]
|
||||
@@ -3050,8 +3064,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#835e599ed229e4e01b6fa3566e02ea45c73e2e9c"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -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#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#01ac3ec8009f04f7615842b9152338844b806184"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -5480,7 +5494,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.3.2"
|
||||
version = "1.3.4"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5580,7 +5594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.2"
|
||||
version = "1.3.4"
|
||||
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.2"
|
||||
version = "1.3.4"
|
||||
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.2
|
||||
version: 1.3.4
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -47,9 +47,9 @@ AppDir:
|
||||
- libasound2
|
||||
- libsystemd0
|
||||
- curl
|
||||
- libva2
|
||||
- libva-drm2
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.2
|
||||
version: 1.3.4
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -50,9 +50,9 @@ AppDir:
|
||||
- libasound2
|
||||
- libsystemd0
|
||||
- curl
|
||||
- libva2
|
||||
- libva-drm2
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
|
||||
13
build.py
13
build.py
@@ -111,7 +111,7 @@ def make_parser():
|
||||
'--hwcodec',
|
||||
action='store_true',
|
||||
help='Enable feature hwcodec' + (
|
||||
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
||||
'' if windows or osx else ', need libva-dev.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--vram',
|
||||
@@ -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
|
||||
|
||||
@@ -289,7 +298,7 @@ Version: %s
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
Homepage: https://rustdesk.com
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Description: A remote control software.
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -135,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器
|
||||
```
|
||||
在Dockerfile的RUN apt update之前插入两行:
|
||||
|
||||
RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list
|
||||
RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list
|
||||
RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \
|
||||
sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list
|
||||
```
|
||||
|
||||
2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码:
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<!-- https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="RustDesk"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -68,6 +68,7 @@ function build {
|
||||
pushd "$SCRIPTDIR/.."
|
||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
||||
popd
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
|
||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
||||
|
||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
#
|
||||
# Script to build F-Droid release of RustDesk
|
||||
#
|
||||
@@ -23,6 +21,43 @@ set -x
|
||||
# + build: perform actual build of APK file
|
||||
#
|
||||
|
||||
# Start of functions
|
||||
|
||||
# Install Flutter of version `VERSION` from Github repository
|
||||
# into directory `FLUTTER_DIR` and apply patches if needed
|
||||
|
||||
prepare_flutter() {
|
||||
VERSION="${1}"
|
||||
FLUTTER_DIR="${2}"
|
||||
|
||||
if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then
|
||||
git clone https://github.com/flutter/flutter "${FLUTTER_DIR}"
|
||||
fi
|
||||
|
||||
pushd "${FLUTTER_DIR}"
|
||||
|
||||
git restore .
|
||||
git checkout "${VERSION}"
|
||||
|
||||
# Patch flutter
|
||||
|
||||
if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then
|
||||
git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
|
||||
fi
|
||||
|
||||
flutter config --no-analytics
|
||||
|
||||
popd # ${FLUTTER_DIR}
|
||||
}
|
||||
|
||||
# Start of script
|
||||
|
||||
set -x
|
||||
|
||||
# Note current working directory as root dir for patches
|
||||
|
||||
ROOTDIR="${PWD}"
|
||||
|
||||
# Parse command-line arguments
|
||||
|
||||
VERNAME="${1}"
|
||||
@@ -101,18 +136,27 @@ prebuild)
|
||||
.env.CARGO_NDK_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
# Flutter used to compile main Rustdesk library
|
||||
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.ANDROID_FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
fi
|
||||
|
||||
# Flutter used to compile Flutter<->Rust bridge files
|
||||
|
||||
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 +171,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 +208,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
|
||||
@@ -205,14 +232,16 @@ prebuild)
|
||||
|
||||
cargo install \
|
||||
cargo-ndk \
|
||||
--version "${CARGO_NDK_VERSION}"
|
||||
--version "${CARGO_NDK_VERSION}" \
|
||||
--locked
|
||||
|
||||
# Install rust bridge generator
|
||||
|
||||
cargo install cargo-expand
|
||||
cargo install flutter_rust_bridge_codegen \
|
||||
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||
--features "uuid"
|
||||
--features "uuid" \
|
||||
--locked
|
||||
|
||||
# Populate native vcpkg dependencies
|
||||
|
||||
@@ -275,12 +304,66 @@ prebuild)
|
||||
|
||||
git apply res/fdroid/patches/*.patch
|
||||
|
||||
# If Flutter version used to generate bridge files differs from Flutter
|
||||
# version used to compile Rustdesk library, generate bridge using the
|
||||
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
|
||||
|
||||
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
|
||||
# Install Flutter bridge version
|
||||
|
||||
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
|
||||
|
||||
# Save changes
|
||||
|
||||
git add .
|
||||
|
||||
# Edit pubspec to make flutter bridge version work
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \
|
||||
flutter/pubspec.yaml
|
||||
|
||||
# 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
|
||||
|
||||
# Add bridge files to save-list
|
||||
|
||||
git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.*
|
||||
|
||||
# Restore everything
|
||||
|
||||
git checkout '*'
|
||||
git clean -dffx
|
||||
git reset
|
||||
fi
|
||||
|
||||
# Install Flutter version for RustDesk library build
|
||||
|
||||
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
|
||||
|
||||
# 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 these files now, but we still keep the following lines.
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/firebase_analytics/d' \
|
||||
@@ -296,34 +379,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/extended_text: .*/extended_text: 11.1.0/' \
|
||||
-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
|
||||
@@ -335,9 +390,12 @@ build)
|
||||
# '.github/workflows/flutter-build.yml'
|
||||
#
|
||||
|
||||
# Flutter used to compile main Rustdesk library
|
||||
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.ANDROID_FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
@@ -373,16 +431,11 @@ build)
|
||||
|
||||
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
|
||||
|
||||
# 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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:back_button_interceptor/back_button_interceptor.dart';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
@@ -64,6 +65,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;
|
||||
@@ -1611,12 +1615,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;
|
||||
@@ -1699,8 +1697,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:
|
||||
@@ -1818,7 +1818,7 @@ bool isPointInRect(Offset point, Rect rect) {
|
||||
}
|
||||
|
||||
/// return null means center
|
||||
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||
Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
double? left,
|
||||
double? top,
|
||||
double? width,
|
||||
@@ -1832,13 +1832,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);
|
||||
@@ -1872,7 +1868,7 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||
top < frameTop!) {
|
||||
return null;
|
||||
} else {
|
||||
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
||||
return Offset(left, top);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1932,47 +1928,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) {
|
||||
@@ -1981,20 +1953,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);
|
||||
}
|
||||
}
|
||||
@@ -2255,7 +2246,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";
|
||||
@@ -2264,7 +2258,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;
|
||||
}
|
||||
@@ -2274,7 +2268,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"];
|
||||
@@ -2508,9 +2502,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2603,7 +2608,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();
|
||||
@@ -2726,30 +2731,6 @@ Future<bool> osxRequestAudio() async {
|
||||
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
|
||||
}
|
||||
|
||||
class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
|
||||
/// Creates scroll physics that does not let the user scroll.
|
||||
const DraggableNeverScrollableScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldAcceptUserOffset(ScrollMetrics position) {
|
||||
// TODO: find a better solution to check if the offset change is caused by the scrollbar.
|
||||
// Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
|
||||
if (position is ScrollPositionWithSingleContext) {
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
return position.activity is IdleScrollActivity;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get allowImplicitScrolling => false;
|
||||
}
|
||||
|
||||
Widget futureBuilder(
|
||||
{required Future? future, required Widget Function(dynamic data) hasData}) {
|
||||
return FutureBuilder(
|
||||
@@ -3479,6 +3460,35 @@ Widget buildPresetPasswordWarning() {
|
||||
);
|
||||
}
|
||||
|
||||
bool get isLinuxMateDesktop =>
|
||||
isLinux &&
|
||||
(Platform.environment['XDG_CURRENT_DESKTOP']?.toLowerCase() == 'mate' ||
|
||||
Platform.environment['XDG_SESSION_DESKTOP']?.toLowerCase() == 'mate' ||
|
||||
Platform.environment['DESKTOP_SESSION']?.toLowerCase() == 'mate');
|
||||
|
||||
Map<String, dynamic>? _linuxOsDistro;
|
||||
|
||||
String getLinuxOsDistroId() {
|
||||
if (_linuxOsDistro == null) {
|
||||
String osInfo = bind.getOsDistroInfo();
|
||||
if (osInfo.isEmpty) {
|
||||
_linuxOsDistro = {};
|
||||
} else {
|
||||
try {
|
||||
_linuxOsDistro = jsonDecode(osInfo);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to parse os info: $e');
|
||||
// Don't call `bind.getOsDistroInfo()` again if failed to parse osInfo.
|
||||
_linuxOsDistro = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
return (_linuxOsDistro?['id'] ?? '') as String;
|
||||
}
|
||||
|
||||
bool get isLinuxMint =>
|
||||
getLinuxOsDistroId().toLowerCase().contains('linuxmint');
|
||||
|
||||
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
|
||||
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
||||
boxShadow() => isMainDesktopWindow
|
||||
@@ -3623,3 +3633,20 @@ List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
||||
void earlyAssert() {
|
||||
assert('\1' == '1');
|
||||
}
|
||||
|
||||
void checkUpdate() {
|
||||
if (isDesktop || isAndroid) {
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
(Map<String, dynamic> evt) async {
|
||||
if (evt['url'] is String) {
|
||||
stateGlobal.updateUrl.value = evt['url'];
|
||||
}
|
||||
});
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -316,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(
|
||||
@@ -669,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
|
||||
}
|
||||
@@ -741,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)
|
||||
@@ -758,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};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||
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';
|
||||
@@ -270,33 +270,24 @@ class _PeersViewState extends State<_PeersView>
|
||||
},
|
||||
)
|
||||
: peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false)
|
||||
.marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
? ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false).marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
},
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false);
|
||||
}),
|
||||
));
|
||||
: DynamicGridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false);
|
||||
}));
|
||||
|
||||
if (updateEvent == UpdateEvent.load) {
|
||||
_curPeers.clear();
|
||||
@@ -532,15 +523,22 @@ class AddressBookPeersView extends BasePeersView {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -169,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 =
|
||||
@@ -202,7 +209,7 @@ const double kMinFps = 5;
|
||||
const double kDefaultFps = 30;
|
||||
const double kMaxFps = 120;
|
||||
|
||||
const double kMinQuality = 5;
|
||||
const double kMinQuality = 10;
|
||||
const double kDefaultQuality = 50;
|
||||
const double kMaxQuality = 100;
|
||||
const double kMaxMoreQuality = 2000;
|
||||
@@ -237,10 +244,6 @@ const double kDesktopIconButtonSplashRadius = 20;
|
||||
/// [kMinCursorSize] indicates min cursor (w, h)
|
||||
const int kMinCursorSize = 12;
|
||||
|
||||
/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse
|
||||
const kDefaultScrollAmountMultiplier = 5.0;
|
||||
const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||
@@ -572,4 +575,4 @@ extension WindowsTargetExt on int {
|
||||
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
||||
}
|
||||
|
||||
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||
@@ -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() {
|
||||
@@ -330,7 +332,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: '',
|
||||
@@ -346,7 +348,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
@@ -357,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
|
||||
@@ -370,6 +371,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
@@ -430,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;
|
||||
|
||||
@@ -12,9 +12,9 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -40,7 +40,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
var updateUrl = '';
|
||||
var systemError = '';
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
var svcStopped = false.obs;
|
||||
@@ -87,7 +86,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
if (!isOutgoingOnly) buildIDBoard(context),
|
||||
if (!isOutgoingOnly) buildPasswordBoard(context),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildHelpCards(),
|
||||
future: Future.value(
|
||||
Obx(() => buildHelpCards(stateGlobal.updateUrl.value))),
|
||||
builder: (_, data) {
|
||||
if (data.hasData) {
|
||||
if (isIncomingOnly) {
|
||||
@@ -125,47 +125,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Container(
|
||||
width: isIncomingOnly ? 280.0 : 200.0,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: DesktopScrollWrapper(
|
||||
scrollController: _leftPaneScrollController,
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _leftPaneScrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
key: _childKey,
|
||||
children: children,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _leftPaneScrollController,
|
||||
child: Column(
|
||||
key: _childKey,
|
||||
children: children,
|
||||
),
|
||||
if (isOutgoingOnly)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: 12,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.settings,
|
||||
color: _editHover.value
|
||||
? textColor
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
if (isOutgoingOnly)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: 12,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.settings,
|
||||
color: _editHover.value
|
||||
? textColor
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
size: 22,
|
||||
),
|
||||
onTap: () => {
|
||||
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||
{
|
||||
DesktopSettingPage.switch2page(
|
||||
DesktopSettingPage.tabKeys[0])
|
||||
}
|
||||
},
|
||||
onHover: (value) => _editHover.value = value,
|
||||
),
|
||||
onTap: () => {
|
||||
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||
{
|
||||
DesktopSettingPage.switch2page(
|
||||
DesktopSettingPage.tabKeys[0])
|
||||
}
|
||||
},
|
||||
onHover: (value) => _editHover.value = value,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -272,10 +268,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 +311,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 +329,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(
|
||||
@@ -409,7 +416,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
Future<Widget> buildHelpCards() async {
|
||||
Widget buildHelpCards(String updateUrl) {
|
||||
if (!bind.isCustomClient() &&
|
||||
updateUrl.isNotEmpty &&
|
||||
!isCardClosed &&
|
||||
@@ -663,20 +670,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
@override
|
||||
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 {
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||
await gFFI.serverModel.fetchID();
|
||||
final error = await bind.mainGetError();
|
||||
|
||||
@@ -11,15 +11,16 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/plugin/manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
@@ -226,13 +227,11 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: DesktopScrollWrapper(
|
||||
scrollController: controller,
|
||||
child: PageView(
|
||||
controller: controller,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: _children(),
|
||||
)),
|
||||
child: PageView(
|
||||
controller: controller,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: _children(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -281,13 +280,10 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
|
||||
Widget _listView({required List<_TabInfo> tabs}) {
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
|
||||
));
|
||||
return ListView(
|
||||
controller: scrollController,
|
||||
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _listItem({required _TabInfo tab}) {
|
||||
@@ -349,22 +345,19 @@ class _GeneralState extends State<_General> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: [
|
||||
if (!isWeb) service(),
|
||||
theme(),
|
||||
_Card(title: 'Language', children: [language()]),
|
||||
if (!isWeb) hwcodec(),
|
||||
if (!isWeb) audio(context),
|
||||
if (!isWeb) record(context),
|
||||
if (!isWeb) WaylandCard(),
|
||||
other()
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin));
|
||||
return ListView(
|
||||
controller: scrollController,
|
||||
children: [
|
||||
if (!isWeb) service(),
|
||||
theme(),
|
||||
_Card(title: 'Language', children: [language()]),
|
||||
if (!isWeb) hwcodec(),
|
||||
if (!isWeb) audio(context),
|
||||
if (!isWeb) record(context),
|
||||
if (!isWeb) WaylandCard(),
|
||||
other()
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}
|
||||
|
||||
Widget theme() {
|
||||
@@ -705,29 +698,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
_lock(locked, 'Unlock Security Settings', () {
|
||||
locked = false;
|
||||
setState(() => {});
|
||||
}),
|
||||
AbsorbPointer(
|
||||
absorbing: locked,
|
||||
child: Column(children: [
|
||||
permissions(context),
|
||||
password(context),
|
||||
_Card(title: '2FA', children: [tfa()]),
|
||||
_Card(title: 'ID', children: [changeId()]),
|
||||
more(context),
|
||||
]),
|
||||
),
|
||||
],
|
||||
)).marginOnly(bottom: _kListViewBottomMargin));
|
||||
return SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
_lock(locked, 'Unlock Security Settings', () {
|
||||
locked = false;
|
||||
setState(() => {});
|
||||
}),
|
||||
AbsorbPointer(
|
||||
absorbing: locked,
|
||||
child: Column(children: [
|
||||
permissions(context),
|
||||
password(context),
|
||||
_Card(title: '2FA', children: [tfa()]),
|
||||
_Card(title: 'ID', children: [changeId()]),
|
||||
more(context),
|
||||
]),
|
||||
),
|
||||
],
|
||||
)).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}
|
||||
|
||||
Widget tfa() {
|
||||
@@ -1029,7 +1019,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);
|
||||
@@ -1372,112 +1364,82 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
bool get wantKeepAlive => true;
|
||||
bool locked = !isWeb && bind.mainIsInstalled();
|
||||
|
||||
final scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
bool enabled = !locked;
|
||||
final scrollController = ScrollController();
|
||||
final hideServer =
|
||||
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||
// TODO: support web proxy
|
||||
final hideProxy =
|
||||
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_lock(locked, 'Unlock Network Settings', () {
|
||||
locked = false;
|
||||
setState(() => {});
|
||||
}),
|
||||
AbsorbPointer(
|
||||
absorbing: locked,
|
||||
child: Column(children: [
|
||||
if (!hideServer) server(enabled),
|
||||
if (!hideProxy)
|
||||
_Card(title: 'Proxy', children: [
|
||||
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
||||
enabled: enabled),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||
return ListView(controller: scrollController, children: [
|
||||
_lock(locked, 'Unlock Network Settings', () {
|
||||
locked = false;
|
||||
setState(() => {});
|
||||
}),
|
||||
AbsorbPointer(
|
||||
absorbing: locked,
|
||||
child: Column(children: [
|
||||
network(context),
|
||||
]),
|
||||
),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}
|
||||
|
||||
server(bool enabled) {
|
||||
// Simple temp wrapper for PR check
|
||||
tmpWrapper() {
|
||||
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
|
||||
Map<String, dynamic> oldOptions = jsonDecode(bind.mainGetOptionsSync());
|
||||
old(String key) {
|
||||
return (oldOptions[key] ?? '').trim();
|
||||
}
|
||||
Widget network(BuildContext context) {
|
||||
final hideServer =
|
||||
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||
final hideProxy =
|
||||
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
|
||||
RxString idErrMsg = ''.obs;
|
||||
RxString relayErrMsg = ''.obs;
|
||||
RxString apiErrMsg = ''.obs;
|
||||
var idController =
|
||||
TextEditingController(text: old('custom-rendezvous-server'));
|
||||
var relayController = TextEditingController(text: old('relay-server'));
|
||||
var apiController = TextEditingController(text: old('api-server'));
|
||||
var keyController = TextEditingController(text: old('key'));
|
||||
final controllers = [
|
||||
idController,
|
||||
relayController,
|
||||
apiController,
|
||||
keyController,
|
||||
];
|
||||
final errMsgs = [
|
||||
idErrMsg,
|
||||
relayErrMsg,
|
||||
apiErrMsg,
|
||||
];
|
||||
|
||||
submit() async {
|
||||
bool result = await setServerConfig(
|
||||
null,
|
||||
errMsgs,
|
||||
ServerConfig(
|
||||
idServer: idController.text,
|
||||
relayServer: relayController.text,
|
||||
apiServer: apiController.text,
|
||||
key: keyController.text));
|
||||
if (result) {
|
||||
setState(() {});
|
||||
showToast(translate('Successful'));
|
||||
} else {
|
||||
showToast(translate('Failed'));
|
||||
}
|
||||
}
|
||||
|
||||
bool secure = !enabled;
|
||||
return _Card(
|
||||
title: 'ID/Relay Server',
|
||||
title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
||||
idErrMsg.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(
|
||||
context, 'Key', keyController, '', enabled, secure),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [_Button('Apply', submit, enabled: enabled)],
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
)
|
||||
]);
|
||||
if (hideServer && hideProxy) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
return tmpWrapper();
|
||||
return _Card(
|
||||
title: 'Network',
|
||||
children: [
|
||||
Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!hideServer)
|
||||
ListTile(
|
||||
leading: Icon(Icons.dns_outlined, color: _accentColor),
|
||||
title: Text(
|
||||
translate('ID/Relay Server'),
|
||||
style: TextStyle(fontSize: _kContentFontSize),
|
||||
),
|
||||
enabled: !locked,
|
||||
onTap: () => showServerSettings(gFFI.dialogManager),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
),
|
||||
if (!hideServer && !hideProxy)
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideProxy)
|
||||
ListTile(
|
||||
leading:
|
||||
Icon(Icons.network_ping_outlined, color: _accentColor),
|
||||
title: Text(
|
||||
translate('Socks5/Http(s) Proxy'),
|
||||
style: TextStyle(fontSize: _kContentFontSize),
|
||||
),
|
||||
enabled: !locked,
|
||||
onTap: changeSocks5Proxy,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1492,19 +1454,14 @@ class _DisplayState extends State<_Display> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
viewStyle(context),
|
||||
scrollStyle(context),
|
||||
imageQuality(context),
|
||||
codec(context),
|
||||
if (!isWeb) privacyModeImpl(context),
|
||||
other(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||
return ListView(controller: scrollController, children: [
|
||||
viewStyle(context),
|
||||
scrollStyle(context),
|
||||
imageQuality(context),
|
||||
codec(context),
|
||||
if (!isWeb) privacyModeImpl(context),
|
||||
other(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}
|
||||
|
||||
Widget viewStyle(BuildContext context) {
|
||||
@@ -1727,15 +1684,12 @@ class _AccountState extends State<_Account> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: [
|
||||
_Card(title: 'Account', children: [accountAction(), useInfo()]),
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin));
|
||||
return ListView(
|
||||
controller: scrollController,
|
||||
children: [
|
||||
_Card(title: 'Account', children: [accountAction(), useInfo()]),
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}
|
||||
|
||||
Widget accountAction() {
|
||||
@@ -1832,18 +1786,14 @@ class _PluginState extends State<_Plugin> {
|
||||
Widget build(BuildContext context) {
|
||||
bind.pluginListReload();
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ChangeNotifierProvider.value(
|
||||
value: pluginManager,
|
||||
child: Consumer<PluginManager>(builder: (context, model, child) {
|
||||
return ListView(
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
|
||||
).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}),
|
||||
),
|
||||
return ChangeNotifierProvider.value(
|
||||
value: pluginManager,
|
||||
child: Consumer<PluginManager>(builder: (context, model, child) {
|
||||
return ListView(
|
||||
controller: scrollController,
|
||||
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
|
||||
).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1895,75 +1845,72 @@ class _AboutState extends State<_About> {
|
||||
final fingerprint = data['fingerprint'].toString();
|
||||
const linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
child: _Card(title: translate('About RustDesk'), children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Version')}: $version')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Build Date')}: $buildDate')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
if (!isWeb)
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com/privacy.html');
|
||||
},
|
||||
child: Text(
|
||||
translate('Privacy Statement'),
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com');
|
||||
},
|
||||
child: Text(
|
||||
translate('Website'),
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
Container(
|
||||
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||
child: SelectionArea(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
translate('Slogan_tip'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white),
|
||||
)
|
||||
],
|
||||
return SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: _Card(title: translate('About RustDesk'), children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Version')}: $version')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Build Date')}: $buildDate')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
if (!isWeb)
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com/privacy.html');
|
||||
},
|
||||
child: Text(
|
||||
translate('Privacy Statement'),
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com');
|
||||
},
|
||||
child: Text(
|
||||
translate('Website'),
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
Container(
|
||||
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||
child: SelectionArea(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
).marginSymmetric(vertical: 4.0)
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin)
|
||||
]),
|
||||
));
|
||||
Text(
|
||||
translate('Slogan_tip'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
).marginSymmetric(vertical: 4.0)
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin)
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2281,26 +2228,39 @@ _LabeledTextField(
|
||||
String errorText,
|
||||
bool enabled,
|
||||
bool secure) {
|
||||
return Row(
|
||||
return Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(150),
|
||||
1: FlexColumnWidth(),
|
||||
},
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
'${translate(label)}:',
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: disabledTextColor(context, enabled)),
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Text(
|
||||
'${translate(label)}:',
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: disabledTextColor(context, enabled),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
obscureText: secure,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
errorText: errorText.isNotEmpty ? errorText : null),
|
||||
errorText: errorText.isNotEmpty ? errorText : null,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: disabledTextColor(context, enabled),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 8);
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
@@ -742,12 +741,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
ScrollController horizontal,
|
||||
ScrollController vertical,
|
||||
) {
|
||||
final scrollConfig = CustomMouseWheelScrollConfig(
|
||||
scrollDuration: kDefaultScrollDuration,
|
||||
scrollCurve: Curves.linearToEaseOut,
|
||||
mouseWheelTurnsThrottleTimeMs:
|
||||
kDefaultMouseWheelThrottleDuration.inMilliseconds,
|
||||
scrollAmountMultiplier: kDefaultScrollAmountMultiplier);
|
||||
var widget = child;
|
||||
if (layoutSize.width < size.width) {
|
||||
widget = ScrollConfiguration(
|
||||
@@ -793,36 +786,26 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
);
|
||||
}
|
||||
if (layoutSize.width < size.width) {
|
||||
widget = ImprovedScrolling(
|
||||
scrollController: horizontal,
|
||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
||||
customMouseWheelScrollConfig: scrollConfig,
|
||||
child: RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: horizontal,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
notificationPredicate: layoutSize.height < size.height
|
||||
? (notification) => notification.depth == 1
|
||||
: defaultScrollNotificationPredicate,
|
||||
child: widget,
|
||||
),
|
||||
widget = RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: horizontal,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
notificationPredicate: layoutSize.height < size.height
|
||||
? (notification) => notification.depth == 1
|
||||
: defaultScrollNotificationPredicate,
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
if (layoutSize.height < size.height) {
|
||||
widget = ImprovedScrolling(
|
||||
scrollController: vertical,
|
||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
||||
customMouseWheelScrollConfig: scrollConfig,
|
||||
child: RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: vertical,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
child: widget,
|
||||
),
|
||||
widget = RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: vertical,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
|
||||
class DesktopScrollWrapper extends StatelessWidget {
|
||||
final ScrollController scrollController;
|
||||
final Widget child;
|
||||
const DesktopScrollWrapper(
|
||||
{Key? key, required this.scrollController, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImprovedScrolling(
|
||||
scrollController: scrollController,
|
||||
enableCustomMouseWheelScrolling: true,
|
||||
// enableKeyboardScrolling: true, // strange behavior on mac
|
||||
customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
|
||||
scrollDuration: kDefaultScrollDuration,
|
||||
scrollCurve: Curves.linearToEaseOut,
|
||||
mouseWheelTurnsThrottleTimeMs:
|
||||
kDefaultMouseWheelThrottleDuration.inMilliseconds,
|
||||
scrollAmountMultiplier: kDefaultScrollAmountMultiplier),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ Future<void> initEnv(String appType) async {
|
||||
void runMainApp(bool startService) async {
|
||||
// register uni links
|
||||
await initEnv(kAppTypeMain);
|
||||
checkUpdate();
|
||||
// trigger connection status updater
|
||||
await bind.mainCheckConnectStatus();
|
||||
if (startService) {
|
||||
@@ -156,6 +157,7 @@ void runMainApp(bool startService) async {
|
||||
|
||||
void runMobileApp() async {
|
||||
await initEnv(kAppTypeMain);
|
||||
checkUpdate();
|
||||
if (isAndroid) androidChannelInit();
|
||||
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||
draggablePositions.load();
|
||||
@@ -483,7 +485,16 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
child = keyListenerBuilder(context, child);
|
||||
}
|
||||
if (isLinux) {
|
||||
child = buildVirtualWindowFrame(context, child);
|
||||
// `(!(isLinuxMateDesktop || isLinuxMint))` is not used here for clarity.
|
||||
// `isLinuxMint` will call ffi function.
|
||||
if (!isLinuxMateDesktop) {
|
||||
if (!isLinuxMint) {
|
||||
debugPrint(
|
||||
'Linux distro is not linuxmint, and desktop is not mate, '
|
||||
'so we build virtual window frame.');
|
||||
child = buildVirtualWindowFrame(context, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return child;
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ 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:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -40,14 +41,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
final _idController = IDTextEditingController();
|
||||
final RxBool _idEmpty = true.obs;
|
||||
|
||||
/// Update url. If it's not null, means an update is available.
|
||||
var _updateUrl = '';
|
||||
List<Peer> peers = [];
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157244
|
||||
Iterable<Peer> _autocompleteOpts = [];
|
||||
|
||||
_ConnectionPageState() {
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
_idController.addListener(() {
|
||||
@@ -69,22 +71,6 @@ 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 {
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -94,7 +80,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (!bind.isCustomClient()) _buildUpdateUI(),
|
||||
if (!bind.isCustomClient())
|
||||
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
|
||||
_buildRemoteIDTextField(),
|
||||
])),
|
||||
SliverFillRemaining(
|
||||
@@ -113,13 +100,21 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
|
||||
/// UI for software update.
|
||||
/// If [_updateUrl] is not empty, shows a button to update the software.
|
||||
Widget _buildUpdateUI() {
|
||||
return _updateUrl.isEmpty
|
||||
/// If _updateUrl] is not empty, shows a button to update the software.
|
||||
Widget _buildUpdateUI(String updateUrl) {
|
||||
return updateUrl.isEmpty
|
||||
? const SizedBox(height: 0)
|
||||
: InkWell(
|
||||
onTap: () async {
|
||||
final url = 'https://rustdesk.com/download';
|
||||
// https://pub.dev/packages/url_launcher#configuration
|
||||
// https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs
|
||||
//
|
||||
// `await launchUrl(Uri.parse(url))` can also run if skip
|
||||
// 1. The following check
|
||||
// 2. `<action android:name="android.support.customtabs.action.CustomTabsService" />` in AndroidManifest.xml
|
||||
//
|
||||
// But it is better to add the check.
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
}
|
||||
@@ -166,7 +161,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: '',
|
||||
@@ -182,7 +177,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
@@ -194,7 +189,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers
|
||||
_autocompleteOpts = peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
@@ -206,6 +201,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
fieldViewBuilder: (BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
@@ -274,6 +270,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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -57,9 +74,6 @@ class _RemotePageState extends State<RemotePage> {
|
||||
|
||||
final TextEditingController _textController =
|
||||
TextEditingController(text: initText);
|
||||
// This timer is used to check the composing status of the soft keyboard.
|
||||
// It is used for Android, Korean(and other similar) input method.
|
||||
Timer? _composingTimer;
|
||||
|
||||
_RemotePageState(String id) {
|
||||
initSharedStates(id);
|
||||
@@ -98,11 +112,15 @@ class _RemotePageState extends State<RemotePage> {
|
||||
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);
|
||||
@@ -114,7 +132,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_physicalFocusNode.dispose();
|
||||
await gFFI.close();
|
||||
_timer?.cancel();
|
||||
_composingTimer?.cancel();
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
@@ -129,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.
|
||||
@@ -150,7 +188,6 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.ffiModel.pi.version.isNotEmpty) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
}
|
||||
_composingTimer?.cancel();
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
@@ -214,13 +251,6 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
|
||||
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
||||
_composingTimer?.cancel();
|
||||
if (_textController.value.isComposingRangeValid) {
|
||||
_composingTimer = Timer(Duration(milliseconds: 25), () {
|
||||
_handleNonIOSSoftKeyboardInput(_textController.value.text);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var oldValue = _value;
|
||||
_value = newValue;
|
||||
if (oldValue.isNotEmpty &&
|
||||
@@ -261,14 +291,10 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSoftKeyboardInput(String newValue) async {
|
||||
// handle mobile virtual keyboard
|
||||
void handleSoftKeyboardInput(String newValue) {
|
||||
if (isIOS) {
|
||||
// fix: TextFormField onChanged event triggered multiple times when Korean input
|
||||
// https://github.com/rustdesk/rustdesk/pull/9644
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
|
||||
if (newValue != _textController.text) return;
|
||||
_handleIOSSoftKeyboardInput(_textController.text);
|
||||
_handleIOSSoftKeyboardInput(newValue);
|
||||
} else {
|
||||
_handleNonIOSSoftKeyboardInput(newValue);
|
||||
}
|
||||
@@ -534,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,
|
||||
@@ -554,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,
|
||||
),
|
||||
),
|
||||
@@ -763,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();
|
||||
@@ -811,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,13 +864,16 @@ 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;
|
||||
|
||||
final pi = gFFI.ffiModel.pi;
|
||||
final isMac = pi.platform == kPeerPlatformMacOS;
|
||||
final isWin = pi.platform == kPeerPlatformWindows;
|
||||
final isLinux = pi.platform == kPeerPlatformLinux;
|
||||
final modifiers = <Widget>[
|
||||
wrap('Ctrl ', () {
|
||||
setState(() => inputModel.ctrl = !inputModel.ctrl);
|
||||
@@ -910,6 +954,28 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
wrap('PgDn', () {
|
||||
inputModel.inputKey('VK_NEXT');
|
||||
}),
|
||||
// to-do: support PrtScr on Mac
|
||||
if (isWin || isLinux)
|
||||
wrap('PrtScr', () {
|
||||
inputModel.inputKey('VK_SNAPSHOT');
|
||||
}),
|
||||
if (isWin || isLinux)
|
||||
wrap('ScrollLock', () {
|
||||
inputModel.inputKey('VK_SCROLL');
|
||||
}),
|
||||
if (isWin || isLinux)
|
||||
wrap('Pause', () {
|
||||
inputModel.inputKey('VK_PAUSE');
|
||||
}),
|
||||
if (isWin || isLinux)
|
||||
// Maybe it's better to call it "Menu"
|
||||
// https://en.wikipedia.org/wiki/Menu_key
|
||||
wrap('Menu', () {
|
||||
inputModel.inputKey('Apps');
|
||||
}),
|
||||
wrap('Enter', () {
|
||||
inputModel.inputKey('VK_ENTER');
|
||||
}),
|
||||
SizedBox(width: 9999),
|
||||
wrap('', () {
|
||||
inputModel.inputKey('VK_LEFT');
|
||||
@@ -960,11 +1026,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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -978,7 +1044,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;
|
||||
@@ -1010,11 +1075,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),
|
||||
);
|
||||
}
|
||||
@@ -1217,7 +1283,9 @@ void showOptions(
|
||||
toggles +
|
||||
[privacyModeWidget]),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||
_disableAndroidSoftKeyboard();
|
||||
});
|
||||
}
|
||||
|
||||
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||
@@ -1236,7 +1304,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||
_disableAndroidSoftKeyboard();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1278,7 +1348,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||
_disableAndroidSoftKeyboard();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
@@ -70,6 +71,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
false; //androidVersion >= 26; // remove because not work on every device
|
||||
var _ignoreBatteryOpt = false;
|
||||
var _enableStartOnBoot = false;
|
||||
var _checkUpdateOnStartup = false;
|
||||
var _floatingWindowDisabled = false;
|
||||
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
||||
var _enableAbr = false;
|
||||
@@ -154,6 +156,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableStartOnBoot = enableStartOnBoot;
|
||||
}
|
||||
|
||||
var checkUpdateOnStartup =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableCheckUpdate);
|
||||
if (checkUpdateOnStartup != _checkUpdateOnStartup) {
|
||||
update = true;
|
||||
_checkUpdateOnStartup = checkUpdateOnStartup;
|
||||
}
|
||||
|
||||
var floatingWindowDisabled =
|
||||
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||
@@ -552,6 +561,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||
}));
|
||||
|
||||
if (!bind.isCustomClient()) {
|
||||
enhancementsTiles.add(
|
||||
SettingsTile.switchTile(
|
||||
initialValue: _checkUpdateOnStartup,
|
||||
title:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(translate('Check for software update on startup')),
|
||||
]),
|
||||
onToggle: (bool toValue) async {
|
||||
await mainSetLocalBoolOption(kOptionEnableCheckUpdate, toValue);
|
||||
setState(() => _checkUpdateOnStartup = toValue);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onFloatingWindowChanged(bool toValue) async {
|
||||
if (toValue) {
|
||||
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||
@@ -828,11 +853,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
|
||||
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||
}
|
||||
|
||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||
try {
|
||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
@@ -146,6 +147,16 @@ void setTemporaryPasswordLengthDialog(
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> options = {};
|
||||
try {
|
||||
options = jsonDecode(await bind.mainGetOptions());
|
||||
} catch (e) {
|
||||
print("Invalid server config: $e");
|
||||
}
|
||||
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||
}
|
||||
|
||||
void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||
var isInProgress = false;
|
||||
@@ -184,6 +195,43 @@ void showServerSettingsWithValue(
|
||||
return ret;
|
||||
}
|
||||
|
||||
Widget buildField(
|
||||
String label, TextEditingController controller, String errorMsg,
|
||||
{String? Function(String?)? validator, bool autofocus = false}) {
|
||||
if (isDesktop || isWeb) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(label),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
),
|
||||
validator: validator,
|
||||
autofocus: autofocus,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||
),
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
@@ -191,56 +239,45 @@ void showServerSettingsWithValue(
|
||||
...ServerConfigImportExportWidgets(controllers, errMsgs),
|
||||
],
|
||||
),
|
||||
content: Form(
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Form(
|
||||
child: Obx(() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: idCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('ID Server'),
|
||||
errorText: idServerMsg.value.isEmpty
|
||||
? null
|
||||
: idServerMsg.value),
|
||||
)
|
||||
] +
|
||||
[
|
||||
if (isAndroid)
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg.value.isEmpty
|
||||
? null
|
||||
: relayServerMsg.value),
|
||||
)
|
||||
] +
|
||||
[
|
||||
TextFormField(
|
||||
controller: apiCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('API Server'),
|
||||
),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (v) {
|
||||
if (v != null && v.isNotEmpty) {
|
||||
if (!(v.startsWith('http://') ||
|
||||
v.startsWith("https://"))) {
|
||||
return translate("invalid_http");
|
||||
}
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildField(translate('ID Server'), idCtrl, idServerMsg.value,
|
||||
autofocus: true),
|
||||
SizedBox(height: 8),
|
||||
if (!isIOS && !isWeb) ...[
|
||||
buildField(translate('Relay Server'), relayCtrl,
|
||||
relayServerMsg.value),
|
||||
SizedBox(height: 8),
|
||||
],
|
||||
buildField(
|
||||
translate('API Server'),
|
||||
apiCtrl,
|
||||
apiServerMsg.value,
|
||||
validator: (v) {
|
||||
if (v != null && v.isNotEmpty) {
|
||||
if (!(v.startsWith('http://') ||
|
||||
v.startsWith("https://"))) {
|
||||
return translate("invalid_http");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
buildField('Key', keyCtrl, ''),
|
||||
if (isInProgress)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: keyCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Key',
|
||||
),
|
||||
),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) const LinearProgressIndicator(),
|
||||
]))),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: () {
|
||||
close();
|
||||
|
||||
@@ -33,6 +33,8 @@ bool filterAbTagByIntersection() {
|
||||
const _personalAddressBookName = "My address book";
|
||||
const _legacyAddressBookName = "Legacy address book";
|
||||
|
||||
const kUntagged = "Untagged";
|
||||
|
||||
enum ForcePullAb {
|
||||
listAndCurrent,
|
||||
current,
|
||||
@@ -424,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();
|
||||
@@ -645,6 +648,9 @@ class AbModel {
|
||||
}
|
||||
|
||||
Color getCurrentAbTagColor(String tag) {
|
||||
if (tag == kUntagged) {
|
||||
return MyTheme.accent;
|
||||
}
|
||||
int? colorValue = current.tagColors[tag];
|
||||
if (colorValue != null) {
|
||||
return Color(colorValue);
|
||||
|
||||
@@ -100,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));
|
||||
@@ -257,6 +261,27 @@ class FileModel {
|
||||
debugPrint("Failed to decode onSelectedFiles: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void sendEmptyDirs(dynamic obj) {
|
||||
late final List<dynamic> emptyDirs;
|
||||
try {
|
||||
emptyDirs = jsonDecode(obj['dirs'] as String);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to decode sendEmptyDirs: $e");
|
||||
}
|
||||
final otherSideData = remoteController.directoryData();
|
||||
final toPath = otherSideData.directory.path;
|
||||
final isPeerWindows = otherSideData.options.isWindows;
|
||||
|
||||
final isLocalWindows = isWindows || isWebOnWindows;
|
||||
for (var dir in emptyDirs) {
|
||||
if (isLocalWindows != isPeerWindows) {
|
||||
dir = PathUtil.convert(dir, isLocalWindows, isPeerWindows);
|
||||
}
|
||||
var peerPath = PathUtil.join(toPath, dir, isPeerWindows);
|
||||
remoteController.createDirWithRemote(peerPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryData {
|
||||
@@ -470,7 +495,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;
|
||||
@@ -496,6 +522,43 @@ class FileController {
|
||||
debugPrint(
|
||||
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
|
||||
}
|
||||
|
||||
if (isWeb ||
|
||||
(!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;
|
||||
@@ -689,12 +752,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 {
|
||||
@@ -1064,6 +1131,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;
|
||||
@@ -1071,6 +1139,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
|
||||
@@ -1104,6 +1190,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;
|
||||
@@ -1127,6 +1232,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 {
|
||||
@@ -1373,6 +1500,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);
|
||||
@@ -1383,6 +1528,12 @@ class PathUtil {
|
||||
return pathUtil.split(path);
|
||||
}
|
||||
|
||||
static String convert(String path, bool isMainWindows, bool isOtherWindows) {
|
||||
final mainPathUtil = isMainWindows ? windowsContext : posixContext;
|
||||
final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
|
||||
return otherPathUtil.joinAll(mainPathUtil.split(path));
|
||||
}
|
||||
|
||||
static String dirname(String path, bool isWindows) {
|
||||
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||
return pathUtil.dirname(path);
|
||||
|
||||
@@ -544,7 +544,40 @@ class InputModel {
|
||||
handleKeyDownEventModifiers(e);
|
||||
}
|
||||
|
||||
if (isMobile || (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 ?? '',
|
||||
@@ -735,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()})));
|
||||
@@ -771,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})));
|
||||
}
|
||||
@@ -797,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'})));
|
||||
}
|
||||
@@ -823,7 +856,7 @@ class InputModel {
|
||||
_stopFling = true;
|
||||
if (isViewOnly) return;
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent('touch', 'pan_start', e.position);
|
||||
handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,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,
|
||||
@@ -929,7 +962,7 @@ class InputModel {
|
||||
|
||||
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent('touch', 'pan_end', e.position);
|
||||
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1047,7 +1080,7 @@ class InputModel {
|
||||
onExit: true,
|
||||
);
|
||||
|
||||
int trySetNearestRange(int v, int min, int max, int n) {
|
||||
static double tryGetNearestRange(double v, double min, double max, double n) {
|
||||
if (v < min && v >= min - n) {
|
||||
v = min;
|
||||
}
|
||||
@@ -1087,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,
|
||||
@@ -1105,8 +1138,8 @@ class InputModel {
|
||||
return;
|
||||
}
|
||||
evtValue = {
|
||||
'x': pos.x,
|
||||
'y': pos.y,
|
||||
'x': pos.x.toInt(),
|
||||
'y': pos.y.toInt(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1148,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;
|
||||
@@ -1166,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();
|
||||
@@ -1188,8 +1221,8 @@ class InputModel {
|
||||
evt['x'] = '0';
|
||||
evt['y'] = '0';
|
||||
} else {
|
||||
evt['x'] = '${pos.x}';
|
||||
evt['y'] = '${pos.y}';
|
||||
evt['x'] = '${pos.x.toInt()}';
|
||||
evt['y'] = '${pos.y.toInt()}';
|
||||
}
|
||||
|
||||
Map<int, String> mapButtons = {
|
||||
@@ -1329,33 +1362,56 @@ class InputModel {
|
||||
y = pos.dy;
|
||||
}
|
||||
|
||||
var evtX = 0;
|
||||
var evtY = 0;
|
||||
try {
|
||||
evtX = x.round();
|
||||
evtY = y.round();
|
||||
} catch (e) {
|
||||
debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
|
||||
return null;
|
||||
}
|
||||
return InputModel.getPointInRemoteRect(
|
||||
true, peerPlatform, kind, evtType, x, y, rect,
|
||||
buttons: buttons);
|
||||
}
|
||||
|
||||
int minX = rect.left.toInt();
|
||||
static Point<double>? getPointInRemoteRect(
|
||||
bool isLocalDesktop,
|
||||
String? peerPlatform,
|
||||
String kind,
|
||||
String evtType,
|
||||
double evtX,
|
||||
double evtY,
|
||||
Rect rect,
|
||||
{int buttons = kPrimaryMouseButton}) {
|
||||
double minX = rect.left;
|
||||
// https://github.com/rustdesk/rustdesk/issues/6678
|
||||
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
|
||||
int maxX = (rect.left + rect.width).toInt() -
|
||||
double maxX = (rect.left + rect.width) -
|
||||
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
|
||||
int minY = rect.top.toInt();
|
||||
int maxY = (rect.top + rect.height).toInt() -
|
||||
double minY = rect.top;
|
||||
double maxY = (rect.top + rect.height) -
|
||||
(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);
|
||||
|
||||
@@ -6,8 +6,10 @@ 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';
|
||||
@@ -142,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;
|
||||
|
||||
@@ -306,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') {
|
||||
@@ -397,6 +402,10 @@ class FfiModel with ChangeNotifier {
|
||||
if (isWeb) {
|
||||
parent.target?.fileModel.onSelectedFiles(evt);
|
||||
}
|
||||
} else if (name == "send_emptry_dirs") {
|
||||
if (isWeb) {
|
||||
parent.target?.fileModel.sendEmptyDirs(evt);
|
||||
}
|
||||
} else if (name == "record_status") {
|
||||
if (desktopType == DesktopType.remote || isMobile) {
|
||||
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
|
||||
@@ -1263,7 +1272,9 @@ class ImageModel with ChangeNotifier {
|
||||
rgba,
|
||||
rect?.width.toInt() ?? 0,
|
||||
rect?.height.toInt() ?? 0,
|
||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||
isWeb | isWindows | isLinux
|
||||
? ui.PixelFormat.rgba8888
|
||||
: ui.PixelFormat.bgra8888,
|
||||
);
|
||||
if (parent.target?.id != pid) return;
|
||||
await update(image);
|
||||
@@ -1274,13 +1285,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!);
|
||||
@@ -1292,20 +1296,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;
|
||||
@@ -1428,6 +1430,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();
|
||||
|
||||
@@ -1470,15 +1478,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;
|
||||
@@ -1507,16 +1529,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);
|
||||
@@ -1620,6 +1651,9 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
panX(double dx) {
|
||||
_x += dx;
|
||||
if (isMobile) {
|
||||
isMobileCanvasChanged = true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1627,17 +1661,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;
|
||||
@@ -1649,13 +1686,13 @@ 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();
|
||||
}
|
||||
|
||||
@@ -1666,10 +1703,7 @@ class CanvasModel with ChangeNotifier {
|
||||
if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / _devicePixelRatio;
|
||||
}
|
||||
final displayWidth = getDisplayWidth();
|
||||
final displayHeight = getDisplayHeight();
|
||||
_x = (size.width - displayWidth * _scale) / 2;
|
||||
_y = (size.height - displayHeight * _scale) / 2;
|
||||
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -1678,6 +1712,8 @@ class CanvasModel with ChangeNotifier {
|
||||
_x = 0;
|
||||
_y = 0;
|
||||
_scale = 1.0;
|
||||
_lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
_timerMobileFocusCanvasCursor?.cancel();
|
||||
}
|
||||
|
||||
updateScrollPercent() {
|
||||
@@ -1695,6 +1731,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
|
||||
@@ -1888,9 +1960,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;
|
||||
@@ -1900,7 +1976,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;
|
||||
@@ -1939,8 +2019,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;
|
||||
@@ -1949,22 +2031,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)) {
|
||||
@@ -1979,22 +2061,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();
|
||||
}
|
||||
|
||||
@@ -2006,9 +2129,9 @@ class CursorModel with ChangeNotifier {
|
||||
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;
|
||||
@@ -2066,13 +2189,34 @@ class CursorModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (dx == 0 && dy == 0) return;
|
||||
_x += dx;
|
||||
_y += dy;
|
||||
|
||||
Point<double>? 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,
|
||||
_y + dy,
|
||||
rect,
|
||||
buttons: kPrimaryButton);
|
||||
if (newPos == null) {
|
||||
return;
|
||||
}
|
||||
dx = newPos.x - _x;
|
||||
dy = newPos.y - _y;
|
||||
_x = newPos.x;
|
||||
_y = newPos.y;
|
||||
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);
|
||||
@@ -2085,7 +2229,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 &&
|
||||
@@ -2101,15 +2245,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();
|
||||
}
|
||||
@@ -2686,6 +2865,7 @@ class FFI {
|
||||
canvasModel.scale,
|
||||
ffiModel.pi.currentDisplay);
|
||||
}
|
||||
imageModel.callbacksOnFirstImage.clear();
|
||||
await imageModel.update(null);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -25,6 +25,8 @@ class StateGlobal {
|
||||
|
||||
final isPortrait = false.obs;
|
||||
|
||||
final updateUrl = ''.obs;
|
||||
|
||||
String _inputSource = '';
|
||||
|
||||
// Use for desktop -> remote toolbar -> resolution
|
||||
|
||||
@@ -1801,6 +1801,26 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("mainMaxEncryptLen");
|
||||
}
|
||||
|
||||
bool mainAudioSupportLoopback({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<String> sessionReadLocalEmptyDirsRecursiveSync(
|
||||
{required UuidValue sessionId,
|
||||
required String path,
|
||||
required bool includeHidden,
|
||||
dynamic hint}) {
|
||||
throw UnimplementedError("sessionReadLocalEmptyDirsRecursiveSync");
|
||||
}
|
||||
|
||||
Future<void> sessionReadRemoteEmptyDirsRecursiveSync(
|
||||
{required UuidValue sessionId,
|
||||
required String path,
|
||||
required bool includeHidden,
|
||||
dynamic hint}) {
|
||||
throw UnimplementedError("sessionReadRemoteEmptyDirsRecursiveSync");
|
||||
}
|
||||
|
||||
Future<void> sessionRenameFile(
|
||||
{required UuidValue sessionId,
|
||||
required int actId,
|
||||
@@ -1828,5 +1848,9 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("sessionGetConnToken");
|
||||
}
|
||||
|
||||
String getOsDistroInfo({dynamic hint}) {
|
||||
return '';
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
extern bool gIsConnectionManager;
|
||||
|
||||
GtkWidget *find_gl_area(GtkWidget *widget);
|
||||
void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
@@ -39,9 +42,10 @@ static void my_application_activate(GApplication* application) {
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
GdkScreen* screen = NULL;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
screen = gtk_window_get_screen(window);
|
||||
if (screen != NULL && GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
@@ -76,6 +80,8 @@ static void my_application_activate(GApplication* application) {
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
try_set_transparent(window, screen, view);
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
@@ -121,3 +127,106 @@ MyApplication* my_application_new() {
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
GtkWidget *find_gl_area(GtkWidget *widget)
|
||||
{
|
||||
if (GTK_IS_GL_AREA(widget)) {
|
||||
return widget;
|
||||
}
|
||||
|
||||
if (GTK_IS_CONTAINER(widget)) {
|
||||
GList *children = gtk_container_get_children(GTK_CONTAINER(widget));
|
||||
for (GList *iter = children; iter != NULL; iter = g_list_next(iter)) {
|
||||
GtkWidget *child = GTK_WIDGET(iter->data);
|
||||
GtkWidget *gl_area = find_gl_area(child);
|
||||
if (gl_area != NULL) {
|
||||
g_list_free(children);
|
||||
return gl_area;
|
||||
}
|
||||
}
|
||||
g_list_free(children);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool is_linux_mint()
|
||||
{
|
||||
bool is_mint = false;
|
||||
char line[256];
|
||||
FILE *fp = fopen("/etc/os-release", "r");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (strstr(line, "ID=linuxmint") != NULL) {
|
||||
is_mint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
return is_mint;
|
||||
}
|
||||
|
||||
bool is_desktop_mate()
|
||||
{
|
||||
const char* desktop = NULL;
|
||||
desktop = getenv("XDG_CURRENT_DESKTOP");
|
||||
printf("Linux desktop, XDG_CURRENT_DESKTOP: %s\n", desktop == NULL ? "" : desktop);
|
||||
if (desktop == NULL) {
|
||||
desktop = getenv("XDG_SESSION_DESKTOP");
|
||||
printf("Linux desktop, XDG_SESSION_DESKTOP: %s\n", desktop == NULL ? "" : desktop);
|
||||
}
|
||||
if (desktop == NULL) {
|
||||
desktop = getenv("DESKTOP_SESSION");
|
||||
printf("Linux desktop, DESKTOP_SESSION: %s\n", desktop == NULL ? "" : desktop);
|
||||
}
|
||||
if (desktop != NULL && strcasecmp(desktop, "mate") == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool skip_setting_transparent()
|
||||
{
|
||||
if (is_desktop_mate()) {
|
||||
printf("Linux desktop, MATE\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_linux_mint()) {
|
||||
printf("Linux desktop, Linux Mint\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://github.com/flutter/flutter/issues/152154
|
||||
// Remove this workaround when flutter version is updated.
|
||||
void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view)
|
||||
{
|
||||
GtkWidget *gl_area = NULL;
|
||||
|
||||
if (skip_setting_transparent()) {
|
||||
printf("Skip setting transparent\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("Try setting transparent\n");
|
||||
|
||||
gl_area = find_gl_area(GTK_WIDGET(view));
|
||||
if (gl_area != NULL) {
|
||||
gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE);
|
||||
}
|
||||
|
||||
if (screen != NULL) {
|
||||
GdkVisual *visual = NULL;
|
||||
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
|
||||
visual = gdk_screen_get_rgba_visual(screen);
|
||||
if (visual != NULL && gdk_screen_is_composited(screen)) {
|
||||
gtk_widget_set_visual(GTK_WIDGET(window), visual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: "519350f1f40746798299e94786197d058353bac9"
|
||||
resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033"
|
||||
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:
|
||||
@@ -530,15 +530,6 @@ packages:
|
||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
flutter_improved_scrolling:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6"
|
||||
url: "https://github.com/rustdesk-org/flutter_improved_scrolling"
|
||||
source: git
|
||||
version: "0.0.3"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -745,10 +736,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 +784,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 +856,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 +1028,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:
|
||||
@@ -1285,10 +1268,11 @@ packages:
|
||||
texture_rgba_renderer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: texture_rgba_renderer
|
||||
sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
||||
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
||||
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
||||
source: git
|
||||
version: "0.0.16"
|
||||
timing:
|
||||
dependency: transitive
|
||||
@@ -1325,10 +1309,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 +1543,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 +1568,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 +1614,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.2+51
|
||||
version: 1.3.4+53
|
||||
|
||||
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
|
||||
@@ -71,14 +71,11 @@ dependencies:
|
||||
debounce_throttle: ^2.0.0
|
||||
file_picker: ^5.1.0
|
||||
flutter_svg: ^2.0.5
|
||||
flutter_improved_scrolling:
|
||||
# currently, we use flutter 3.10.0+.
|
||||
#
|
||||
# for flutter 3.0.5, please use official version(just comment code below).
|
||||
# if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below).
|
||||
uni_links:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
||||
uni_links: ^0.5.1
|
||||
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
|
||||
@@ -87,7 +84,10 @@ dependencies:
|
||||
password_strength: ^0.2.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_keyboard_visibility: ^5.4.0
|
||||
texture_rgba_renderer: ^0.0.16
|
||||
texture_rgba_renderer:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
||||
ref: 42797e0f03141dc2b585f76c64a13974508058b4
|
||||
percent_indicator: ^4.2.2
|
||||
dropdown_button2: ^2.0.0
|
||||
flutter_gpu_texture_renderer:
|
||||
@@ -104,7 +104,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 +186,4 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
|
||||
@@ -345,7 +345,7 @@ fn convert_to_tfc_key(key: Key) -> Option<TFC_Key> {
|
||||
Key::Numpad9 => TFC_Key::N9,
|
||||
Key::Decimal => TFC_Key::NumpadDecimal,
|
||||
Key::Clear => TFC_Key::NumpadClear,
|
||||
Key::Pause => TFC_Key::PlayPause,
|
||||
Key::Pause => TFC_Key::Pause,
|
||||
Key::Print => TFC_Key::Print,
|
||||
Key::Snapshot => TFC_Key::PrintScreen,
|
||||
Key::Insert => TFC_Key::Insert,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1710,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 => {
|
||||
@@ -2225,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";
|
||||
@@ -2234,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";
|
||||
@@ -2370,6 +2375,7 @@ pub mod keys {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -13,22 +13,35 @@ pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
|
||||
|
||||
pub struct Distro {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
impl Distro {
|
||||
fn new() -> Self {
|
||||
// to-do:
|
||||
// 1. Remove `run_cmds`, read file once
|
||||
// 2. Add more distro infos
|
||||
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let id = run_cmds("awk -F'=' '/^ID=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
Self { name, version_id }
|
||||
Self {
|
||||
name,
|
||||
id,
|
||||
version_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.2"
|
||||
version = "1.3.4"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -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")]
|
||||
@@ -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 {
|
||||
@@ -982,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;
|
||||
}
|
||||
@@ -192,15 +193,11 @@ impl EncoderApi for HwRamEncoder {
|
||||
}
|
||||
|
||||
fn support_abr(&self) -> bool {
|
||||
["qsv", "vaapi", "mediacodec", "videotoolbox"]
|
||||
.iter()
|
||||
.all(|&x| !self.config.name.contains(x))
|
||||
["qsv", "vaapi"].iter().all(|&x| !self.config.name.contains(x))
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
["vaapi", "mediacodec", "videotoolbox"]
|
||||
.iter()
|
||||
.all(|&x| !self.config.name.contains(x))
|
||||
["vaapi"].iter().all(|&x| !self.config.name.contains(x))
|
||||
}
|
||||
|
||||
fn latency_free(&self) -> bool {
|
||||
|
||||
@@ -158,6 +158,7 @@ impl Recorder {
|
||||
#[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()));
|
||||
}
|
||||
@@ -194,33 +195,33 @@ impl Recorder {
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
for f in vp8s.frames.iter() {
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
for f in vp9s.frames.iter() {
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Av1s(av1s) => {
|
||||
for f in av1s.frames.iter() {
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
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) => {
|
||||
for f in h264s.frames.iter() {
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
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) => {
|
||||
for f in h265s.frames.iter() {
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.check_pts(f.pts, f.key, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
@@ -230,8 +231,18 @@ impl Recorder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> 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 {
|
||||
@@ -342,7 +353,7 @@ impl Drop for WebmRecorder {
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
struct HwRecorder {
|
||||
muxer: Muxer,
|
||||
muxer: Option<Muxer>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
written: bool,
|
||||
@@ -362,7 +373,7 @@ impl RecorderApi for HwRecorder {
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||
Ok(HwRecorder {
|
||||
muxer,
|
||||
muxer: Some(muxer),
|
||||
ctx,
|
||||
ctx2,
|
||||
written: false,
|
||||
@@ -376,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;
|
||||
}
|
||||
@@ -390,9 +405,11 @@ 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 {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -253,17 +390,7 @@ impl Capturer {
|
||||
|
||||
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
|
||||
if self.output_texture {
|
||||
let rotation = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0,
|
||||
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||
_ => {
|
||||
// Unsupported rotation, try anyway
|
||||
0
|
||||
}
|
||||
};
|
||||
Ok(Frame::Texture((self.get_texture(timeout)?, rotation)))
|
||||
Ok(Frame::Texture(self.get_texture(timeout)?))
|
||||
} else {
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
@@ -338,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());
|
||||
@@ -362,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,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),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.3.2
|
||||
pkgver=1.3.4
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
@@ -7,7 +7,7 @@ arch=('x86_64')
|
||||
url=""
|
||||
license=('AGPL-3.0')
|
||||
groups=()
|
||||
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libvdpau' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire')
|
||||
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire')
|
||||
makedepends=()
|
||||
checkdepends=()
|
||||
optdepends=()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.2
|
||||
Version: 1.3.4
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
URL: https://rustdesk.com
|
||||
Vendor: rustdesk <info@rustdesk.com>
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.2
|
||||
Version: 1.3.4
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base
|
||||
URL: https://rustdesk.com
|
||||
Vendor: rustdesk <info@rustdesk.com>
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-base
|
||||
Recommends: libayatana-appindicator-gtk3
|
||||
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Version: 1.1.9
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Recommends: libayatana-appindicator3-1
|
||||
|
||||
%description
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.2
|
||||
Version: 1.3.4
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base
|
||||
URL: https://rustdesk.com
|
||||
Vendor: rustdesk <info@rustdesk.com>
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva2 pam gstreamer1-plugins-base
|
||||
Recommends: libayatana-appindicator-gtk3
|
||||
|
||||
%description
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
From 7f12898fe8fd12c1042c98b34825ab2eda89e54d Mon Sep 17 00:00:00 2001
|
||||
From: 21pages <sunboeasy@gmail.com>
|
||||
Date: Sun, 24 Nov 2024 12:58:39 +0800
|
||||
Subject: [PATCH 1/2] videotoolbox changing bitrate
|
||||
|
||||
Signed-off-by: 21pages <sunboeasy@gmail.com>
|
||||
---
|
||||
libavcodec/videotoolboxenc.c | 39 ++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 39 insertions(+)
|
||||
|
||||
diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c
|
||||
index 5ea9afee22..89c927cdcc 100644
|
||||
--- a/libavcodec/videotoolboxenc.c
|
||||
+++ b/libavcodec/videotoolboxenc.c
|
||||
@@ -278,6 +278,8 @@ typedef struct VTEncContext {
|
||||
int max_slice_bytes;
|
||||
int power_efficient;
|
||||
int max_ref_frames;
|
||||
+
|
||||
+ int last_bit_rate;
|
||||
} VTEncContext;
|
||||
|
||||
static int vt_dump_encoder(AVCodecContext *avctx)
|
||||
@@ -1174,6 +1176,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx,
|
||||
int64_t one_second_value = 0;
|
||||
void *nums[2];
|
||||
|
||||
+ vtctx->last_bit_rate = bit_rate;
|
||||
int status = VTCompressionSessionCreate(kCFAllocatorDefault,
|
||||
avctx->width,
|
||||
avctx->height,
|
||||
@@ -2618,6 +2621,41 @@ static int vtenc_send_frame(AVCodecContext *avctx,
|
||||
return 0;
|
||||
}
|
||||
|
||||
+static void update_config(AVCodecContext *avctx)
|
||||
+{
|
||||
+ VTEncContext *vtctx = avctx->priv_data;
|
||||
+
|
||||
+ if (avctx->codec_id != AV_CODEC_ID_PRORES) {
|
||||
+ if (avctx->bit_rate != vtctx->last_bit_rate) {
|
||||
+ av_log(avctx, AV_LOG_INFO, "Setting bit rate to %d\n", avctx->bit_rate);
|
||||
+ vtctx->last_bit_rate = avctx->bit_rate;
|
||||
+ SInt32 bit_rate = avctx->bit_rate;
|
||||
+ CFNumberRef bit_rate_num = CFNumberCreate(kCFAllocatorDefault,
|
||||
+ kCFNumberSInt32Type,
|
||||
+ &bit_rate);
|
||||
+ if (!bit_rate_num) return;
|
||||
+
|
||||
+ if (vtctx->constant_bit_rate) {
|
||||
+ int status = VTSessionSetProperty(vtctx->session,
|
||||
+ compat_keys.kVTCompressionPropertyKey_ConstantBitRate,
|
||||
+ bit_rate_num);
|
||||
+ if (status == kVTPropertyNotSupportedErr) {
|
||||
+ av_log(avctx, AV_LOG_ERROR, "Error: -constant_bit_rate true is not supported by the encoder.\n");
|
||||
+ }
|
||||
+ } else {
|
||||
+ int status = VTSessionSetProperty(vtctx->session,
|
||||
+ kVTCompressionPropertyKey_AverageBitRate,
|
||||
+ bit_rate_num);
|
||||
+ if (!status) {
|
||||
+ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ CFRelease(bit_rate_num);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
static av_cold int vtenc_frame(
|
||||
AVCodecContext *avctx,
|
||||
AVPacket *pkt,
|
||||
@@ -2630,6 +2668,7 @@ static av_cold int vtenc_frame(
|
||||
CMSampleBufferRef buf = NULL;
|
||||
ExtraSEI *sei = NULL;
|
||||
|
||||
+ update_config(avctx);
|
||||
if (frame) {
|
||||
status = vtenc_send_frame(avctx, vtctx, frame);
|
||||
|
||||
--
|
||||
2.43.0.windows.1
|
||||
|
||||
246
res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch
Normal file
246
res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch
Normal file
@@ -0,0 +1,246 @@
|
||||
From ed73f8f6494d74ae47218f9503c7e3de385d9253 Mon Sep 17 00:00:00 2001
|
||||
From: 21pages <sunboeasy@gmail.com>
|
||||
Date: Sun, 24 Nov 2024 14:17:39 +0800
|
||||
Subject: [PATCH 1/2] mediacodec changing bitrate
|
||||
|
||||
Signed-off-by: 21pages <sunboeasy@gmail.com>
|
||||
---
|
||||
libavcodec/mediacodec_wrapper.c | 97 +++++++++++++++++++++++++++++++++
|
||||
libavcodec/mediacodec_wrapper.h | 7 +++
|
||||
libavcodec/mediacodecenc.c | 18 ++++++
|
||||
3 files changed, 122 insertions(+)
|
||||
|
||||
diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c
|
||||
index 306359071e..7edb38a7d7 100644
|
||||
--- a/libavcodec/mediacodec_wrapper.c
|
||||
+++ b/libavcodec/mediacodec_wrapper.c
|
||||
@@ -35,6 +35,8 @@
|
||||
#include "ffjni.h"
|
||||
#include "mediacodec_wrapper.h"
|
||||
|
||||
+#define PARAMETER_KEY_VIDEO_BITRATE "video-bitrate"
|
||||
+
|
||||
struct JNIAMediaCodecListFields {
|
||||
|
||||
jclass mediacodec_list_class;
|
||||
@@ -195,6 +197,8 @@ struct JNIAMediaCodecFields {
|
||||
jmethodID set_input_surface_id;
|
||||
jmethodID signal_end_of_input_stream_id;
|
||||
|
||||
+ jmethodID set_parameters_id;
|
||||
+
|
||||
jclass mediainfo_class;
|
||||
|
||||
jmethodID init_id;
|
||||
@@ -248,6 +252,8 @@ static const struct FFJniField jni_amediacodec_mapping[] = {
|
||||
{ "android/media/MediaCodec", "setInputSurface", "(Landroid/view/Surface;)V", FF_JNI_METHOD, OFFSET(set_input_surface_id), 0 },
|
||||
{ "android/media/MediaCodec", "signalEndOfInputStream", "()V", FF_JNI_METHOD, OFFSET(signal_end_of_input_stream_id), 0 },
|
||||
|
||||
+ { "android/media/MediaCodec", "setParameters", "(Landroid/os/Bundle;)V", FF_JNI_METHOD, OFFSET(set_parameters_id), 0 },
|
||||
+
|
||||
{ "android/media/MediaCodec$BufferInfo", NULL, NULL, FF_JNI_CLASS, OFFSET(mediainfo_class), 1 },
|
||||
|
||||
{ "android/media/MediaCodec.BufferInfo", "<init>", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 },
|
||||
@@ -292,6 +298,24 @@ typedef struct FFAMediaCodecJni {
|
||||
|
||||
static const FFAMediaCodec media_codec_jni;
|
||||
|
||||
+struct JNIABundleFields
|
||||
+{
|
||||
+ jclass bundle_class;
|
||||
+ jmethodID init_id;
|
||||
+ jmethodID put_int_id;
|
||||
+};
|
||||
+
|
||||
+#define OFFSET(x) offsetof(struct JNIABundleFields, x)
|
||||
+static const struct FFJniField jni_abundle_mapping[] = {
|
||||
+ { "android/os/Bundle", NULL, NULL, FF_JNI_CLASS, OFFSET(bundle_class), 1 },
|
||||
+
|
||||
+ { "android/os/Bundle", "<init>", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 },
|
||||
+ { "android/os/Bundle", "putInt", "(Ljava/lang/String;I)V", FF_JNI_METHOD, OFFSET(put_int_id), 1 },
|
||||
+
|
||||
+ { NULL }
|
||||
+};
|
||||
+#undef OFFSET
|
||||
+
|
||||
#define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \
|
||||
(env) = ff_jni_get_env(log_ctx); \
|
||||
if (!(env)) { \
|
||||
@@ -1761,6 +1785,69 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
+static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value)
|
||||
+{
|
||||
+ JNIEnv *env = NULL;
|
||||
+ struct JNIABundleFields jfields = { 0 };
|
||||
+ jobject object = NULL;
|
||||
+ jstring key = NULL;
|
||||
+ FFAMediaCodecJni *codec = (FFAMediaCodecJni *)ctx;
|
||||
+ void *log_ctx = codec;
|
||||
+ int ret = -1;
|
||||
+
|
||||
+ JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL);
|
||||
+
|
||||
+ if (ff_jni_init_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx) < 0) {
|
||||
+ av_log(log_ctx, AV_LOG_ERROR, "Failed to init jfields\n");
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
+ object = (*env)->NewObject(env, jfields.bundle_class, jfields.init_id);
|
||||
+ if (!object) {
|
||||
+ av_log(log_ctx, AV_LOG_ERROR, "Failed to create bundle object\n");
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
+ key = ff_jni_utf_chars_to_jstring(env, name, log_ctx);
|
||||
+ if (!key) {
|
||||
+ av_log(log_ctx, AV_LOG_ERROR, "Failed to convert key to jstring\n");
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
+ (*env)->CallVoidMethod(env, object, jfields.put_int_id, key, value);
|
||||
+ if (ff_jni_exception_check(env, 1, log_ctx) < 0) {
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
+ if (!codec->jfields.set_parameters_id) {
|
||||
+ av_log(log_ctx, AV_LOG_ERROR, "System doesn't support setParameters\n");
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
+ (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object);
|
||||
+ if (ff_jni_exception_check(env, 1, log_ctx) < 0) {
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
+ ret = 0;
|
||||
+
|
||||
+fail:
|
||||
+ if (key) {
|
||||
+ (*env)->DeleteLocalRef(env, key);
|
||||
+ }
|
||||
+ if (object) {
|
||||
+ (*env)->DeleteLocalRef(env, object);
|
||||
+ }
|
||||
+ ff_jni_reset_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static int mediacodec_jni_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate)
|
||||
+{
|
||||
+ return mediacodec_jni_setParameter(ctx, PARAMETER_KEY_VIDEO_BITRATE, bitrate);
|
||||
+}
|
||||
+
|
||||
static const FFAMediaFormat media_format_jni = {
|
||||
.class = &amediaformat_class,
|
||||
|
||||
@@ -1820,6 +1907,8 @@ static const FFAMediaCodec media_codec_jni = {
|
||||
.getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode,
|
||||
.cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers,
|
||||
.signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream,
|
||||
+
|
||||
+ .setDynamicBitrate = mediacodec_jni_setDynamicBitrate,
|
||||
};
|
||||
|
||||
typedef struct FFAMediaFormatNdk {
|
||||
@@ -2428,6 +2517,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
+static int mediacodec_ndk_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate)
|
||||
+{
|
||||
+ av_log(ctx, AV_LOG_ERROR, "ndk setDynamicBitrate unavailable\n");
|
||||
+ return -1;
|
||||
+}
|
||||
+
|
||||
static const FFAMediaFormat media_format_ndk = {
|
||||
.class = &amediaformat_ndk_class,
|
||||
|
||||
@@ -2489,6 +2584,8 @@ static const FFAMediaCodec media_codec_ndk = {
|
||||
.getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode,
|
||||
.cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers,
|
||||
.signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream,
|
||||
+
|
||||
+ .setDynamicBitrate = mediacodec_ndk_setDynamicBitrate,
|
||||
};
|
||||
|
||||
FFAMediaFormat *ff_AMediaFormat_new(int ndk)
|
||||
diff --git a/libavcodec/mediacodec_wrapper.h b/libavcodec/mediacodec_wrapper.h
|
||||
index 11a4260497..86c64556ad 100644
|
||||
--- a/libavcodec/mediacodec_wrapper.h
|
||||
+++ b/libavcodec/mediacodec_wrapper.h
|
||||
@@ -219,6 +219,8 @@ struct FFAMediaCodec {
|
||||
|
||||
// For encoder with FFANativeWindow as input.
|
||||
int (*signalEndOfInputStream)(FFAMediaCodec *);
|
||||
+
|
||||
+ int (*setDynamicBitrate)(FFAMediaCodec *codec, int bitrate);
|
||||
};
|
||||
|
||||
static inline char *ff_AMediaCodec_getName(FFAMediaCodec *codec)
|
||||
@@ -343,6 +345,11 @@ static inline int ff_AMediaCodec_signalEndOfInputStream(FFAMediaCodec *codec)
|
||||
return codec->signalEndOfInputStream(codec);
|
||||
}
|
||||
|
||||
+static inline int ff_AMediaCodec_setDynamicBitrate(FFAMediaCodec *codec, int bitrate)
|
||||
+{
|
||||
+ return codec->setDynamicBitrate(codec, bitrate);
|
||||
+}
|
||||
+
|
||||
int ff_Build_SDK_INT(AVCodecContext *avctx);
|
||||
|
||||
enum FFAMediaFormatColorRange {
|
||||
diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c
|
||||
index d3bf27cb7f..621529d686 100644
|
||||
--- a/libavcodec/mediacodecenc.c
|
||||
+++ b/libavcodec/mediacodecenc.c
|
||||
@@ -73,6 +73,8 @@ typedef struct MediaCodecEncContext {
|
||||
int bitrate_mode;
|
||||
int level;
|
||||
int pts_as_dts;
|
||||
+
|
||||
+ int last_bit_rate;
|
||||
} MediaCodecEncContext;
|
||||
|
||||
enum {
|
||||
@@ -155,6 +157,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx)
|
||||
int ret;
|
||||
int gop;
|
||||
|
||||
+ s->last_bit_rate = avctx->bit_rate;
|
||||
+
|
||||
if (s->use_ndk_codec < 0)
|
||||
s->use_ndk_codec = !av_jni_get_java_vm(avctx);
|
||||
|
||||
@@ -515,12 +519,26 @@ static int mediacodec_send(AVCodecContext *avctx,
|
||||
return 0;
|
||||
}
|
||||
|
||||
+static void update_config(AVCodecContext *avctx)
|
||||
+{
|
||||
+ MediaCodecEncContext *s = avctx->priv_data;
|
||||
+ if (avctx->bit_rate != s->last_bit_rate) {
|
||||
+ s->last_bit_rate = avctx->bit_rate;
|
||||
+ if (0 != ff_AMediaCodec_setDynamicBitrate(s->codec, avctx->bit_rate)) {
|
||||
+ av_log(avctx, AV_LOG_ERROR, "Failed to set bitrate to %d\n", avctx->bit_rate);
|
||||
+ } else {
|
||||
+ av_log(avctx, AV_LOG_INFO, "Set bitrate to %d\n", avctx->bit_rate);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
static int mediacodec_encode(AVCodecContext *avctx, AVPacket *pkt)
|
||||
{
|
||||
MediaCodecEncContext *s = avctx->priv_data;
|
||||
int ret;
|
||||
int got_packet = 0;
|
||||
|
||||
+ update_config(avctx);
|
||||
// Return on three case:
|
||||
// 1. Serious error
|
||||
// 2. Got a packet success
|
||||
--
|
||||
2.34.1
|
||||
|
||||
1993
res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch
Normal file
1993
res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,9 @@ vcpkg_from_github(
|
||||
patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch
|
||||
patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch
|
||||
patch/0003-amf-colorspace.patch
|
||||
patch/0004-videotoolbox-changing-bitrate.patch
|
||||
patch/0005-mediacodec-changing-bitrate.patch
|
||||
patch/0006-dlopen-libva.patch
|
||||
)
|
||||
|
||||
if(SOURCE_PATH MATCHES " ")
|
||||
@@ -77,13 +80,15 @@ else()
|
||||
endif()
|
||||
|
||||
if(VCPKG_TARGET_IS_LINUX)
|
||||
string(APPEND OPTIONS "\
|
||||
string(APPEND OPTIONS "\
|
||||
--target-os=linux \
|
||||
--enable-pthreads \
|
||||
--disable-vdpau \
|
||||
")
|
||||
|
||||
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm")
|
||||
else()
|
||||
string(APPEND OPTIONS "\
|
||||
string(APPEND OPTIONS "\
|
||||
--enable-cuda \
|
||||
--enable-ffnvcodec \
|
||||
--enable-encoder=h264_nvenc \
|
||||
@@ -98,8 +103,9 @@ if(VCPKG_TARGET_IS_LINUX)
|
||||
--enable-encoder=h264_vaapi \
|
||||
--enable-encoder=hevc_vaapi \
|
||||
")
|
||||
|
||||
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
|
||||
string(APPEND OPTIONS "\
|
||||
string(APPEND OPTIONS "\
|
||||
--enable-cuda_llvm \
|
||||
")
|
||||
endif()
|
||||
@@ -127,7 +133,8 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
||||
--enable-libmfx \
|
||||
--enable-encoder=h264_qsv \
|
||||
--enable-encoder=hevc_qsv \
|
||||
")
|
||||
")
|
||||
|
||||
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
|
||||
set(LIB_MACHINE_ARG /machine:x86)
|
||||
string(APPEND OPTIONS " --arch=i686 --enable-cross-compile")
|
||||
@@ -189,6 +196,7 @@ endif()
|
||||
|
||||
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"")
|
||||
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"")
|
||||
|
||||
if(VCPKG_TARGET_IS_WINDOWS)
|
||||
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"")
|
||||
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"")
|
||||
@@ -202,9 +210,11 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER)
|
||||
get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME)
|
||||
set(ENV{CC} "${CC_filename}")
|
||||
string(APPEND OPTIONS " --cc=${CC_filename}")
|
||||
|
||||
if(VCPKG_HOST_IS_WINDOWS)
|
||||
string(APPEND OPTIONS " --host_cc=${CC_filename}")
|
||||
endif()
|
||||
|
||||
list(APPEND prog_env "${CC_path}")
|
||||
endif()
|
||||
|
||||
@@ -282,6 +292,7 @@ if(VCPKG_HOST_IS_WINDOWS)
|
||||
else()
|
||||
# find_program(SHELL bash)
|
||||
endif()
|
||||
|
||||
list(REMOVE_DUPLICATES prog_env)
|
||||
vcpkg_add_to_path(PREPEND ${prog_env})
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(oboe_wrapper CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
add_library(oboe_wrapper STATIC
|
||||
oboe.cc
|
||||
)
|
||||
|
||||
target_include_directories(oboe_wrapper PRIVATE "${CURRENT_INSTALLED_DIR}/include")
|
||||
|
||||
install(TARGETS oboe_wrapper
|
||||
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
@@ -1,118 +0,0 @@
|
||||
#include <oboe/Oboe.h>
|
||||
#include <math.h>
|
||||
#include <deque>
|
||||
#include <pthread.h>
|
||||
|
||||
// I got link problem with std::mutex, so use pthread instead
|
||||
class CThreadLock
|
||||
{
|
||||
public:
|
||||
CThreadLock();
|
||||
virtual ~CThreadLock();
|
||||
|
||||
void Lock();
|
||||
void Unlock();
|
||||
|
||||
private:
|
||||
pthread_mutex_t mutexlock;
|
||||
};
|
||||
|
||||
CThreadLock::CThreadLock()
|
||||
{
|
||||
// init lock here
|
||||
pthread_mutex_init(&mutexlock, 0);
|
||||
}
|
||||
|
||||
CThreadLock::~CThreadLock()
|
||||
{
|
||||
// deinit lock here
|
||||
pthread_mutex_destroy(&mutexlock);
|
||||
}
|
||||
void CThreadLock::Lock()
|
||||
{
|
||||
// lock
|
||||
pthread_mutex_lock(&mutexlock);
|
||||
}
|
||||
void CThreadLock::Unlock()
|
||||
{
|
||||
// unlock
|
||||
pthread_mutex_unlock(&mutexlock);
|
||||
}
|
||||
|
||||
class Player : public oboe::AudioStreamDataCallback
|
||||
{
|
||||
public:
|
||||
Player(int channels, int sample_rate)
|
||||
{
|
||||
this->channels = channels;
|
||||
oboe::AudioStreamBuilder builder;
|
||||
// The builder set methods can be chained for convenience.
|
||||
builder.setSharingMode(oboe::SharingMode::Exclusive)
|
||||
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
||||
->setChannelCount(channels)
|
||||
->setSampleRate(sample_rate)
|
||||
->setFormat(oboe::AudioFormat::Float)
|
||||
->setDataCallback(this)
|
||||
->openManagedStream(outStream);
|
||||
// Typically, start the stream after querying some stream information, as well as some input from the user
|
||||
outStream->requestStart();
|
||||
}
|
||||
|
||||
~Player() {
|
||||
outStream->requestStop();
|
||||
}
|
||||
|
||||
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override
|
||||
{
|
||||
float *floatData = (float *)audioData;
|
||||
int i = 0;
|
||||
mtx.Lock();
|
||||
auto n = channels * numFrames;
|
||||
for (; i < n && i < (int)buffer.size(); ++i, ++floatData)
|
||||
{
|
||||
*floatData = buffer.front();
|
||||
buffer.pop_front();
|
||||
}
|
||||
mtx.Unlock();
|
||||
for (; i < n; ++i, ++floatData)
|
||||
{
|
||||
*floatData = 0;
|
||||
}
|
||||
return oboe::DataCallbackResult::Continue;
|
||||
}
|
||||
|
||||
void push(const float *v, int n)
|
||||
{
|
||||
mtx.Lock();
|
||||
for (auto i = 0; i < n; ++i, ++v)
|
||||
buffer.push_back(*v);
|
||||
// in case memory overuse
|
||||
if (buffer.size() > 48 * 1024 * 120)
|
||||
buffer.clear();
|
||||
mtx.Unlock();
|
||||
}
|
||||
|
||||
private:
|
||||
oboe::ManagedStream outStream;
|
||||
int channels;
|
||||
std::deque<float> buffer;
|
||||
CThreadLock mtx;
|
||||
};
|
||||
|
||||
extern "C"
|
||||
{
|
||||
void *create_oboe_player(int channels, int sample_rate)
|
||||
{
|
||||
return new Player(channels, sample_rate);
|
||||
}
|
||||
|
||||
void push_oboe_data(void *player, const float* v, int n)
|
||||
{
|
||||
static_cast<Player *>(player)->push(v, n);
|
||||
}
|
||||
|
||||
void destroy_oboe_player(void *player)
|
||||
{
|
||||
delete static_cast<Player *>(player);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
vcpkg_configure_cmake(
|
||||
SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}"
|
||||
OPTIONS
|
||||
-DCURRENT_INSTALLED_DIR=${CURRENT_INSTALLED_DIR}
|
||||
PREFER_NINJA
|
||||
)
|
||||
|
||||
vcpkg_cmake_install()
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "oboe-wrapper",
|
||||
"version": "0",
|
||||
"description": "None",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "vcpkg-cmake",
|
||||
"host": true
|
||||
},
|
||||
{
|
||||
"name": "vcpkg-cmake-config",
|
||||
"host": true
|
||||
},
|
||||
{
|
||||
"name": "oboe",
|
||||
"host": false
|
||||
}
|
||||
]
|
||||
}
|
||||
520
src/client.rs
520
src/client.rs
@@ -2,14 +2,12 @@ use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use clipboard_master::{CallbackResult, ClipboardHandler};
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
Device, Host, StreamConfig,
|
||||
};
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use magnum_opus::{Channels::*, Decoder as AudioDecoder};
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
use ringbuf::{ring_buffer::RbBase, Rb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -73,8 +71,10 @@ use crate::{
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use crate::clipboard::CLIPBOARD_INTERVAL;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL};
|
||||
use crate::clipboard::{check_clipboard, ClipboardSide};
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::ui_session_interface::SessionPermissionConfig;
|
||||
@@ -117,8 +117,7 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
|
||||
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
|
||||
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
pub const AUDIO_BUFFER_MS: usize = 150;
|
||||
pub const AUDIO_BUFFER_MS: usize = 3000;
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -134,13 +133,12 @@ pub(crate) struct ClientClipboardContext {
|
||||
/// Client of the remote desktop.
|
||||
pub struct Client;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
struct TextClipboardState {
|
||||
is_required: bool,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AUDIO_HOST: Host = cpal::default_host();
|
||||
}
|
||||
@@ -148,6 +146,10 @@ lazy_static::lazy_static! {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
|
||||
}
|
||||
|
||||
@@ -163,66 +165,6 @@ pub fn get_key_state(key: enigo::Key) -> bool {
|
||||
ENIGO.lock().unwrap().get_key_state(key)
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "android")] {
|
||||
|
||||
use hbb_common::libc::{c_float, c_int};
|
||||
type Oboe = *mut c_void;
|
||||
extern "C" {
|
||||
fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe;
|
||||
fn push_oboe_data(oboe: Oboe, d: *const c_float, n: c_int);
|
||||
fn destroy_oboe_player(oboe: Oboe);
|
||||
}
|
||||
|
||||
struct OboePlayer {
|
||||
raw: Oboe,
|
||||
}
|
||||
|
||||
impl Default for OboePlayer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
raw: std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OboePlayer {
|
||||
fn new(channels: i32, sample_rate: i32) -> Self {
|
||||
unsafe {
|
||||
Self {
|
||||
raw: create_oboe_player(channels, sample_rate),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn is_null(&self) -> bool {
|
||||
self.raw.is_null()
|
||||
}
|
||||
|
||||
fn push(&mut self, d: &[f32]) {
|
||||
if self.raw.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
push_oboe_data(self.raw, d.as_ptr(), d.len() as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OboePlayer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.raw.is_null() {
|
||||
destroy_oboe_player(self.raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Start a new connection.
|
||||
pub async fn start(
|
||||
@@ -712,12 +654,12 @@ impl Client {
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn set_is_text_clipboard_required(b: bool) {
|
||||
TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
fn try_stop_clipboard() {
|
||||
// There's a bug here.
|
||||
// If session is closed by the peer, `has_sessions_running()` will always return true.
|
||||
@@ -812,9 +754,41 @@ impl Client {
|
||||
|
||||
Some(rx_started)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn try_start_clipboard(_p: Option<()>) -> Option<UnboundedReceiver<()>> {
|
||||
let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap();
|
||||
if clipboard_lock.running {
|
||||
return None;
|
||||
}
|
||||
clipboard_lock.running = true;
|
||||
|
||||
log::info!("Start text clipboard loop");
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().running {
|
||||
break;
|
||||
}
|
||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
|
||||
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(msg) = crate::clipboard::get_clipboards_msg(true) {
|
||||
crate::flutter::send_text_clipboard_msg(msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
|
||||
}
|
||||
log::info!("Stop text clipboard loop");
|
||||
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
impl TextClipboardState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
@@ -887,100 +861,135 @@ impl ClipboardHandler for ClientClipboardHandler {
|
||||
#[derive(Default)]
|
||||
pub struct AudioHandler {
|
||||
audio_decoder: Option<(AudioDecoder, Vec<f32>)>,
|
||||
#[cfg(target_os = "android")]
|
||||
oboe: Option<OboePlayer>,
|
||||
#[cfg(target_os = "linux")]
|
||||
simple: Option<psimple::Simple>,
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
audio_buffer: AudioBuffer,
|
||||
sample_rate: (u32, u32),
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
audio_stream: Option<Box<dyn StreamTrait>>,
|
||||
channels: u16,
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
device_channel: u16,
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
ready: Arc<std::sync::Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
struct AudioBuffer(pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>);
|
||||
struct AudioBuffer(
|
||||
pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
|
||||
usize,
|
||||
[usize; 30],
|
||||
);
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
impl Default for AudioBuffer {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(std::sync::Mutex::new(
|
||||
ringbuf::HeapRb::<f32>::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel
|
||||
)))
|
||||
Self(
|
||||
Arc::new(std::sync::Mutex::new(
|
||||
ringbuf::HeapRb::<f32>::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel
|
||||
)),
|
||||
48000 * 2,
|
||||
[0; 30],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
impl AudioBuffer {
|
||||
pub fn resize(&self, sample_rate: usize, channels: usize) {
|
||||
pub fn resize(&mut self, sample_rate: usize, channels: usize) {
|
||||
let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000;
|
||||
let old_capacity = self.0.lock().unwrap().capacity();
|
||||
if capacity != old_capacity {
|
||||
*self.0.lock().unwrap() = ringbuf::HeapRb::<f32>::new(capacity);
|
||||
self.1 = sample_rate * channels;
|
||||
log::info!("Audio buffer resized from {old_capacity} to {capacity}");
|
||||
}
|
||||
}
|
||||
|
||||
// clear when full to avoid long time noise
|
||||
#[inline]
|
||||
pub fn clear_if_full(&self) {
|
||||
let full = self.0.lock().unwrap().is_full();
|
||||
if full {
|
||||
self.0.lock().unwrap().clear();
|
||||
log::trace!("Audio buffer cleared");
|
||||
fn try_shrink(&mut self, having: usize) {
|
||||
extern crate chrono;
|
||||
use chrono::prelude::*;
|
||||
|
||||
let mut i = (having * 10) / self.1;
|
||||
if i > 29 {
|
||||
i = 29;
|
||||
}
|
||||
self.2[i] += 1;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
static mut tms: i64 = 0;
|
||||
let dt = Local::now().timestamp_millis();
|
||||
unsafe {
|
||||
if tms == 0 {
|
||||
tms = dt;
|
||||
return;
|
||||
} else if dt < tms + 12000 {
|
||||
return;
|
||||
}
|
||||
tms = dt;
|
||||
}
|
||||
|
||||
// the safer water mark to drop
|
||||
let mut zero = 0;
|
||||
// the water mark taking most of time
|
||||
let mut max = 0;
|
||||
for i in 0..30 {
|
||||
if self.2[i] == 0 && zero == i {
|
||||
zero += 1;
|
||||
}
|
||||
|
||||
if self.2[i] > self.2[max] {
|
||||
self.2[max] = 0;
|
||||
max = i;
|
||||
} else {
|
||||
self.2[i] = 0;
|
||||
}
|
||||
}
|
||||
zero = zero * 2 / 3;
|
||||
|
||||
// how many data can be dropped:
|
||||
// 1. will not drop if buffered data is less than 600ms
|
||||
// 2. choose based on min(zero, max)
|
||||
const N: usize = 4;
|
||||
self.2[max] = 0;
|
||||
if max < 6 {
|
||||
return;
|
||||
} else if max > zero * N {
|
||||
max = zero * N;
|
||||
}
|
||||
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
let cap = lock.capacity();
|
||||
let having = lock.occupied_len();
|
||||
let skip = (cap * max / (30 * N) + 1) & (!1);
|
||||
if (having > skip * 3) && (skip > 0) {
|
||||
lock.skip(skip);
|
||||
log::info!("skip {skip}, based {max} {zero}");
|
||||
}
|
||||
}
|
||||
|
||||
/// append pcm to audio buffer, if buffered data
|
||||
/// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS
|
||||
/// will be kept.
|
||||
fn append_pcm2(&self, buffer: &[f32]) -> usize {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
let cap = lock.capacity();
|
||||
if buffer.len() > cap {
|
||||
lock.push_slice_overwrite(buffer);
|
||||
return cap;
|
||||
}
|
||||
|
||||
let having = lock.occupied_len() + buffer.len();
|
||||
if having > cap {
|
||||
lock.skip(having - cap);
|
||||
}
|
||||
lock.push_slice_overwrite(buffer);
|
||||
lock.occupied_len()
|
||||
}
|
||||
|
||||
/// append pcm to audio buffer, trying to drop data
|
||||
/// when data is too much (per 12 seconds) based
|
||||
/// statistics.
|
||||
pub fn append_pcm(&mut self, buffer: &[f32]) {
|
||||
let having = self.append_pcm2(buffer);
|
||||
self.try_shrink(having);
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioHandler {
|
||||
/// Start the audio playback.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
|
||||
use psimple::Simple;
|
||||
use pulse::sample::{Format, Spec};
|
||||
use pulse::stream::Direction;
|
||||
|
||||
let spec = Spec {
|
||||
format: Format::F32le,
|
||||
channels: format0.channels as _,
|
||||
rate: format0.sample_rate as _,
|
||||
};
|
||||
if !spec.is_valid() {
|
||||
bail!("Invalid audio format");
|
||||
}
|
||||
|
||||
self.simple = Some(Simple::new(
|
||||
None, // Use the default server
|
||||
&crate::get_app_name(), // Our application’s name
|
||||
Direction::Playback, // We want a playback stream
|
||||
None, // Use the default device
|
||||
"playback", // Description of our stream
|
||||
&spec, // Our sample format
|
||||
None, // Use default channel map
|
||||
None, // Use default buffering attributes
|
||||
)?);
|
||||
self.sample_rate = (format0.sample_rate, format0.sample_rate);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the audio playback.
|
||||
#[cfg(target_os = "android")]
|
||||
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
|
||||
self.oboe = Some(OboePlayer::new(
|
||||
format0.channels as _,
|
||||
format0.sample_rate as _,
|
||||
));
|
||||
self.sample_rate = (format0.sample_rate, format0.sample_rate);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the audio playback.
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
|
||||
let device = AUDIO_HOST
|
||||
.default_output_device()
|
||||
@@ -993,7 +1002,9 @@ impl AudioHandler {
|
||||
let sample_format = config.sample_format();
|
||||
log::info!("Default output format: {:?}", config);
|
||||
log::info!("Remote input format: {:?}", format0);
|
||||
let config: StreamConfig = config.into();
|
||||
let mut config: StreamConfig = config.into();
|
||||
config.buffer_size = cpal::BufferSize::Fixed(64);
|
||||
|
||||
self.sample_rate = (format0.sample_rate, config.sample_rate.0);
|
||||
let mut build_output_stream = |config: StreamConfig| match sample_format {
|
||||
cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device),
|
||||
@@ -1041,28 +1052,16 @@ impl AudioHandler {
|
||||
/// Handle audio frame and play it.
|
||||
#[inline]
|
||||
pub fn handle_frame(&mut self, frame: AudioFrame) {
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() {
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if self.simple.is_none() {
|
||||
log::debug!("PulseAudio simple binding does not exists");
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
if self.oboe.is_none() {
|
||||
return;
|
||||
}
|
||||
self.audio_decoder.as_mut().map(|(d, buffer)| {
|
||||
if let Ok(n) = d.decode_float(&frame.data, buffer, false) {
|
||||
let channels = self.channels;
|
||||
let n = n * (channels as usize);
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
{
|
||||
let sample_rate0 = self.sample_rate.0;
|
||||
let sample_rate = self.sample_rate.1;
|
||||
let audio_buffer = self.audio_buffer.0.clone();
|
||||
let mut buffer = buffer[0..n].to_owned();
|
||||
if sample_rate != sample_rate0 {
|
||||
buffer = crate::audio_resample(
|
||||
@@ -1081,25 +1080,13 @@ impl AudioHandler {
|
||||
self.device_channel,
|
||||
);
|
||||
}
|
||||
self.audio_buffer.clear_if_full();
|
||||
audio_buffer.lock().unwrap().push_slice_overwrite(&buffer);
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
self.oboe.as_mut().map(|x| x.push(&buffer[0..n]));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let data_u8 =
|
||||
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
|
||||
self.simple.as_mut().map(|x| x.write(data_u8));
|
||||
self.audio_buffer.append_pcm(&buffer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Build audio output stream for current device.
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
fn build_output_stream<T: cpal::Sample + cpal::SizedSample + cpal::FromSample<f32>>(
|
||||
&mut self,
|
||||
config: &StreamConfig,
|
||||
@@ -1117,18 +1104,46 @@ impl AudioHandler {
|
||||
let timeout = None;
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [T], _: &_| {
|
||||
move |data: &mut [T], info: &cpal::OutputCallbackInfo| {
|
||||
if !*ready.lock().unwrap() {
|
||||
*ready.lock().unwrap() = true;
|
||||
}
|
||||
let mut lock = audio_buffer.lock().unwrap();
|
||||
|
||||
let mut n = data.len();
|
||||
if lock.occupied_len() < n {
|
||||
n = lock.occupied_len();
|
||||
let mut lock = audio_buffer.lock().unwrap();
|
||||
let mut having = lock.occupied_len();
|
||||
// android two timestamps, one from zero, another not
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if having < n {
|
||||
let tms = info.timestamp();
|
||||
let how_long = tms
|
||||
.playback
|
||||
.duration_since(&tms.callback)
|
||||
.unwrap_or(Duration::from_millis(0));
|
||||
|
||||
// must long enough to fight back scheuler delay
|
||||
if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000)
|
||||
{
|
||||
drop(lock);
|
||||
std::thread::sleep(how_long.div_f32(1.2));
|
||||
lock = audio_buffer.lock().unwrap();
|
||||
having = lock.occupied_len();
|
||||
}
|
||||
|
||||
if having < n {
|
||||
n = having;
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
if having < n {
|
||||
n = having;
|
||||
}
|
||||
let mut elems = vec![0.0f32; n];
|
||||
lock.pop_slice(&mut elems);
|
||||
if n > 0 {
|
||||
lock.pop_slice(&mut elems);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut input = elems.into_iter();
|
||||
for sample in data.iter_mut() {
|
||||
*sample = match input.next() {
|
||||
@@ -1173,9 +1188,15 @@ impl VideoHandler {
|
||||
pub fn new(format: CodecFormat, _display: usize) -> Self {
|
||||
let luid = Self::get_adapter_luid();
|
||||
log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}");
|
||||
let rgba_format =
|
||||
if cfg!(feature = "flutter") && (cfg!(windows) || cfg!(target_os = "linux")) {
|
||||
ImageFormat::ABGR
|
||||
} else {
|
||||
ImageFormat::ARGB
|
||||
};
|
||||
VideoHandler {
|
||||
decoder: Decoder::new(format, luid),
|
||||
rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()),
|
||||
rgb: ImageRgb::new(rgba_format, crate::get_dst_align_rgba()),
|
||||
texture: Default::default(),
|
||||
recorder: Default::default(),
|
||||
record: false,
|
||||
@@ -1394,18 +1415,18 @@ impl LoginConfigHandler {
|
||||
let server = server_key.next().unwrap_or_default();
|
||||
let args = server_key.next().unwrap_or_default();
|
||||
let key = if server == PUBLIC_SERVER {
|
||||
PUBLIC_RS_PUB_KEY
|
||||
PUBLIC_RS_PUB_KEY.to_owned()
|
||||
} else {
|
||||
let mut args_map: HashMap<&str, &str> = HashMap::new();
|
||||
let mut args_map: HashMap<String, &str> = HashMap::new();
|
||||
for arg in args.split('&') {
|
||||
if let Some(kv) = arg.find('=') {
|
||||
let k = &arg[0..kv];
|
||||
let k = arg[0..kv].to_lowercase();
|
||||
let v = &arg[kv + 1..];
|
||||
args_map.insert(k, v);
|
||||
}
|
||||
}
|
||||
let key = args_map.remove("key").unwrap_or_default();
|
||||
key
|
||||
key.to_owned()
|
||||
};
|
||||
|
||||
// here we can check <id>/r@server
|
||||
@@ -1413,7 +1434,7 @@ impl LoginConfigHandler {
|
||||
if real_id != raw_id {
|
||||
force_relay = true;
|
||||
}
|
||||
self.other_server = Some((real_id.clone(), server.to_owned(), key.to_owned()));
|
||||
self.other_server = Some((real_id.clone(), server.to_owned(), key));
|
||||
id = format!("{real_id}@{server}");
|
||||
} else {
|
||||
let real_id = crate::ui_interface::handle_relay_id(&id);
|
||||
@@ -2260,74 +2281,60 @@ impl LoginConfigHandler {
|
||||
|
||||
/// Media data.
|
||||
pub enum MediaData {
|
||||
VideoQueue(usize),
|
||||
VideoQueue,
|
||||
VideoFrame(Box<VideoFrame>),
|
||||
AudioFrame(Box<AudioFrame>),
|
||||
AudioFormat(AudioFormat),
|
||||
Reset(Option<usize>),
|
||||
Reset,
|
||||
RecordScreen(bool),
|
||||
}
|
||||
|
||||
pub type MediaSender = mpsc::Sender<MediaData>;
|
||||
|
||||
struct VideoHandlerController {
|
||||
handler: VideoHandler,
|
||||
skip_beginning: u32,
|
||||
}
|
||||
|
||||
/// Start video and audio thread.
|
||||
/// Return two [`MediaSender`], they should be given to the media producer.
|
||||
/// Start video thread.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `video_callback` - The callback for video frame. Being called when a video frame is ready.
|
||||
pub fn start_video_audio_threads<F, T>(
|
||||
pub fn start_video_thread<F, T>(
|
||||
session: Session<T>,
|
||||
display: usize,
|
||||
video_receiver: mpsc::Receiver<MediaData>,
|
||||
video_queue: Arc<RwLock<ArrayQueue<VideoFrame>>>,
|
||||
fps: Arc<RwLock<Option<usize>>>,
|
||||
chroma: Arc<RwLock<Option<Chroma>>>,
|
||||
discard_queue: Arc<RwLock<bool>>,
|
||||
video_callback: F,
|
||||
) -> (
|
||||
MediaSender,
|
||||
MediaSender,
|
||||
Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
|
||||
Arc<RwLock<Option<usize>>>,
|
||||
Arc<RwLock<Option<Chroma>>>,
|
||||
)
|
||||
where
|
||||
) where
|
||||
F: 'static + FnMut(usize, &mut scrap::ImageRgb, *mut c_void, bool) + Send,
|
||||
T: InvokeUiSession,
|
||||
{
|
||||
let (video_sender, video_receiver) = mpsc::channel::<MediaData>();
|
||||
let video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>> = Default::default();
|
||||
let video_queue_map_cloned = video_queue_map.clone();
|
||||
let mut video_callback = video_callback;
|
||||
|
||||
let fps = Arc::new(RwLock::new(None));
|
||||
let decode_fps_map = fps.clone();
|
||||
let chroma = Arc::new(RwLock::new(None));
|
||||
let chroma_cloned = chroma.clone();
|
||||
let mut last_chroma = None;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
#[cfg(windows)]
|
||||
sync_cpu_usage();
|
||||
get_hwcodec_config();
|
||||
let mut handler_controller_map = HashMap::new();
|
||||
let mut video_handler = None;
|
||||
let mut count = 0;
|
||||
let mut duration = std::time::Duration::ZERO;
|
||||
let mut skip_beginning = 0;
|
||||
loop {
|
||||
if let Ok(data) = video_receiver.recv() {
|
||||
match data {
|
||||
MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => {
|
||||
MediaData::VideoFrame(_) | MediaData::VideoQueue => {
|
||||
let vf = match data {
|
||||
MediaData::VideoFrame(vf) => *vf,
|
||||
MediaData::VideoQueue(display) => {
|
||||
if let Some(video_queue) =
|
||||
video_queue_map.read().unwrap().get(&display)
|
||||
{
|
||||
if let Some(vf) = video_queue.pop() {
|
||||
vf
|
||||
} else {
|
||||
MediaData::VideoFrame(vf) => {
|
||||
*discard_queue.write().unwrap() = false;
|
||||
*vf
|
||||
}
|
||||
MediaData::VideoQueue => {
|
||||
if let Some(vf) = video_queue.read().unwrap().pop() {
|
||||
if discard_queue.read().unwrap().clone() {
|
||||
continue;
|
||||
}
|
||||
vf
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -2340,36 +2347,25 @@ where
|
||||
let display = vf.display as usize;
|
||||
let start = std::time::Instant::now();
|
||||
let format = CodecFormat::from(&vf);
|
||||
if !handler_controller_map.contains_key(&display) {
|
||||
if video_handler.is_none() {
|
||||
let mut handler = VideoHandler::new(format, display);
|
||||
let record = session.lc.read().unwrap().record;
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
if record {
|
||||
handler.record_screen(record, id, display);
|
||||
}
|
||||
handler_controller_map.insert(
|
||||
display,
|
||||
VideoHandlerController {
|
||||
handler,
|
||||
skip_beginning: 0,
|
||||
},
|
||||
);
|
||||
video_handler = Some(handler);
|
||||
}
|
||||
if let Some(handler_controller) = handler_controller_map.get_mut(&display) {
|
||||
if let Some(handler) = video_handler.as_mut() {
|
||||
let mut pixelbuffer = true;
|
||||
let mut tmp_chroma = None;
|
||||
let format_changed =
|
||||
handler_controller.handler.decoder.format() != format;
|
||||
match handler_controller.handler.handle_frame(
|
||||
vf,
|
||||
&mut pixelbuffer,
|
||||
&mut tmp_chroma,
|
||||
) {
|
||||
let format_changed = handler.decoder.format() != format;
|
||||
match handler.handle_frame(vf, &mut pixelbuffer, &mut tmp_chroma) {
|
||||
Ok(true) => {
|
||||
video_callback(
|
||||
display,
|
||||
&mut handler_controller.handler.rgb,
|
||||
handler_controller.handler.texture.texture,
|
||||
&mut handler.rgb,
|
||||
handler.texture.texture,
|
||||
pixelbuffer,
|
||||
);
|
||||
|
||||
@@ -2381,7 +2377,7 @@ where
|
||||
|
||||
// fps calculation
|
||||
fps_calculate(
|
||||
handler_controller,
|
||||
&mut skip_beginning,
|
||||
&fps,
|
||||
format_changed,
|
||||
start.elapsed(),
|
||||
@@ -2410,53 +2406,36 @@ where
|
||||
|
||||
// check invalid decoders
|
||||
let mut should_update_supported = false;
|
||||
handler_controller_map
|
||||
.iter()
|
||||
.map(|(_, h)| {
|
||||
if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER {
|
||||
let mut lc = session.lc.write().unwrap();
|
||||
let format = h.handler.decoder.format();
|
||||
if !lc.mark_unsupported.contains(&format) {
|
||||
lc.mark_unsupported.push(format);
|
||||
should_update_supported = true;
|
||||
log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", h.handler.decoder.valid(), h.handler.fail_counter, lc.mark_unsupported);
|
||||
}
|
||||
if let Some(handler) = video_handler.as_mut() {
|
||||
if !handler.decoder.valid()
|
||||
|| handler.fail_counter >= MAX_DECODE_FAIL_COUNTER
|
||||
{
|
||||
let mut lc = session.lc.write().unwrap();
|
||||
let format = handler.decoder.format();
|
||||
if !lc.mark_unsupported.contains(&format) {
|
||||
lc.mark_unsupported.push(format);
|
||||
should_update_supported = true;
|
||||
log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", handler.decoder.valid(), handler.fail_counter, lc.mark_unsupported);
|
||||
}
|
||||
})
|
||||
.count();
|
||||
}
|
||||
}
|
||||
if should_update_supported {
|
||||
session.send(Data::Message(
|
||||
session.lc.read().unwrap().update_supported_decodings(),
|
||||
));
|
||||
}
|
||||
}
|
||||
MediaData::Reset(display) => {
|
||||
if let Some(display) = display {
|
||||
if let Some(handler_controler) =
|
||||
handler_controller_map.get_mut(&display)
|
||||
{
|
||||
handler_controler.handler.reset(None);
|
||||
}
|
||||
} else {
|
||||
for (_, handler_controler) in handler_controller_map.iter_mut() {
|
||||
handler_controler.handler.reset(None);
|
||||
}
|
||||
MediaData::Reset => {
|
||||
if let Some(handler) = video_handler.as_mut() {
|
||||
handler.reset(None);
|
||||
}
|
||||
}
|
||||
MediaData::RecordScreen(start) => {
|
||||
log::info!("record screen command: start: {start}");
|
||||
let record = session.lc.read().unwrap().record;
|
||||
session.update_record_status(start);
|
||||
if record != start {
|
||||
session.lc.write().unwrap().record = start;
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
for (display, handler_controler) in handler_controller_map.iter_mut() {
|
||||
handler_controler.handler.record_screen(
|
||||
start,
|
||||
id.clone(),
|
||||
*display,
|
||||
);
|
||||
}
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
if let Some(handler) = video_handler.as_mut() {
|
||||
handler.record_screen(start, id, display);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -2467,14 +2446,6 @@ where
|
||||
}
|
||||
log::info!("Video decoder loop exits");
|
||||
});
|
||||
let audio_sender = start_audio_thread();
|
||||
return (
|
||||
video_sender,
|
||||
audio_sender,
|
||||
video_queue_map_cloned,
|
||||
decode_fps_map,
|
||||
chroma_cloned,
|
||||
);
|
||||
}
|
||||
|
||||
/// Start an audio thread
|
||||
@@ -2506,7 +2477,7 @@ pub fn start_audio_thread() -> MediaSender {
|
||||
|
||||
#[inline]
|
||||
fn fps_calculate(
|
||||
handler_controller: &mut VideoHandlerController,
|
||||
skip_beginning: &mut usize,
|
||||
fps: &Arc<RwLock<Option<usize>>>,
|
||||
format_changed: bool,
|
||||
elapsed: std::time::Duration,
|
||||
@@ -2516,11 +2487,11 @@ fn fps_calculate(
|
||||
if format_changed {
|
||||
*count = 0;
|
||||
*duration = std::time::Duration::ZERO;
|
||||
handler_controller.skip_beginning = 0;
|
||||
*skip_beginning = 0;
|
||||
}
|
||||
// // The first frame will be very slow
|
||||
if handler_controller.skip_beginning < 3 {
|
||||
handler_controller.skip_beginning += 1;
|
||||
if *skip_beginning < 3 {
|
||||
*skip_beginning += 1;
|
||||
return;
|
||||
}
|
||||
*duration += elapsed;
|
||||
@@ -3328,6 +3299,7 @@ lazy_static::lazy_static! {
|
||||
("VK_PRINT", Key::ControlKey(ControlKey::Print)),
|
||||
("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)),
|
||||
("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)),
|
||||
("VK_SCROLL", Key::ControlKey(ControlKey::Scroll)),
|
||||
("VK_INSERT", Key::ControlKey(ControlKey::Insert)),
|
||||
("VK_DELETE", Key::ControlKey(ControlKey::Delete)),
|
||||
("VK_HELP", Key::ControlKey(ControlKey::Help)),
|
||||
|
||||
@@ -43,6 +43,18 @@ pub trait FileManager: Interface {
|
||||
self.send(Data::CancelJob(id));
|
||||
}
|
||||
|
||||
fn read_empty_dirs(&self, path: String, include_hidden: bool) {
|
||||
let mut msg_out = Message::new();
|
||||
let mut file_action = FileAction::new();
|
||||
file_action.set_read_empty_dirs(ReadEmptyDirs {
|
||||
path,
|
||||
include_hidden,
|
||||
..Default::default()
|
||||
});
|
||||
msg_out.set_file_action(file_action);
|
||||
self.send(Data::Message(msg_out));
|
||||
}
|
||||
|
||||
fn read_remote_dir(&self, path: String, include_hidden: bool) {
|
||||
let mut msg_out = Message::new();
|
||||
let mut file_action = FileAction::new();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
num::NonZeroI64,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
@@ -8,9 +9,9 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL};
|
||||
use crate::clipboard::{update_clipboard, ClipboardSide};
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
use crate::{audio_service, ConnInner, CLIENT_SERVER};
|
||||
use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER};
|
||||
use crate::{
|
||||
client::{
|
||||
self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender,
|
||||
@@ -49,8 +50,6 @@ use scrap::CodecFormat;
|
||||
|
||||
pub struct Remote<T: InvokeUiSession> {
|
||||
handler: Session<T>,
|
||||
video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
|
||||
video_sender: MediaSender,
|
||||
audio_sender: MediaSender,
|
||||
receiver: mpsc::UnboundedReceiver<Data>,
|
||||
sender: mpsc::UnboundedSender<Data>,
|
||||
@@ -67,13 +66,11 @@ pub struct Remote<T: InvokeUiSession> {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
client_conn_id: i32, // used for file clipboard
|
||||
data_count: Arc<AtomicUsize>,
|
||||
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
|
||||
video_format: CodecFormat,
|
||||
elevation_requested: bool,
|
||||
fps_control: FpsControl,
|
||||
decode_fps: Arc<RwLock<Option<usize>>>,
|
||||
chroma: Arc<RwLock<Option<Chroma>>>,
|
||||
peer_info: ParsedPeerInfo,
|
||||
video_threads: HashMap<usize, VideoThread>,
|
||||
chroma: Arc<RwLock<Option<Chroma>>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -94,20 +91,12 @@ impl ParsedPeerInfo {
|
||||
impl<T: InvokeUiSession> Remote<T> {
|
||||
pub fn new(
|
||||
handler: Session<T>,
|
||||
video_queue: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
|
||||
video_sender: MediaSender,
|
||||
audio_sender: MediaSender,
|
||||
receiver: mpsc::UnboundedReceiver<Data>,
|
||||
sender: mpsc::UnboundedSender<Data>,
|
||||
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
|
||||
decode_fps: Arc<RwLock<Option<usize>>>,
|
||||
chroma: Arc<RwLock<Option<Chroma>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
handler,
|
||||
video_queue_map: video_queue,
|
||||
video_sender,
|
||||
audio_sender,
|
||||
audio_sender: crate::client::start_audio_thread(),
|
||||
receiver,
|
||||
sender,
|
||||
read_jobs: Vec::new(),
|
||||
@@ -120,15 +109,13 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
client_conn_id: 0,
|
||||
data_count: Arc::new(AtomicUsize::new(0)),
|
||||
frame_count_map,
|
||||
video_format: CodecFormat::Unknown,
|
||||
stop_voice_call_sender: None,
|
||||
voice_call_request_timestamp: None,
|
||||
elevation_requested: false,
|
||||
fps_control: Default::default(),
|
||||
decode_fps,
|
||||
chroma,
|
||||
peer_info: Default::default(),
|
||||
video_threads: Default::default(),
|
||||
chroma: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +237,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
_ = status_timer.tick() => {
|
||||
self.fps_control(direct);
|
||||
let elapsed = fps_instant.elapsed().as_millis();
|
||||
if elapsed < 1000 {
|
||||
continue;
|
||||
@@ -260,14 +246,14 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
speed = speed * 1000 / elapsed as usize;
|
||||
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
|
||||
|
||||
let mut frame_count_map_write = self.frame_count_map.write().unwrap();
|
||||
let frame_count_map = frame_count_map_write.clone();
|
||||
frame_count_map_write.values_mut().for_each(|v| *v = 0);
|
||||
drop(frame_count_map_write);
|
||||
let fps = frame_count_map.iter().map(|(k, v)| {
|
||||
let fps = self.video_threads.iter().map(|(k, v)| {
|
||||
// Correcting the inaccuracy of status_timer
|
||||
(k.clone(), (*v as i32) * 1000 / elapsed as i32)
|
||||
(k.clone(), (*v.frame_count.read().unwrap() as i32) * 1000 / elapsed as i32)
|
||||
}).collect::<HashMap<usize, i32>>();
|
||||
self.video_threads.iter().for_each(|(_, v)| {
|
||||
*v.frame_count.write().unwrap() = 0;
|
||||
});
|
||||
self.fps_control(direct, fps.clone());
|
||||
let chroma = self.chroma.read().unwrap().clone();
|
||||
let chroma = match chroma {
|
||||
Some(Chroma::I444) => "4:4:4",
|
||||
@@ -275,10 +261,16 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
None => "-",
|
||||
};
|
||||
let chroma = Some(chroma.to_string());
|
||||
let codec_format = if self.video_format == CodecFormat::Unknown {
|
||||
None
|
||||
} else {
|
||||
Some(self.video_format.clone())
|
||||
};
|
||||
self.handler.update_quality_status(QualityStatus {
|
||||
speed: Some(speed),
|
||||
fps,
|
||||
chroma,
|
||||
codec_format,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -302,7 +294,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.unwrap()
|
||||
.set_disconnected(round);
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if _set_disconnected_ok {
|
||||
Client::try_stop_clipboard();
|
||||
}
|
||||
@@ -498,6 +490,22 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.check_clipboard_file_context();
|
||||
}
|
||||
Data::Message(msg) => {
|
||||
match &msg.union {
|
||||
Some(message::Union::Misc(misc)) => match misc.union {
|
||||
Some(misc::Union::RefreshVideo(_)) => {
|
||||
self.video_threads.iter().for_each(|(_, v)| {
|
||||
*v.discard_queue.write().unwrap() = true;
|
||||
});
|
||||
}
|
||||
Some(misc::Union::RefreshVideoDisplay(display)) => {
|
||||
if let Some(v) = self.video_threads.get_mut(&(display as usize)) {
|
||||
*v.discard_queue.write().unwrap() = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
allow_err!(peer.send(&msg).await);
|
||||
}
|
||||
Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => {
|
||||
@@ -838,7 +846,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
Data::RecordScreen(start) => {
|
||||
let _ = self.video_sender.send(MediaData::RecordScreen(start));
|
||||
self.handler.lc.write().unwrap().record = start;
|
||||
for (_, v) in self.video_threads.iter_mut() {
|
||||
v.video_sender.send(MediaData::RecordScreen(start)).ok();
|
||||
}
|
||||
}
|
||||
Data::ElevateDirect => {
|
||||
let mut request = ElevationRequest::new();
|
||||
@@ -881,9 +892,18 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.on_voice_call_closed("Closed manually by the peer");
|
||||
allow_err!(peer.send(&msg).await);
|
||||
}
|
||||
Data::ResetDecoder(display) => {
|
||||
self.video_sender.send(MediaData::Reset(display)).ok();
|
||||
}
|
||||
Data::ResetDecoder(display) => match display {
|
||||
Some(display) => {
|
||||
if let Some(v) = self.video_threads.get_mut(&display) {
|
||||
v.video_sender.send(MediaData::Reset).ok();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
for (_, v) in self.video_threads.iter_mut() {
|
||||
v.video_sender.send(MediaData::Reset).ok();
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
@@ -1011,100 +1031,115 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// Currently, this function only considers decoding speed and queue length, not network delay.
|
||||
// The controlled end can consider auto fps as the maximum decoding fps.
|
||||
#[inline]
|
||||
fn fps_control(&mut self, direct: bool) {
|
||||
fn fps_control(&mut self, direct: bool, real_fps_map: HashMap<usize, i32>) {
|
||||
self.video_threads.iter_mut().for_each(|(k, v)| {
|
||||
let real_fps = real_fps_map.get(k).cloned().unwrap_or_default();
|
||||
if real_fps == 0 {
|
||||
v.fps_control.inactive_counter += 1;
|
||||
} else {
|
||||
v.fps_control.inactive_counter = 0;
|
||||
}
|
||||
});
|
||||
let custom_fps = self.handler.lc.read().unwrap().custom_fps.clone();
|
||||
let custom_fps = custom_fps.lock().unwrap().clone();
|
||||
let mut custom_fps = custom_fps.unwrap_or(30);
|
||||
if custom_fps < 5 || custom_fps > 120 {
|
||||
custom_fps = 30;
|
||||
}
|
||||
let ctl = &mut self.fps_control;
|
||||
let len = self
|
||||
.video_queue_map
|
||||
.read()
|
||||
.unwrap()
|
||||
let inactive_threshold = 15;
|
||||
let max_queue_len = self
|
||||
.video_threads
|
||||
.iter()
|
||||
.map(|v| v.1.len())
|
||||
.map(|v| v.1.video_queue.read().unwrap().len())
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let decode_fps = self.decode_fps.read().unwrap().clone();
|
||||
let Some(mut decode_fps) = decode_fps else {
|
||||
let min_decode_fps = self
|
||||
.video_threads
|
||||
.iter()
|
||||
.filter(|v| v.1.fps_control.inactive_counter < inactive_threshold)
|
||||
.map(|v| *v.1.decode_fps.read().unwrap())
|
||||
.min()
|
||||
.flatten();
|
||||
let Some(min_decode_fps) = min_decode_fps else {
|
||||
return;
|
||||
};
|
||||
if cfg!(feature = "flutter") {
|
||||
let active_displays = ctl
|
||||
.last_active_time
|
||||
.iter()
|
||||
.filter(|t| t.1.elapsed().as_secs() < 5)
|
||||
.count();
|
||||
if active_displays > 1 {
|
||||
decode_fps = decode_fps / active_displays;
|
||||
}
|
||||
}
|
||||
let mut limited_fps = if direct {
|
||||
decode_fps * 9 / 10 // 30 got 27
|
||||
min_decode_fps * 9 / 10 // 30 got 27
|
||||
} else {
|
||||
decode_fps * 4 / 5 // 30 got 24
|
||||
min_decode_fps * 4 / 5 // 30 got 24
|
||||
};
|
||||
if limited_fps > custom_fps {
|
||||
limited_fps = custom_fps;
|
||||
}
|
||||
let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone();
|
||||
let should_decrease = (len > 1
|
||||
&& last_auto_fps.clone().unwrap_or(custom_fps as _) > limited_fps)
|
||||
|| len > std::cmp::max(1, limited_fps / 2);
|
||||
|
||||
// increase judgement
|
||||
if len <= 1 {
|
||||
if ctl.idle_counter < usize::MAX {
|
||||
let displays = self.video_threads.keys().cloned().collect::<Vec<_>>();
|
||||
let mut fps_trending = |display: usize| {
|
||||
let thread = self.video_threads.get_mut(&display)?;
|
||||
let ctl = &mut thread.fps_control;
|
||||
let len = thread.video_queue.read().unwrap().len();
|
||||
let decode_fps = thread.decode_fps.read().unwrap().clone()?;
|
||||
let last_auto_fps = last_auto_fps.clone().unwrap_or(custom_fps as _);
|
||||
if ctl.inactive_counter > inactive_threshold {
|
||||
return None;
|
||||
}
|
||||
if len > 1 && last_auto_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) {
|
||||
ctl.idle_counter = 0;
|
||||
return Some(false);
|
||||
}
|
||||
if len <= 1 {
|
||||
ctl.idle_counter += 1;
|
||||
if ctl.idle_counter > 3 && last_auto_fps + 3 <= limited_fps {
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctl.idle_counter = 0;
|
||||
}
|
||||
let mut should_increase = false;
|
||||
if let Some(last_auto_fps) = last_auto_fps.clone() {
|
||||
// ever set
|
||||
if last_auto_fps + 3 <= limited_fps && ctl.idle_counter > 3 {
|
||||
// limited_fps is 3 larger than last set, and idle time is more than 3 seconds
|
||||
should_increase = true;
|
||||
if len > 1 {
|
||||
ctl.idle_counter = 0;
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
let trendings: Vec<_> = displays.iter().map(|k| fps_trending(*k)).collect();
|
||||
let should_decrease = trendings.iter().any(|v| *v == Some(false));
|
||||
let should_increase = !should_decrease && trendings.iter().any(|v| *v == Some(true));
|
||||
if last_auto_fps.is_none() || should_decrease || should_increase {
|
||||
// limited_fps to ensure decoding is faster than encoding
|
||||
let mut auto_fps = limited_fps;
|
||||
if should_decrease && limited_fps < len {
|
||||
if should_decrease && limited_fps < max_queue_len {
|
||||
auto_fps = limited_fps / 2;
|
||||
}
|
||||
if auto_fps < 1 {
|
||||
auto_fps = 1;
|
||||
}
|
||||
let mut misc = Misc::new();
|
||||
misc.set_option(OptionMessage {
|
||||
custom_fps: auto_fps as _,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg = Message::new();
|
||||
msg.set_misc(misc);
|
||||
self.sender.send(Data::Message(msg)).ok();
|
||||
log::info!("Set fps to {}", auto_fps);
|
||||
ctl.last_queue_size = len;
|
||||
self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps);
|
||||
if Some(auto_fps) != last_auto_fps {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_option(OptionMessage {
|
||||
custom_fps: auto_fps as _,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg = Message::new();
|
||||
msg.set_misc(misc);
|
||||
self.sender.send(Data::Message(msg)).ok();
|
||||
log::info!("Set fps to {}", auto_fps);
|
||||
self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps);
|
||||
}
|
||||
}
|
||||
// send refresh
|
||||
for (display, video_queue) in self.video_queue_map.read().unwrap().iter() {
|
||||
let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2);
|
||||
for (display, thread) in self.video_threads.iter_mut() {
|
||||
let ctl = &mut thread.fps_control;
|
||||
let video_queue = thread.video_queue.read().unwrap();
|
||||
let tolerable = std::cmp::min(min_decode_fps, video_queue.capacity() / 2);
|
||||
if ctl.refresh_times < 20 // enough
|
||||
&& (video_queue.len() > tolerable
|
||||
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10))
|
||||
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.map(|t|t.elapsed().as_secs() > 10).unwrap_or(false)))
|
||||
{
|
||||
// Refresh causes client set_display, left frames cause flickering.
|
||||
while let Some(_) = video_queue.pop() {}
|
||||
drop(video_queue);
|
||||
self.handler.refresh_video(*display as _);
|
||||
log::info!("Refresh display {} to reduce delay", display);
|
||||
ctl.refresh_times += 1;
|
||||
ctl.last_refresh_instant = Instant::now();
|
||||
ctl.last_refresh_instant = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1120,39 +1155,29 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.send_toggle_virtual_display_msg(peer).await;
|
||||
self.send_toggle_privacy_mode_msg(peer).await;
|
||||
}
|
||||
let incoming_format = CodecFormat::from(&vf);
|
||||
if self.video_format != incoming_format {
|
||||
self.video_format = incoming_format.clone();
|
||||
self.handler.update_quality_status(QualityStatus {
|
||||
codec_format: Some(incoming_format),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
self.video_format = CodecFormat::from(&vf);
|
||||
|
||||
let display = vf.display as usize;
|
||||
let mut video_queue_write = self.video_queue_map.write().unwrap();
|
||||
if !video_queue_write.contains_key(&display) {
|
||||
video_queue_write.insert(
|
||||
display,
|
||||
ArrayQueue::<VideoFrame>::new(crate::client::VIDEO_QUEUE_SIZE),
|
||||
);
|
||||
if !self.video_threads.contains_key(&display) {
|
||||
self.new_video_thread(display);
|
||||
}
|
||||
let Some(thread) = self.video_threads.get_mut(&display) else {
|
||||
return true;
|
||||
};
|
||||
if Self::contains_key_frame(&vf) {
|
||||
if let Some(video_queue) = video_queue_write.get_mut(&display) {
|
||||
while let Some(_) = video_queue.pop() {}
|
||||
}
|
||||
self.video_sender
|
||||
thread
|
||||
.video_sender
|
||||
.send(MediaData::VideoFrame(Box::new(vf)))
|
||||
.ok();
|
||||
} else {
|
||||
if let Some(video_queue) = video_queue_write.get_mut(&display) {
|
||||
video_queue.force_push(vf);
|
||||
let video_queue = thread.video_queue.read().unwrap();
|
||||
if video_queue.force_push(vf).is_some() {
|
||||
drop(video_queue);
|
||||
self.handler.refresh_video(display as _);
|
||||
} else {
|
||||
thread.video_sender.send(MediaData::VideoQueue).ok();
|
||||
}
|
||||
self.video_sender.send(MediaData::VideoQueue(display)).ok();
|
||||
}
|
||||
self.fps_control
|
||||
.last_active_time
|
||||
.insert(display, Instant::now());
|
||||
}
|
||||
Some(message::Union::Hash(hash)) => {
|
||||
self.handler
|
||||
@@ -1177,7 +1202,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.check_clipboard_file_context();
|
||||
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) {
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
let rx = Client::try_start_clipboard(None);
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -1188,7 +1213,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
},
|
||||
));
|
||||
// To make sure current text clipboard data is updated.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Some(mut rx) = rx {
|
||||
timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok();
|
||||
}
|
||||
@@ -1209,6 +1234,11 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
});
|
||||
}
|
||||
}
|
||||
// to-do: Android, is `sync_init_clipboard` really needed?
|
||||
// https://github.com/rustdesk/rustdesk/discussions/9010
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
|
||||
// on connection established client
|
||||
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
||||
@@ -1240,7 +1270,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
update_clipboard(vec![cb], ClipboardSide::Client);
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
let content = if cb.compress {
|
||||
hbb_common::compress::decompress(&cb.content)
|
||||
@@ -1251,12 +1281,16 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.handler.clipboard(content);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
crate::clipboard::handle_msg_clipboard(cb);
|
||||
}
|
||||
}
|
||||
Some(message::Union::MultiClipboards(_mcb)) => {
|
||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
||||
#[cfg(target_os = "android")]
|
||||
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
@@ -1265,6 +1299,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
Some(message::Union::FileResponse(fr)) => {
|
||||
match fr.union {
|
||||
Some(file_response::Union::EmptyDirs(res)) => {
|
||||
self.handler.update_empty_dirs(res);
|
||||
}
|
||||
Some(file_response::Union::Dir(fd)) => {
|
||||
#[cfg(windows)]
|
||||
let entries = fd.entries.to_vec();
|
||||
@@ -1421,14 +1458,14 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
Ok(Permission::Keyboard) => {
|
||||
*self.handler.server_keyboard_enabled.write().unwrap() = p.enabled;
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
self.handler.set_permission("keyboard", p.enabled);
|
||||
}
|
||||
Ok(Permission::Clipboard) => {
|
||||
*self.handler.server_clipboard_enabled.write().unwrap() = p.enabled;
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
self.handler.set_permission("clipboard", p.enabled);
|
||||
}
|
||||
@@ -1457,9 +1494,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
Some(misc::Union::SwitchDisplay(s)) => {
|
||||
self.handler.handle_peer_switch_display(&s);
|
||||
self.video_sender
|
||||
.send(MediaData::Reset(Some(s.display as _)))
|
||||
.ok();
|
||||
if let Some(thread) = self.video_threads.get_mut(&(s.display as usize)) {
|
||||
thread.video_sender.send(MediaData::Reset).ok();
|
||||
}
|
||||
|
||||
if s.width > 0 && s.height > 0 {
|
||||
self.handler.set_display(
|
||||
s.x,
|
||||
@@ -1907,6 +1945,53 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn new_video_thread(&mut self, display: usize) {
|
||||
let video_queue = Arc::new(RwLock::new(ArrayQueue::new(client::VIDEO_QUEUE_SIZE)));
|
||||
let (video_sender, video_receiver) = std::sync::mpsc::channel::<MediaData>();
|
||||
let decode_fps = Arc::new(RwLock::new(None));
|
||||
let frame_count = Arc::new(RwLock::new(0));
|
||||
let discard_queue = Arc::new(RwLock::new(false));
|
||||
let video_thread = VideoThread {
|
||||
video_queue: video_queue.clone(),
|
||||
video_sender,
|
||||
decode_fps: decode_fps.clone(),
|
||||
frame_count: frame_count.clone(),
|
||||
fps_control: Default::default(),
|
||||
discard_queue: discard_queue.clone(),
|
||||
};
|
||||
let handler = self.handler.ui_handler.clone();
|
||||
crate::client::start_video_thread(
|
||||
self.handler.clone(),
|
||||
display,
|
||||
video_receiver,
|
||||
video_queue,
|
||||
decode_fps,
|
||||
self.chroma.clone(),
|
||||
discard_queue,
|
||||
move |display: usize,
|
||||
data: &mut scrap::ImageRgb,
|
||||
_texture: *mut c_void,
|
||||
pixelbuffer: bool| {
|
||||
*frame_count.write().unwrap() += 1;
|
||||
if pixelbuffer {
|
||||
handler.on_rgba(display, data);
|
||||
} else {
|
||||
#[cfg(all(feature = "vram", feature = "flutter"))]
|
||||
handler.on_texture(display, _texture);
|
||||
}
|
||||
},
|
||||
);
|
||||
self.video_threads.insert(display, video_thread);
|
||||
let auto_record = self.handler.lc.read().unwrap().record;
|
||||
if auto_record && self.video_threads.len() == 1 {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_client_record_status(true);
|
||||
let mut msg = Message::new();
|
||||
msg.set_misc(misc);
|
||||
self.sender.send(Data::Message(msg)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoveJob {
|
||||
@@ -1939,22 +2024,19 @@ impl RemoveJob {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct FpsControl {
|
||||
last_queue_size: usize,
|
||||
refresh_times: usize,
|
||||
last_refresh_instant: Instant,
|
||||
last_refresh_instant: Option<Instant>,
|
||||
idle_counter: usize,
|
||||
last_active_time: HashMap<usize, Instant>,
|
||||
inactive_counter: usize,
|
||||
}
|
||||
|
||||
impl Default for FpsControl {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_queue_size: Default::default(),
|
||||
refresh_times: Default::default(),
|
||||
last_refresh_instant: Instant::now(),
|
||||
idle_counter: 0,
|
||||
last_active_time: Default::default(),
|
||||
}
|
||||
}
|
||||
struct VideoThread {
|
||||
video_queue: Arc<RwLock<ArrayQueue<VideoFrame>>>,
|
||||
video_sender: MediaSender,
|
||||
decode_fps: Arc<RwLock<Option<usize>>>,
|
||||
frame_count: Arc<RwLock<usize>>,
|
||||
discard_queue: Arc<RwLock<bool>>,
|
||||
fps_control: FpsControl,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use arboard::{ClipboardData, ClipboardFormat};
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use clipboard_master::{ClipboardHandler, Master, Shutdown};
|
||||
use hbb_common::{bail, log, message_proto::*, ResultType};
|
||||
use std::{
|
||||
@@ -16,6 +18,7 @@ const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner";
|
||||
// Add special format for Excel XML Spreadsheet
|
||||
const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet";
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
|
||||
// cache the clipboard msg
|
||||
@@ -27,9 +30,12 @@ lazy_static::lazy_static! {
|
||||
static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
const CLIPBOARD_GET_MAX_RETRY: usize = 3;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
|
||||
ClipboardFormat::Text,
|
||||
ClipboardFormat::Html,
|
||||
@@ -146,6 +152,7 @@ impl ClipboardContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn check_clipboard(
|
||||
ctx: &mut Option<ClipboardContext>,
|
||||
side: ClipboardSide,
|
||||
@@ -194,6 +201,7 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
let mut to_update_data = proto::from_multi_clipbards(multi_clipboards);
|
||||
if to_update_data.is_empty() {
|
||||
@@ -224,17 +232,20 @@ fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
std::thread::spawn(move || {
|
||||
update_clipboard_(multi_clipboards, side);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||
pub struct ClipboardContext {
|
||||
inner: arboard::Clipboard,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||
#[allow(unreachable_code)]
|
||||
impl ClipboardContext {
|
||||
@@ -337,10 +348,20 @@ impl ClipboardContext {
|
||||
|
||||
pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool {
|
||||
use hbb_common::get_version_number;
|
||||
get_version_number(peer_version) >= get_version_number("1.3.0")
|
||||
&& !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform)
|
||||
if get_version_number(peer_version) < get_version_number("1.3.0") {
|
||||
return false;
|
||||
}
|
||||
if ["", &whoami::Platform::Ios.to_string()].contains(&peer_platform) {
|
||||
return false;
|
||||
}
|
||||
if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn get_current_clipboard_msg(
|
||||
peer_version: &str,
|
||||
peer_platform: &str,
|
||||
@@ -406,6 +427,7 @@ impl std::fmt::Display for ClipboardSide {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn start_clipbard_master_thread(
|
||||
handler: impl ClipboardHandler + Send + 'static,
|
||||
tx_start_res: Sender<(Option<Shutdown>, String)>,
|
||||
@@ -437,6 +459,7 @@ pub fn start_clipbard_master_thread(
|
||||
|
||||
pub use proto::get_msg_if_not_support_multi_clip;
|
||||
mod proto {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use arboard::ClipboardData;
|
||||
use hbb_common::{
|
||||
compress::{compress as compress_func, decompress},
|
||||
@@ -459,6 +482,7 @@ mod proto {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn image_to_proto(a: arboard::ImageData) -> Clipboard {
|
||||
match &a {
|
||||
arboard::ImageData::Rgba(rgba) => {
|
||||
@@ -519,6 +543,7 @@ mod proto {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> {
|
||||
let d = match data {
|
||||
ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text),
|
||||
@@ -531,6 +556,7 @@ mod proto {
|
||||
Some(d)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn create_multi_clipboards(vec_data: Vec<ClipboardData>) -> MultiClipboards {
|
||||
MultiClipboards {
|
||||
clipboards: vec_data
|
||||
@@ -541,6 +567,7 @@ mod proto {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn from_clipboard(clipboard: Clipboard) -> Option<ClipboardData> {
|
||||
let data = if clipboard.compress {
|
||||
decompress(&clipboard.content)
|
||||
@@ -569,6 +596,7 @@ mod proto {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
||||
multi_clipboards
|
||||
.into_iter()
|
||||
@@ -597,3 +625,49 @@ mod proto {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn handle_msg_clipboard(mut cb: Clipboard) {
|
||||
use hbb_common::protobuf::Message;
|
||||
|
||||
if cb.compress {
|
||||
cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content));
|
||||
}
|
||||
let multi_clips = MultiClipboards {
|
||||
clipboards: vec![cb],
|
||||
..Default::default()
|
||||
};
|
||||
if let Ok(bytes) = multi_clips.write_to_bytes() {
|
||||
let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) {
|
||||
use hbb_common::protobuf::Message;
|
||||
|
||||
for cb in mcb.clipboards.iter_mut() {
|
||||
if cb.compress {
|
||||
cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content));
|
||||
}
|
||||
}
|
||||
if let Ok(bytes) = mcb.write_to_bytes() {
|
||||
let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_clipboards_msg(client: bool) -> Option<Message> {
|
||||
let mut clipboards = scrap::android::ffi::get_clipboards(client)?;
|
||||
let mut msg = Message::new();
|
||||
for c in &mut clipboards.clipboards {
|
||||
let compressed = hbb_common::compress::compress(&c.content);
|
||||
let compress = compressed.len() < c.content.len();
|
||||
if compress {
|
||||
c.content = compressed.into();
|
||||
}
|
||||
c.compress = compress;
|
||||
}
|
||||
msg.set_multi_clipboards(clipboards);
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
@@ -837,7 +837,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> {
|
||||
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
|
||||
}
|
||||
}
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1051,6 +1051,11 @@ pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec<Ke
|
||||
}
|
||||
|
||||
pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> String {
|
||||
let fd_json = _make_fd_to_json(id, path, entries);
|
||||
serde_json::to_string(&fd_json).unwrap_or("".into())
|
||||
}
|
||||
|
||||
pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Map<String, Value> {
|
||||
use serde_json::json;
|
||||
let mut fd_json = serde_json::Map::new();
|
||||
fd_json.insert("id".into(), json!(id));
|
||||
@@ -1066,7 +1071,33 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Strin
|
||||
entries_out.push(entry_map);
|
||||
}
|
||||
fd_json.insert("entries".into(), json!(entries_out));
|
||||
serde_json::to_string(&fd_json).unwrap_or("".into())
|
||||
fd_json
|
||||
}
|
||||
|
||||
pub fn make_vec_fd_to_json(fds: &[FileDirectory]) -> String {
|
||||
let mut fd_jsons = vec![];
|
||||
|
||||
for fd in fds.iter() {
|
||||
let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries);
|
||||
fd_jsons.push(fd_json);
|
||||
}
|
||||
|
||||
serde_json::to_string(&fd_jsons).unwrap_or("".into())
|
||||
}
|
||||
|
||||
pub fn make_empty_dirs_response_to_json(res: &ReadEmptyDirsResponse) -> String {
|
||||
let mut map: Map<String, Value> = serde_json::Map::new();
|
||||
map.insert("path".into(), json!(res.path));
|
||||
|
||||
let mut fd_jsons = vec![];
|
||||
|
||||
for fd in res.empty_dirs.iter() {
|
||||
let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries);
|
||||
fd_jsons.push(fd_json);
|
||||
}
|
||||
map.insert("empty_dirs".into(), fd_jsons.into());
|
||||
|
||||
serde_json::to_string(&map).unwrap_or("".into())
|
||||
}
|
||||
|
||||
/// The function to handle the url scheme sent by the system.
|
||||
|
||||
@@ -56,8 +56,8 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType<CustomServer> {
|
||||
*
|
||||
* This allows using a ',' (comma) symbol as a final delimiter.
|
||||
*/
|
||||
if s.contains("host=") {
|
||||
let stripped = &s[s.find("host=").unwrap_or(0)..s.len()];
|
||||
if s.to_lowercase().contains("host=") {
|
||||
let stripped = &s[s.to_lowercase().find("host=").unwrap_or(0)..s.len()];
|
||||
let strs: Vec<&str> = stripped.split(",").collect();
|
||||
let mut host = String::default();
|
||||
let mut key = String::default();
|
||||
@@ -65,16 +65,17 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType<CustomServer> {
|
||||
let mut relay = String::default();
|
||||
let strs_iter = strs.iter();
|
||||
for el in strs_iter {
|
||||
if el.starts_with("host=") {
|
||||
let el_lower = el.to_lowercase();
|
||||
if el_lower.starts_with("host=") {
|
||||
host = el.chars().skip(5).collect();
|
||||
}
|
||||
if el.starts_with("key=") {
|
||||
if el_lower.starts_with("key=") {
|
||||
key = el.chars().skip(4).collect();
|
||||
}
|
||||
if el.starts_with("api=") {
|
||||
if el_lower.starts_with("api=") {
|
||||
api = el.chars().skip(4).collect();
|
||||
}
|
||||
if el.starts_with("relay=") {
|
||||
if el_lower.starts_with("relay=") {
|
||||
relay = el.chars().skip(6).collect();
|
||||
}
|
||||
}
|
||||
@@ -169,6 +170,18 @@ mod test {
|
||||
relay: "server.example.net".to_owned(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
get_custom_server_from_string(
|
||||
"rustdesk-Host=server.example.net,Key=Zm9vYmFyLiwyCg==,RELAY=server.example.net.exe"
|
||||
)
|
||||
.unwrap(),
|
||||
CustomServer {
|
||||
host: "server.example.net".to_owned(),
|
||||
key: "Zm9vYmFyLiwyCg==".to_owned(),
|
||||
api: "".to_owned(),
|
||||
relay: "server.example.net".to_owned(),
|
||||
}
|
||||
);
|
||||
let lic = CustomServer {
|
||||
host: "1.1.1.1".to_owned(),
|
||||
key: "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=".to_owned(),
|
||||
|
||||
@@ -726,6 +726,20 @@ impl InvokeUiSession for FlutterHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_empty_dirs(&self, res: ReadEmptyDirsResponse) {
|
||||
self.push_event(
|
||||
"empty_dirs",
|
||||
&[
|
||||
("is_local", "false"),
|
||||
(
|
||||
"value",
|
||||
&crate::common::make_empty_dirs_response_to_json(&res),
|
||||
),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
// unused in flutter
|
||||
fn update_transfer_list(&self) {}
|
||||
|
||||
@@ -1250,15 +1264,17 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn update_text_clipboard_required() {
|
||||
let is_required = sessions::get_sessions()
|
||||
.iter()
|
||||
.any(|s| s.is_text_clipboard_required());
|
||||
#[cfg(target_os = "android")]
|
||||
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
|
||||
Client::set_is_text_clipboard_required(is_required);
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn send_text_clipboard_msg(msg: Message) {
|
||||
for s in sessions::get_sessions() {
|
||||
if s.is_text_clipboard_required() {
|
||||
@@ -2051,7 +2067,7 @@ pub mod sessions {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn has_sessions_running(conn_type: ConnType) -> bool {
|
||||
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
|
||||
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
client::file_trait::FileManager,
|
||||
common::make_fd_to_json,
|
||||
common::{make_fd_to_json, make_vec_fd_to_json},
|
||||
flutter::{
|
||||
self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option,
|
||||
},
|
||||
@@ -19,6 +19,7 @@ use hbb_common::allow_err;
|
||||
use hbb_common::{
|
||||
config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
|
||||
fs, lazy_static, log,
|
||||
message_proto::Hash,
|
||||
rendezvous_proto::ConnType,
|
||||
ResultType,
|
||||
};
|
||||
@@ -274,7 +275,7 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
|
||||
session.toggle_option(value.clone());
|
||||
try_sync_peer_option(&session, &session_id, &value, None);
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" {
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
}
|
||||
@@ -682,6 +683,27 @@ pub fn session_read_local_dir_sync(
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub fn session_read_local_empty_dirs_recursive_sync(
|
||||
_session_id: SessionID,
|
||||
path: String,
|
||||
include_hidden: bool,
|
||||
) -> String {
|
||||
if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) {
|
||||
return make_vec_fd_to_json(&fds);
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub fn session_read_remote_empty_dirs_recursive_sync(
|
||||
session_id: SessionID,
|
||||
path: String,
|
||||
include_hidden: bool,
|
||||
) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.read_empty_dirs(path, include_hidden);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
return session.get_platform(is_remote);
|
||||
@@ -774,13 +796,6 @@ pub fn main_get_sound_inputs() -> Vec<String> {
|
||||
vec![String::from("")]
|
||||
}
|
||||
|
||||
pub fn main_get_default_sound_input() -> Option<String> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return get_default_sound_input();
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn main_get_login_device_info() -> SyncReturn<String> {
|
||||
SyncReturn(get_login_device_info_json())
|
||||
}
|
||||
@@ -817,6 +832,17 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
|
||||
SyncReturn(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "android")]
|
||||
fn enable_server_clipboard(keyboard_enabled: &str, clip_enabled: &str) {
|
||||
use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard;
|
||||
let keyboard_enabled =
|
||||
config::option2bool(config::keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled);
|
||||
let clip_enabled = config::option2bool(config::keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled);
|
||||
crate::ui_cm_interface::switch_permission_all("clipboard".to_owned(), clip_enabled);
|
||||
let _ = call_clipboard_manager_enable_service_clipboard(keyboard_enabled && clip_enabled);
|
||||
}
|
||||
|
||||
pub fn main_set_option(key: String, value: String) {
|
||||
#[cfg(target_os = "android")]
|
||||
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
|
||||
@@ -824,6 +850,11 @@ pub fn main_set_option(key: String, value: String) {
|
||||
config::keys::OPTION_ENABLE_KEYBOARD,
|
||||
&value,
|
||||
));
|
||||
enable_server_clipboard(&value, &get_option(config::keys::OPTION_ENABLE_CLIPBOARD));
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) {
|
||||
enable_server_clipboard(&get_option(config::keys::OPTION_ENABLE_KEYBOARD), &value);
|
||||
}
|
||||
if key.eq("custom-rendezvous-server") {
|
||||
set_option(key, value.clone());
|
||||
@@ -1387,7 +1418,8 @@ pub fn main_get_last_remote_id() -> String {
|
||||
}
|
||||
|
||||
pub fn main_get_software_update_url() {
|
||||
if get_local_option("enable-check-update".to_string()) != "N" {
|
||||
let opt = get_local_option(config::keys::OPTION_ENABLE_CHECK_UPDATE.to_string());
|
||||
if config::option2bool(config::keys::OPTION_ENABLE_CHECK_UPDATE, &opt) {
|
||||
crate::common::check_software_update();
|
||||
}
|
||||
}
|
||||
@@ -2301,6 +2333,35 @@ pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usi
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_audio_support_loopback() -> SyncReturn<bool> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let is_surpport = true;
|
||||
#[cfg(feature = "screencapturekit")]
|
||||
let is_surpport = crate::audio_service::is_screen_capture_kit_available();
|
||||
#[cfg(not(any(target_os = "windows", feature = "screencapturekit")))]
|
||||
let is_surpport = false;
|
||||
SyncReturn(is_surpport)
|
||||
}
|
||||
|
||||
pub fn get_os_distro_info() -> SyncReturn<String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let distro = &hbb_common::platform::linux::DISTRO;
|
||||
SyncReturn(
|
||||
serde_json::to_string(&HashMap::from([
|
||||
("name", distro.name.clone()),
|
||||
("id", distro.id.clone()),
|
||||
("version_id", distro.version_id.clone()),
|
||||
]))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
SyncReturn("".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod server_side {
|
||||
use hbb_common::{config, log};
|
||||
|
||||
@@ -45,6 +45,10 @@ pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum FS {
|
||||
ReadEmptyDirs {
|
||||
dir: String,
|
||||
include_hidden: bool,
|
||||
},
|
||||
ReadDir {
|
||||
dir: String,
|
||||
include_hidden: bool,
|
||||
|
||||
@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
("Update client clipboard", ""),
|
||||
("Untagged", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
("Update client clipboard", ""),
|
||||
("Untagged", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user