mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-18 02:31:00 +03:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a69d525af | ||
|
|
307827be3c | ||
|
|
40cb59336f | ||
|
|
a9e0ea8520 | ||
|
|
baeee642dd | ||
|
|
416efe9fd3 | ||
|
|
8b5ac390d1 | ||
|
|
212e8e7559 | ||
|
|
41a20b50ea | ||
|
|
3742b51d58 | ||
|
|
1a21dff5d4 | ||
|
|
bbf7d9e08a | ||
|
|
ffed29e632 | ||
|
|
0f6538c1a7 | ||
|
|
ff2e055a5a | ||
|
|
cdf97f8717 | ||
|
|
b2af79a3c5 | ||
|
|
74cc5abd09 | ||
|
|
32c4712d5e | ||
|
|
3244395bfb | ||
|
|
1cb0e1ce7b | ||
|
|
42394fcbdd | ||
|
|
0b32e741f7 | ||
|
|
80c5d59916 | ||
|
|
e95823f543 | ||
|
|
06fe972683 | ||
|
|
3057396c02 | ||
|
|
7db9543fee | ||
|
|
58d86acf0d | ||
|
|
859020583d | ||
|
|
0cab620ba5 | ||
|
|
4338fcc51a | ||
|
|
5f6f1e8d36 | ||
|
|
a91f244f35 | ||
|
|
82bf04da81 | ||
|
|
a679e4a5e3 | ||
|
|
3c79404534 | ||
|
|
ba707d1149 | ||
|
|
93d88f30b4 | ||
|
|
f5bc136b07 | ||
|
|
ae69cbb207 | ||
|
|
39e3da1eb0 | ||
|
|
c1322b47c3 | ||
|
|
67f83bd5dd | ||
|
|
e424d01f3d | ||
|
|
a424830893 | ||
|
|
3c5810cc01 | ||
|
|
30bd4e1cef | ||
|
|
7956953669 | ||
|
|
dcba4615a2 | ||
|
|
0bf9de8256 | ||
|
|
77f1c7e74c | ||
|
|
27478946ea | ||
|
|
1f25a8af86 | ||
|
|
d75caad71f | ||
|
|
adf0226641 | ||
|
|
137f58a84a | ||
|
|
7c45a68870 | ||
|
|
99edab4b61 | ||
|
|
e50b72622c | ||
|
|
60dc40f47f | ||
|
|
841c331981 | ||
|
|
4eafa5a585 | ||
|
|
5a740e891e | ||
|
|
1fcc7001bd | ||
|
|
e57854422a | ||
|
|
8c39979848 | ||
|
|
2c38648e39 | ||
|
|
97aa739d69 | ||
|
|
b0042f29fb | ||
|
|
e3ca82945f | ||
|
|
bf6a3a7067 | ||
|
|
d25670c79a | ||
|
|
32b26e4ad3 | ||
|
|
818439db48 | ||
|
|
e23a9da1a8 | ||
|
|
37ebac2a9e | ||
|
|
46bf552afc | ||
|
|
70151e3dd8 | ||
|
|
e933f0baf2 | ||
|
|
f2a612c3d9 | ||
|
|
4a648f0068 | ||
|
|
5b52742cf7 | ||
|
|
237d234277 | ||
|
|
ed0cba281f | ||
|
|
2e0eaed322 | ||
|
|
e2a6d66805 | ||
|
|
8d6de9ca59 | ||
|
|
db108d964b | ||
|
|
f016d453fa | ||
|
|
60ea8d2c2b | ||
|
|
12ff1319f1 | ||
|
|
f224d8872e | ||
|
|
5cf2d5f062 | ||
|
|
92dd0ee1dd | ||
|
|
70c20fc76f | ||
|
|
07e0b5ac10 | ||
|
|
12f7fc3d33 | ||
|
|
60f47cb549 | ||
|
|
d33fa3f073 | ||
|
|
2e4fafcf46 | ||
|
|
ab451b9056 | ||
|
|
bc875a35b0 | ||
|
|
8e12a34634 | ||
|
|
77204127f2 | ||
|
|
65c2ccdc93 | ||
|
|
964d4f1f87 | ||
|
|
f559e9c74a | ||
|
|
610009528b | ||
|
|
0f10a88b23 | ||
|
|
60049c8cc5 | ||
|
|
50aa5880de | ||
|
|
47143318ba | ||
|
|
c27791a9ac | ||
|
|
b19d732a3a | ||
|
|
cd3db3a686 | ||
|
|
35fb9f8897 | ||
|
|
ec042434be | ||
|
|
f8041a3de5 | ||
|
|
dd90096e13 | ||
|
|
9ab5512bfa | ||
|
|
32ab56f864 | ||
|
|
78d7bfac01 | ||
|
|
57570c3ba6 | ||
|
|
ffac670f95 | ||
|
|
be16f1be44 | ||
|
|
fd0f85d565 | ||
|
|
8de5f3f0d3 | ||
|
|
0bb537b872 | ||
|
|
987da00be0 | ||
|
|
e9e2214d29 | ||
|
|
ac9f3317f1 | ||
|
|
7da85d277e | ||
|
|
274244b055 | ||
|
|
8fa611daed | ||
|
|
64d0fb17f7 | ||
|
|
6d1d844b14 | ||
|
|
686dd11d8e | ||
|
|
9d42ee9df8 | ||
|
|
9562768a04 | ||
|
|
54b8daede4 | ||
|
|
bd51afe86c | ||
|
|
a84b9bd2c8 | ||
|
|
ce1dac3b86 | ||
|
|
2dcd9f02cd | ||
|
|
416d57bec6 |
230
.github/workflows/build-macos-arm64.yml
vendored
230
.github/workflows/build-macos-arm64.yml
vendored
@@ -1,230 +0,0 @@
|
|||||||
name: Flutter Nightly MacOS Arm64 Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
#schedule:
|
|
||||||
# schedule build every night
|
|
||||||
# - cron: "0/6 * * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
|
||||||
CARGO_NDK_VERSION: "3.1.2"
|
|
||||||
LLVM_VERSION: "15.0.6"
|
|
||||||
FLUTTER_VERSION: "3.19.6"
|
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
|
||||||
# for arm64 linux because official Dart SDK does not work
|
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
|
||||||
TAG_NAME: "nightly"
|
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
|
||||||
# vcpkg version: 2024.03.25
|
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
|
||||||
VERSION: "1.2.5"
|
|
||||||
NDK_VERSION: "r26d"
|
|
||||||
#signing keys env variable checks
|
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
|
||||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
|
||||||
# To make a custom build with your own servers set the below secret values
|
|
||||||
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
|
||||||
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
|
||||||
API_SERVER: "${{ secrets.API_SERVER }}"
|
|
||||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
|
||||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-appimage:
|
|
||||||
name: Build image ${{ matrix.job.target }}
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- {
|
|
||||||
target: x86_64-unknown-linux-gnu,
|
|
||||||
arch: x86_64,
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
target: aarch64-unknown-linux-gnu,
|
|
||||||
arch: aarch64,
|
|
||||||
}
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Rename Binary
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y wget libarchive-tools
|
|
||||||
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
|
||||||
|
|
||||||
- name: Patch archlinux PKGBUILD
|
|
||||||
if: matrix.job.arch == 'x86_64'
|
|
||||||
run: |
|
|
||||||
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
|
|
||||||
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
|
|
||||||
sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD
|
|
||||||
fi
|
|
||||||
bsdtar -zxvf rustdesk-${{ env.VERSION }}.deb
|
|
||||||
tar -xvf ./data.tar.xz
|
|
||||||
case ${{ matrix.job.arch }} in
|
|
||||||
aarch64)
|
|
||||||
mkdir -p flutter/build/linux/arm64/release/bundle
|
|
||||||
cp -rf usr/lib/rustdesk/* flutter/build/linux/arm64/release/bundle/
|
|
||||||
;;
|
|
||||||
x86_64)
|
|
||||||
mkdir -p flutter/build/linux/x64/release/bundle
|
|
||||||
cp -rf usr/lib/rustdesk/* flutter/build/linux/x64/release/bundle/
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Build archlinux package
|
|
||||||
if: matrix.job.arch == 'x86_64'
|
|
||||||
uses: rustdesk-org/arch-makepkg-action@master
|
|
||||||
with:
|
|
||||||
packages: >
|
|
||||||
llvm
|
|
||||||
clang
|
|
||||||
libva
|
|
||||||
libvdpau
|
|
||||||
rust
|
|
||||||
gstreamer
|
|
||||||
unzip
|
|
||||||
git
|
|
||||||
cmake
|
|
||||||
gcc
|
|
||||||
curl
|
|
||||||
wget
|
|
||||||
nasm
|
|
||||||
zip
|
|
||||||
make
|
|
||||||
pkg-config
|
|
||||||
clang
|
|
||||||
gtk3
|
|
||||||
xdotool
|
|
||||||
libxcb
|
|
||||||
libxfixes
|
|
||||||
alsa-lib
|
|
||||||
pipewire
|
|
||||||
python
|
|
||||||
ttf-arphic-uming
|
|
||||||
libappindicator-gtk3
|
|
||||||
pam
|
|
||||||
gst-plugins-base
|
|
||||||
gst-plugin-pipewire
|
|
||||||
scripts: |
|
|
||||||
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
|
|
||||||
|
|
||||||
- name: Publish archlinux package
|
|
||||||
if: matrix.job.arch == 'x86_64'
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
res/rustdesk-${{ env.VERSION }}*.zst
|
|
||||||
|
|
||||||
- name: Build appimage package
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
# set-up appimage-builder
|
|
||||||
pushd /tmp
|
|
||||||
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
|
||||||
chmod +x appimage-builder-x86_64.AppImage
|
|
||||||
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
|
||||||
popd
|
|
||||||
# run appimage-builder
|
|
||||||
pushd appimage
|
|
||||||
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
|
|
||||||
|
|
||||||
- name: Publish appimage package
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
|
||||||
|
|
||||||
build-flatpak:
|
|
||||||
name: Build Flatpak ${{ matrix.job.target }}
|
|
||||||
runs-on: ${{ matrix.job.on }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- {
|
|
||||||
target: x86_64-unknown-linux-gnu,
|
|
||||||
distro: ubuntu18.04,
|
|
||||||
on: ubuntu-20.04,
|
|
||||||
arch: x86_64,
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
target: aarch64-unknown-linux-gnu,
|
|
||||||
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
|
|
||||||
distro: ubuntu22.04,
|
|
||||||
on: [self-hosted, Linux, ARM64],
|
|
||||||
arch: aarch64,
|
|
||||||
}
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Rename Binary
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y wget
|
|
||||||
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
|
||||||
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
|
||||||
id: rpm
|
|
||||||
with:
|
|
||||||
arch: ${{ matrix.job.arch }}
|
|
||||||
distro: ${{ matrix.job.distro }}
|
|
||||||
githubToken: ${{ github.token }}
|
|
||||||
setup: |
|
|
||||||
ls -l "${PWD}"
|
|
||||||
dockerRunArgs: |
|
|
||||||
--volume "${PWD}:/workspace"
|
|
||||||
shell: /bin/bash
|
|
||||||
install: |
|
|
||||||
apt-get update -y
|
|
||||||
apt-get install -y \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
rpm \
|
|
||||||
wget
|
|
||||||
run: |
|
|
||||||
# disable git safe.directory
|
|
||||||
git config --global --add safe.directory "*"
|
|
||||||
pushd /workspace
|
|
||||||
# install
|
|
||||||
apt-get update -y
|
|
||||||
apt-get install -y \
|
|
||||||
cmake \
|
|
||||||
curl \
|
|
||||||
flatpak \
|
|
||||||
flatpak-builder \
|
|
||||||
gcc \
|
|
||||||
git \
|
|
||||||
g++ \
|
|
||||||
libgtk-3-dev \
|
|
||||||
nasm \
|
|
||||||
wget
|
|
||||||
# flatpak deps
|
|
||||||
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
||||||
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
|
|
||||||
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
|
|
||||||
# package
|
|
||||||
pushd flatpak
|
|
||||||
git clone https://github.com/flathub/shared-modules.git --depth=1
|
|
||||||
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
|
||||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
|
|
||||||
|
|
||||||
- name: Publish flatpak package
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
|
||||||
18
.github/workflows/flutter-build.yml
vendored
18
.github/workflows/flutter-build.yml
vendored
@@ -11,10 +11,12 @@ on:
|
|||||||
default: "nightly"
|
default: "nightly"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
WIN_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
|
||||||
CARGO_NDK_VERSION: "3.1.2"
|
CARGO_NDK_VERSION: "3.1.2"
|
||||||
LLVM_VERSION: "15.0.6"
|
LLVM_VERSION: "15.0.6"
|
||||||
FLUTTER_VERSION: "3.19.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_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
# for arm64 linux because official Dart SDK does not work
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
@@ -22,7 +24,7 @@ env:
|
|||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.03.25
|
# vcpkg version: 2024.03.25
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||||
VERSION: "1.2.5"
|
VERSION: "1.2.6"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -84,7 +86,7 @@ jobs:
|
|||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.WIN_RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
@@ -810,7 +812,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||||
- uses: nttld/setup-ndk@v1
|
- uses: nttld/setup-ndk@v1
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
with:
|
with:
|
||||||
@@ -852,6 +854,13 @@ jobs:
|
|||||||
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
||||||
key: ${{ matrix.job.target }}
|
key: ${{ matrix.job.target }}
|
||||||
|
|
||||||
|
- name: fix android for flutter 3.13
|
||||||
|
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||||
|
run: |
|
||||||
|
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||||
|
cd flutter/lib
|
||||||
|
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
||||||
|
|
||||||
- name: Build rustdesk lib
|
- name: Build rustdesk lib
|
||||||
env:
|
env:
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
@@ -1564,6 +1573,7 @@ jobs:
|
|||||||
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
||||||
|
|
||||||
build-rustdesk-web:
|
build-rustdesk-web:
|
||||||
|
if: False
|
||||||
name: build-rustdesk-web
|
name: build-rustdesk-web
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
84
.github/workflows/history.yml
vendored
84
.github/workflows/history.yml
vendored
@@ -1,84 +0,0 @@
|
|||||||
name: Flutter Windows History Build
|
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
|
|
||||||
env:
|
|
||||||
LLVM_VERSION: "10.0"
|
|
||||||
FLUTTER_VERSION: "3.16.9"
|
|
||||||
TAG_NAME: "tmp"
|
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
|
||||||
VERSION: "1.2.5"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-for-history-windows:
|
|
||||||
name: ${{ matrix.job.date }}
|
|
||||||
runs-on: ${{ matrix.job.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ matrix.job.ref }}
|
|
||||||
|
|
||||||
- name: Install LLVM and Clang
|
|
||||||
uses: KyleMayes/install-llvm-action@v1
|
|
||||||
with:
|
|
||||||
version: ${{ env.LLVM_VERSION }}
|
|
||||||
|
|
||||||
- name: Install flutter
|
|
||||||
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
target: ${{ matrix.job.target }}
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
profile: minimal # minimal component installation (ie, no documentation)
|
|
||||||
|
|
||||||
- name: Install flutter rust bridge deps
|
|
||||||
run: |
|
|
||||||
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:
|
|
||||||
vcpkgDirectory: C:\vcpkg
|
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
|
||||||
|
|
||||||
- name: Install vcpkg dependencies
|
|
||||||
run: |
|
|
||||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build rustdesk
|
|
||||||
run: python3 .\build.py --portable --hwcodec --flutter
|
|
||||||
|
|
||||||
- name: Build self-extracted executable
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
pushd ./libs/portable
|
|
||||||
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
|
|
||||||
popd
|
|
||||||
mkdir -p ./SignOutput
|
|
||||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
|
|
||||||
|
|
||||||
- name: Publish Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
./SignOutput/rustdesk-*.exe
|
|
||||||
216
.github/workflows/playground.yml
vendored
Normal file
216
.github/workflows/playground.yml
vendored
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
name: playground
|
||||||
|
|
||||||
|
on:
|
||||||
|
#schedule:
|
||||||
|
# schedule build every night
|
||||||
|
# - cron: "0/6 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
CARGO_NDK_VERSION: "3.1.2"
|
||||||
|
LLVM_VERSION: "15.0.6"
|
||||||
|
FLUTTER_VERSION: "3.13.9"
|
||||||
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
|
# for arm64 linux because official Dart SDK does not work
|
||||||
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
|
TAG_NAME: "nightly"
|
||||||
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
|
# vcpkg version: 2024.03.25
|
||||||
|
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||||
|
VERSION: "1.2.6"
|
||||||
|
NDK_VERSION: "r26d"
|
||||||
|
#signing keys env variable checks
|
||||||
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||||
|
# To make a custom build with your own servers set the below secret values
|
||||||
|
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||||
|
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||||
|
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||||
|
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||||
|
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-rustdesk-android:
|
||||||
|
name: build rustdesk android apk ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
arch: aarch64,
|
||||||
|
target: aarch64-linux-android,
|
||||||
|
os: ubuntu-20.04,
|
||||||
|
openssl-arch: android-arm64,
|
||||||
|
ref: master, # latest
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.job.ref }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
clang \
|
||||||
|
cmake \
|
||||||
|
curl \
|
||||||
|
gcc-multilib \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
g++-multilib \
|
||||||
|
libappindicator3-dev \
|
||||||
|
libasound2-dev \
|
||||||
|
libc6-dev \
|
||||||
|
libclang-10-dev \
|
||||||
|
libgstreamer1.0-dev \
|
||||||
|
libgstreamer-plugins-base1.0-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libpulse-dev \
|
||||||
|
libva-dev \
|
||||||
|
libvdpau-dev \
|
||||||
|
libxcb-randr0-dev \
|
||||||
|
libxcb-shape0-dev \
|
||||||
|
libxcb-xfixes0-dev \
|
||||||
|
libxdo-dev \
|
||||||
|
libxfixes-dev \
|
||||||
|
llvm-10-dev \
|
||||||
|
nasm \
|
||||||
|
yasm \
|
||||||
|
ninja-build \
|
||||||
|
openjdk-11-jdk-headless \
|
||||||
|
pkg-config \
|
||||||
|
tree \
|
||||||
|
wget
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
components: "rustfmt"
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
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
|
||||||
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
|
- uses: nttld/setup-ndk@v1
|
||||||
|
id: setup-ndk
|
||||||
|
with:
|
||||||
|
ndk-version: ${{ env.NDK_VERSION }}
|
||||||
|
add-to-path: true
|
||||||
|
|
||||||
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
uses: lukka/run-vcpkg@v11
|
||||||
|
with:
|
||||||
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
run: |
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
./flutter/build_android_deps.sh arm64-v8a
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
./flutter/build_android_deps.sh armeabi-v7a
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Clone deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pushd /opt
|
||||||
|
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
|
||||||
|
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||||
|
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
|
||||||
|
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||||
|
|
||||||
|
- name: Build rustdesk lib
|
||||||
|
env:
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
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 }}
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
./flutter/ndk_arm64.sh
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
./flutter/ndk_arm.sh
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Build rustdesk
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||||
|
run: |
|
||||||
|
export PATH=/usr/lib/jvm/java-11-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
|
||||||
|
aarch64-linux-android)
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||||
|
# build flutter
|
||||||
|
pushd flutter
|
||||||
|
flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||||
|
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||||
|
# build flutter
|
||||||
|
pushd flutter
|
||||||
|
flutter build apk --release --target-platform android-arm --split-per-abi
|
||||||
|
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
popd
|
||||||
|
mkdir -p signed-apk; pushd signed-apk
|
||||||
|
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
|
||||||
|
|
||||||
|
- uses: r0adkll/sign-android-release@v1
|
||||||
|
name: Sign app APK
|
||||||
|
if: env.ANDROID_SIGNING_KEY != null
|
||||||
|
id: sign-rustdesk
|
||||||
|
with:
|
||||||
|
releaseDirectory: ./signed-apk
|
||||||
|
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ANDROID_ALIAS }}
|
||||||
|
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"
|
||||||
|
|
||||||
|
- name: Publish signed apk package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -4,9 +4,9 @@ on:
|
|||||||
types: [released]
|
types: [released]
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: windows-latest # action can only be run on windows
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
with:
|
with:
|
||||||
identifier: RustDesk.RustDesk
|
identifier: RustDesk.RustDesk
|
||||||
version: ${{ github.event.release.tag_name }}
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
|||||||
35
Cargo.lock
generated
35
Cargo.lock
generated
@@ -2917,7 +2917,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls 0.26.0",
|
"tokio-rustls 0.26.0",
|
||||||
"tokio-socks 0.5.2",
|
"tokio-socks 0.5.2-1",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
"url",
|
"url",
|
||||||
@@ -3037,8 +3037,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.4.15"
|
version = "0.4.18"
|
||||||
source = "git+https://github.com/21pages/hwcodec#1d504ee590c15472813fecc22cee4b8149b2b8cd"
|
source = "git+https://github.com/21pages/hwcodec#b84d5bbefa949194d1fc51a5c7e9d7988e315ce6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -4327,7 +4327,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "pam"
|
name = "pam"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/fufesou/pam#10da2cbbabe32cbc9de22a66abe44738e7ec0ea0"
|
source = "git+https://github.com/fufesou/pam#3a2aaa6e07b176d8e2d66a5eec38d2ddb45f009f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pam-macros",
|
"pam-macros",
|
||||||
@@ -4349,8 +4349,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "pam-sys"
|
name = "pam-sys"
|
||||||
version = "1.0.0-alpha4"
|
version = "1.0.0-alpha4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/fufesou/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7"
|
||||||
checksum = "5e9dfd42858f6a6bb1081079fd9dc259ca3e2aaece6cb689fd36b1058046c969"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -4383,8 +4382,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parity-tokio-ipc"
|
name = "parity-tokio-ipc"
|
||||||
version = "0.7.3-3"
|
version = "0.7.3-4"
|
||||||
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#e8448ade10d6d972d0b2307646424b36ab202ff5"
|
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#3623ec9ebef50c9b118e03b03df831008a4d1441"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -5289,7 +5288,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.5"
|
version = "1.2.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -5386,7 +5385,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.5"
|
version = "1.2.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -6128,7 +6127,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
source = "git+https://github.com/rustdesk-org/tao?branch=dev#1cad16b200485bbccc67dcee2d339eac6e1c16ad"
|
source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -6168,7 +6167,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tao-macros"
|
name = "tao-macros"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "git+https://github.com/rustdesk-org/tao?branch=dev#1cad16b200485bbccc67dcee2d339eac6e1c16ad"
|
source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.79",
|
"proc-macro2 1.0.79",
|
||||||
"quote 1.0.35",
|
"quote 1.0.35",
|
||||||
@@ -6359,9 +6358,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.37.0"
|
version = "1.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -6378,9 +6377,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.79",
|
"proc-macro2 1.0.79",
|
||||||
"quote 1.0.35",
|
"quote 1.0.35",
|
||||||
@@ -6432,8 +6431,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-socks"
|
name = "tokio-socks"
|
||||||
version = "0.5.2"
|
version = "0.5.2-1"
|
||||||
source = "git+https://github.com/rustdesk-org/tokio-socks#51037c93f8be34196fd2b6de9f674e8dfae3d01e"
|
source = "git+https://github.com/rustdesk-org/tokio-socks#94e97c6d7c93b0bcbfa54f2dc397c1da0a6e43d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"either",
|
"either",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.5"
|
version = "1.2.6"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.5
|
version: 1.2.6
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.5
|
version: 1.2.6
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -106,5 +106,6 @@ dependencies {
|
|||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||||
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,11 @@
|
|||||||
android:name=".MainService"
|
android:name=".MainService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".FloatingWindowService"
|
||||||
|
android:enabled="true" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Don't delete the meta-data below.
|
Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||||
|
|||||||
@@ -116,24 +116,20 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (!isSupportVoiceCall()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (isVideoStart() || isAudioStart()) {
|
// No need to check if video or audio is started here.
|
||||||
if (!switchToVoiceCall(mediaProjection)) {
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!switchToVoiceCall(mediaProjection)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
// Return true if not supported, because is was not started.
|
||||||
return false
|
if (!isSupportVoiceCall()) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if (isVideoStart()) {
|
if (isVideoStart()) {
|
||||||
switchOutVoiceCall(mediaProjection)
|
switchOutVoiceCall(mediaProjection)
|
||||||
@@ -180,9 +176,6 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun tryReleaseAudio() {
|
fun tryReleaseAudio() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isAudioStart() || isVideoStart()) {
|
if (isAudioStart() || isVideoStart()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import com.caverock.androidsvg.SVG
|
||||||
|
import ffi.FFI
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class FloatingWindowService : Service(), View.OnTouchListener {
|
||||||
|
|
||||||
|
private lateinit var windowManager: WindowManager
|
||||||
|
private lateinit var layoutParams: WindowManager.LayoutParams
|
||||||
|
private lateinit var floatingView: ImageView
|
||||||
|
private lateinit var originalDrawable: Drawable
|
||||||
|
private lateinit var leftHalfDrawable: Drawable
|
||||||
|
private lateinit var rightHalfDrawable: Drawable
|
||||||
|
|
||||||
|
private var dragging = false
|
||||||
|
private var lastDownX = 0f
|
||||||
|
private var lastDownY = 0f
|
||||||
|
private var viewCreated = false;
|
||||||
|
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logTag = "floatingService"
|
||||||
|
private var firstCreate = true
|
||||||
|
private var viewWidth = 120
|
||||||
|
private var viewHeight = 120
|
||||||
|
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
|
||||||
|
private const val MAX_VIEW_SIZE = 320
|
||||||
|
private var viewUntouchable = false
|
||||||
|
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
|
||||||
|
private var customSvg = ""
|
||||||
|
private var lastLayoutX = 0
|
||||||
|
private var lastLayoutY = 0
|
||||||
|
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
try {
|
||||||
|
if (firstCreate) {
|
||||||
|
firstCreate = false
|
||||||
|
onFirstCreate(windowManager)
|
||||||
|
}
|
||||||
|
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
|
||||||
|
createView(windowManager)
|
||||||
|
handler.postDelayed(runnable, 1000)
|
||||||
|
Log.d(logTag, "onCreate success")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "onCreate failed: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (viewCreated) {
|
||||||
|
windowManager.removeView(floatingView)
|
||||||
|
}
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun createView(windowManager: WindowManager) {
|
||||||
|
floatingView = ImageView(this)
|
||||||
|
viewCreated = true
|
||||||
|
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
|
||||||
|
if (customSvg.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val svg = SVG.getFromString(customSvg)
|
||||||
|
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
|
||||||
|
// This make the svg render clear
|
||||||
|
svg.documentWidth = viewWidth * 1f
|
||||||
|
svg.documentHeight = viewHeight * 1f
|
||||||
|
originalDrawable = svg.renderToPicture().let {
|
||||||
|
BitmapDrawable(
|
||||||
|
resources,
|
||||||
|
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
|
||||||
|
.also { bitmap ->
|
||||||
|
it.draw(Canvas(bitmap))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||||
|
Log.d(logTag, "custom svg loaded")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val originalBitmap = Bitmap.createBitmap(
|
||||||
|
originalDrawable.intrinsicWidth,
|
||||||
|
originalDrawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(originalBitmap)
|
||||||
|
originalDrawable.setBounds(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
originalDrawable.draw(canvas)
|
||||||
|
val leftHalfBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
val rightHalfBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
|
||||||
|
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
|
||||||
|
|
||||||
|
floatingView.setImageDrawable(rightHalfDrawable)
|
||||||
|
floatingView.setOnTouchListener(this)
|
||||||
|
floatingView.alpha = viewTransparency * 1f
|
||||||
|
|
||||||
|
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
|
||||||
|
if (viewUntouchable || viewTransparency == 0f) {
|
||||||
|
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||||
|
}
|
||||||
|
layoutParams = WindowManager.LayoutParams(
|
||||||
|
viewWidth / 2,
|
||||||
|
viewHeight,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
|
||||||
|
flags,
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
)
|
||||||
|
|
||||||
|
layoutParams.gravity = Gravity.TOP or Gravity.START
|
||||||
|
layoutParams.x = lastLayoutX
|
||||||
|
layoutParams.y = lastLayoutY
|
||||||
|
|
||||||
|
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
|
||||||
|
keepScreenOn = when (keepScreenOnOption) {
|
||||||
|
"never" -> KeepScreenOn.NEVER
|
||||||
|
"service-on" -> KeepScreenOn.SERVICE_ON
|
||||||
|
else -> KeepScreenOn.DURING_CONTROLLED
|
||||||
|
}
|
||||||
|
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
|
||||||
|
updateKeepScreenOnLayoutParams()
|
||||||
|
|
||||||
|
windowManager.addView(floatingView, layoutParams)
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFirstCreate(windowManager: WindowManager) {
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
val w = wh.first
|
||||||
|
val h = wh.second
|
||||||
|
// size
|
||||||
|
FFI.getLocalOption("floating-window-size").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val size = it.toInt()
|
||||||
|
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
|
||||||
|
viewWidth = size
|
||||||
|
viewHeight = size
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// untouchable
|
||||||
|
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
|
||||||
|
// transparency
|
||||||
|
FFI.getLocalOption("floating-window-transparency").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val transparency = it.toInt()
|
||||||
|
if (transparency in 0..10) {
|
||||||
|
viewTransparency = transparency * 1f / 10
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// custom svg
|
||||||
|
FFI.getLocalOption("floating-window-svg").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
customSvg = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// position
|
||||||
|
lastLayoutX = 0
|
||||||
|
lastLayoutY = (wh.second - viewHeight) / 2
|
||||||
|
lastOrientation = resources.configuration.orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun performClick() {
|
||||||
|
showPopupMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||||
|
when (event?.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
dragging = false
|
||||||
|
lastDownX = event.rawX
|
||||||
|
lastDownY = event.rawY
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
val clickDragTolerance = 10f
|
||||||
|
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
|
||||||
|
performClick()
|
||||||
|
} else {
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val dx = event.rawX - lastDownX
|
||||||
|
val dy = event.rawY - lastDownY
|
||||||
|
// ignore too small fist start moving(some time is click)
|
||||||
|
if (!dragging && dx*dx+dy*dy < 25) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
layoutParams.x = event.rawX.toInt()
|
||||||
|
layoutParams.y = event.rawY.toInt()
|
||||||
|
layoutParams.width = viewWidth
|
||||||
|
floatingView.setImageDrawable(originalDrawable)
|
||||||
|
windowManager.updateViewLayout(view, layoutParams)
|
||||||
|
lastLayoutX = layoutParams.x
|
||||||
|
lastLayoutY = layoutParams.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveToScreenSide(center: Boolean = false) {
|
||||||
|
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
val w = wh.first
|
||||||
|
if (layoutParams.x < w / 2) {
|
||||||
|
layoutParams.x = 0
|
||||||
|
floatingView.setImageDrawable(rightHalfDrawable)
|
||||||
|
} else {
|
||||||
|
layoutParams.x = w - viewWidth / 2
|
||||||
|
floatingView.setImageDrawable(leftHalfDrawable)
|
||||||
|
}
|
||||||
|
if (center) {
|
||||||
|
layoutParams.y = (wh.second - viewHeight) / 2
|
||||||
|
}
|
||||||
|
layoutParams.width = viewWidth / 2
|
||||||
|
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||||
|
lastLayoutX = layoutParams.x
|
||||||
|
lastLayoutY = layoutParams.y
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
if (newConfig.orientation != lastOrientation) {
|
||||||
|
lastOrientation = newConfig.orientation
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
|
||||||
|
val newW = wh.first
|
||||||
|
val newH = wh.second
|
||||||
|
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
|
// Proportional change
|
||||||
|
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
|
||||||
|
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
|
||||||
|
}
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPopupMenu() {
|
||||||
|
val popupMenu = PopupMenu(this, floatingView)
|
||||||
|
val idShowRustDesk = 0
|
||||||
|
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
||||||
|
val idStopService = 1
|
||||||
|
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||||
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
idShowRustDesk -> {
|
||||||
|
openMainActivity()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
idStopService -> {
|
||||||
|
stopMainService()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupMenu.setOnDismissListener {
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openMainActivity() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
pendingIntent.send()
|
||||||
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopMainService() {
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class KeepScreenOn {
|
||||||
|
NEVER,
|
||||||
|
DURING_CONTROLLED,
|
||||||
|
SERVICE_ON,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (updateKeepScreenOnLayoutParams()) {
|
||||||
|
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateKeepScreenOnLayoutParams(): Boolean {
|
||||||
|
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
|
||||||
|
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
|
||||||
|
if (oldOn != newOn) {
|
||||||
|
Log.d(logTag, "change keep screen on to $newOn")
|
||||||
|
if (newOn) {
|
||||||
|
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
|
||||||
|
} else {
|
||||||
|
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -233,6 +233,17 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GET_VALUE -> {
|
||||||
|
if (call.arguments is String) {
|
||||||
|
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
||||||
|
result.success(isSupportVoiceCall())
|
||||||
|
} else {
|
||||||
|
result.error("-1", "No such key", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
"on_voice_call_started" -> {
|
"on_voice_call_started" -> {
|
||||||
onVoiceCallStarted()
|
onVoiceCallStarted()
|
||||||
}
|
}
|
||||||
@@ -252,19 +263,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
val codecArray = JSONArray()
|
val codecArray = JSONArray()
|
||||||
|
|
||||||
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
var w = 0
|
val wh = getScreenSize(windowManager)
|
||||||
var h = 0
|
var w = wh.first
|
||||||
@Suppress("DEPRECATION")
|
var h = wh.second
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val m = windowManager.maximumWindowMetrics
|
|
||||||
w = m.bounds.width()
|
|
||||||
h = m.bounds.height()
|
|
||||||
} else {
|
|
||||||
val dm = DisplayMetrics()
|
|
||||||
windowManager.defaultDisplay.getRealMetrics(dm)
|
|
||||||
w = dm.widthPixels
|
|
||||||
h = dm.heightPixels
|
|
||||||
}
|
|
||||||
val align = 64
|
val align = 64
|
||||||
w = (w + align - 1) / align * align
|
w = (w + align - 1) / align * align
|
||||||
h = (h + align - 1) / align * align
|
h = (h + align - 1) / align * align
|
||||||
@@ -374,4 +375,17 @@ class MainActivity : FlutterActivity() {
|
|||||||
Log.d(logTag, "onVoiceCallClosed success")
|
Log.d(logTag, "onVoiceCallClosed success")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
|
||||||
|
if (!disableFloatingWindow && MainService.isReady) {
|
||||||
|
startService(Intent(this, FloatingWindowService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ class MainService : Service() {
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
|
||||||
// turn on screen with LIFT_DOWN when screen off
|
// turn on screen with LIFT_DOWN when screen off
|
||||||
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
|
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
Log.d(logTag, "Turn on Screen, WakeLock release")
|
Log.d(logTag, "Turn on Screen, WakeLock release")
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
@@ -75,10 +75,10 @@ class MainService : Service() {
|
|||||||
wakeLock.acquire(5000)
|
wakeLock.acquire(5000)
|
||||||
} else {
|
} else {
|
||||||
when (kind) {
|
when (kind) {
|
||||||
"touch" -> {
|
0 -> { // touch
|
||||||
InputService.ctx?.onTouchInput(mask, x, y)
|
InputService.ctx?.onTouchInput(mask, x, y)
|
||||||
}
|
}
|
||||||
"mouse" -> {
|
1 -> { // mouse
|
||||||
InputService.ctx?.onMouseInput(mask, x, y)
|
InputService.ctx?.onMouseInput(mask, x, y)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -103,6 +103,9 @@ class MainService : Service() {
|
|||||||
put("scale",SCREEN_INFO.scale)
|
put("scale",SCREEN_INFO.scale)
|
||||||
}.toString()
|
}.toString()
|
||||||
}
|
}
|
||||||
|
"is_start" -> {
|
||||||
|
isStart.toString()
|
||||||
|
}
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,10 +175,10 @@ class MainService : Service() {
|
|||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
stopCapture()
|
stopCapture()
|
||||||
}
|
}
|
||||||
"is_hardware_codec" -> {
|
"half_scale" -> {
|
||||||
val isHwCodec = arg1.toBoolean()
|
val halfScale = arg1.toBoolean()
|
||||||
if (isHardwareCodec != isHwCodec) {
|
if (isHalfScale != halfScale) {
|
||||||
isHardwareCodec = isHwCodec
|
isHalfScale = halfScale
|
||||||
updateScreenInfo(resources.configuration.orientation)
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +194,6 @@ class MainService : Service() {
|
|||||||
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||||
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
||||||
|
|
||||||
private fun translate(input: String): String {
|
|
||||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
|
||||||
return FFI.translateLocale(LOCAL_NAME, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _isReady = false // media permission ready status
|
private var _isReady = false // media permission ready status
|
||||||
private var _isStart = false // screen capture start status
|
private var _isStart = false // screen capture start status
|
||||||
@@ -252,10 +250,11 @@ class MainService : Service() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isHardwareCodec: Boolean? = null;
|
private var isHalfScale: Boolean? = null;
|
||||||
private fun updateScreenInfo(orientation: Int) {
|
private fun updateScreenInfo(orientation: Int) {
|
||||||
var w: Int
|
var w: Int
|
||||||
var h: Int
|
var h: Int
|
||||||
@@ -288,7 +287,7 @@ class MainService : Service() {
|
|||||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||||
var scale = 1
|
var scale = 1
|
||||||
if (w != 0 && h != 0) {
|
if (w != 0 && h != 0) {
|
||||||
if (isHardwareCodec == false && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||||
scale = 2
|
scale = 2
|
||||||
w /= scale
|
w /= scale
|
||||||
h /= scale
|
h /= scale
|
||||||
@@ -486,6 +485,7 @@ class MainService : Service() {
|
|||||||
mediaProjection = null
|
mediaProjection = null
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ import android.os.Looper
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.*
|
import android.provider.Settings.*
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import ffi.FFI
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
|
|||||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||||
|
const val GET_VALUE = "get_value"
|
||||||
|
|
||||||
|
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
||||||
|
|
||||||
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
||||||
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
||||||
@@ -56,6 +63,11 @@ data class Info(
|
|||||||
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun isSupportVoiceCall(): Boolean {
|
||||||
|
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
}
|
||||||
|
|
||||||
fun requestPermission(context: Context, type: String) {
|
fun requestPermission(context: Context, type: String) {
|
||||||
XXPermissions.with(context)
|
XXPermissions.with(context)
|
||||||
.permission(type)
|
.permission(type)
|
||||||
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val m = windowManager.maximumWindowMetrics
|
||||||
|
w = m.bounds.width()
|
||||||
|
h = m.bounds.height()
|
||||||
|
} else {
|
||||||
|
val dm = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||||
|
w = dm.widthPixels
|
||||||
|
h = dm.heightPixels
|
||||||
|
}
|
||||||
|
return Pair(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(input: String): String {
|
||||||
|
Log.d("common", "translate:$LOCAL_NAME")
|
||||||
|
return FFI.translateLocale(LOCAL_NAME, input)
|
||||||
|
}
|
||||||
@@ -19,4 +19,5 @@ object FFI {
|
|||||||
external fun refreshScreen()
|
external fun refreshScreen()
|
||||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
external fun setCodecInfo(info: String)
|
external fun setCodecInfo(info: String)
|
||||||
|
external fun getLocalOption(key: String): String
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
<key>com.apple.developer.networking.wifi-info</key>
|
<key>com.apple.developer.networking.wifi-info</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo build --features flutter --release --target aarch64-apple-ios --lib
|
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||||
|
|||||||
@@ -718,7 +718,21 @@ class OverlayDialogManager {
|
|||||||
int _tagCount = 0;
|
int _tagCount = 0;
|
||||||
|
|
||||||
OverlayEntry? _mobileActionsOverlayEntry;
|
OverlayEntry? _mobileActionsOverlayEntry;
|
||||||
RxBool mobileActionsOverlayVisible = false.obs;
|
RxBool mobileActionsOverlayVisible = true.obs;
|
||||||
|
|
||||||
|
setMobileActionsOverlayVisible(bool v, {store = true}) {
|
||||||
|
if (store) {
|
||||||
|
bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
|
||||||
|
}
|
||||||
|
// No need to read the value from local storage after setting it.
|
||||||
|
// It better to toggle the value directly.
|
||||||
|
mobileActionsOverlayVisible.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMobileActionsOverlayVisible() {
|
||||||
|
mobileActionsOverlayVisible.value =
|
||||||
|
bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
|
||||||
|
}
|
||||||
|
|
||||||
void setOverlayState(OverlayKeyState overlayKeyState) {
|
void setOverlayState(OverlayKeyState overlayKeyState) {
|
||||||
_overlayKeyState = overlayKeyState;
|
_overlayKeyState = overlayKeyState;
|
||||||
@@ -865,14 +879,14 @@ class OverlayDialogManager {
|
|||||||
);
|
);
|
||||||
overlayState.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
_mobileActionsOverlayEntry = overlay;
|
_mobileActionsOverlayEntry = overlay;
|
||||||
mobileActionsOverlayVisible.value = true;
|
setMobileActionsOverlayVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void hideMobileActionsOverlay() {
|
void hideMobileActionsOverlay({store = true}) {
|
||||||
if (_mobileActionsOverlayEntry != null) {
|
if (_mobileActionsOverlayEntry != null) {
|
||||||
_mobileActionsOverlayEntry!.remove();
|
_mobileActionsOverlayEntry!.remove();
|
||||||
_mobileActionsOverlayEntry = null;
|
_mobileActionsOverlayEntry = null;
|
||||||
mobileActionsOverlayVisible.value = false;
|
setMobileActionsOverlayVisible(false, store: store);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -891,21 +905,27 @@ class OverlayDialogManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||||
final position = SimpleWrapper(Offset(0, 0));
|
|
||||||
makeMobileActions(BuildContext context, double s) {
|
makeMobileActions(BuildContext context, double s) {
|
||||||
final scale = s < 0.85 ? 0.85 : s;
|
final scale = s < 0.85 ? 0.85 : s;
|
||||||
final session = ffi ?? gFFI;
|
final session = ffi ?? gFFI;
|
||||||
// compute overlay position
|
|
||||||
final screenW = MediaQuery.of(context).size.width;
|
|
||||||
final screenH = MediaQuery.of(context).size.height;
|
|
||||||
const double overlayW = 200;
|
const double overlayW = 200;
|
||||||
const double overlayH = 45;
|
const double overlayH = 45;
|
||||||
final left = (screenW - overlayW * scale) / 2;
|
computeOverlayPosition() {
|
||||||
final top = screenH - (overlayH + 80) * scale;
|
final screenW = MediaQuery.of(context).size.width;
|
||||||
position.value = Offset(left, top);
|
final screenH = MediaQuery.of(context).size.height;
|
||||||
|
final left = (screenW - overlayW * scale) / 2;
|
||||||
|
final top = screenH - (overlayH + 80) * scale;
|
||||||
|
return Offset(left, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggablePositions.mobileActions.isInvalid()) {
|
||||||
|
draggablePositions.mobileActions.update(computeOverlayPosition());
|
||||||
|
} else {
|
||||||
|
draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
|
||||||
|
}
|
||||||
return DraggableMobileActions(
|
return DraggableMobileActions(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
position: position,
|
position: draggablePositions.mobileActions,
|
||||||
width: overlayW,
|
width: overlayW,
|
||||||
height: overlayH,
|
height: overlayH,
|
||||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
||||||
@@ -1008,7 +1028,8 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||||
} else if (!tabTapped &&
|
} else if (!tabTapped &&
|
||||||
onSubmit != null &&
|
onSubmit != null &&
|
||||||
(key.logicalKey == LogicalKeyboardKey.enter || key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
(key.logicalKey == LogicalKeyboardKey.enter ||
|
||||||
|
key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
||||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
@@ -1421,14 +1442,16 @@ String translate(String name) {
|
|||||||
return platformFFI.translate(name, localeName);
|
return platformFFI.translate(name, localeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function must be kept the same as the one in rust and sciter code.
|
||||||
|
// rust: libs/hbb_common/src/config.rs -> option2bool()
|
||||||
|
// sciter: Does not have the function, but it should be kept the same.
|
||||||
bool option2bool(String option, String value) {
|
bool option2bool(String option, String value) {
|
||||||
bool res;
|
bool res;
|
||||||
if (option.startsWith("enable-")) {
|
if (option.startsWith("enable-")) {
|
||||||
res = value != "N";
|
res = value != "N";
|
||||||
} else if (option.startsWith("allow-") ||
|
} else if (option.startsWith("allow-") ||
|
||||||
option == "stop-service" ||
|
option == kOptionStopService ||
|
||||||
option == kOptionDirectServer ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = value == "Y";
|
res = value == "Y";
|
||||||
} else {
|
} else {
|
||||||
@@ -1443,9 +1466,8 @@ String bool2option(String option, bool b) {
|
|||||||
if (option.startsWith('enable-')) {
|
if (option.startsWith('enable-')) {
|
||||||
res = b ? defaultOptionYes : 'N';
|
res = b ? defaultOptionYes : 'N';
|
||||||
} else if (option.startsWith('allow-') ||
|
} else if (option.startsWith('allow-') ||
|
||||||
option == "stop-service" ||
|
option == kOptionStopService ||
|
||||||
option == kOptionDirectServer ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = b ? 'Y' : defaultOptionNo;
|
res = b ? 'Y' : defaultOptionNo;
|
||||||
} else {
|
} else {
|
||||||
@@ -1481,9 +1503,9 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
|
|||||||
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
|
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
|
||||||
}
|
}
|
||||||
|
|
||||||
mainSetPeerBoolOptionSync(String id, String key, bool v) {
|
// Don't use `option2bool()` and `bool2option()` to convert the session option.
|
||||||
bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v));
|
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
|
||||||
}
|
// Because all session options use `Y` and `<Empty>` as values.
|
||||||
|
|
||||||
Future<bool> matchPeer(String searchText, Peer peer) async {
|
Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||||
if (searchText.isEmpty) {
|
if (searchText.isEmpty) {
|
||||||
@@ -2668,27 +2690,40 @@ Future<void> start_service(bool is_start) async {
|
|||||||
!isMacOS ||
|
!isMacOS ||
|
||||||
await callMainCheckSuperUserPermission();
|
await callMainCheckSuperUserPermission();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
|
mainSetBoolOption(kOptionStopService, !is_start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> canBeBlocked() async {
|
||||||
|
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||||
|
var option = option2bool(kOptionAllowRemoteConfigModification,
|
||||||
|
await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
|
||||||
|
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
|
||||||
|
if (use != null && !await use()) {
|
||||||
|
block.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var time0 = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
await bind.mainCheckMouseTime();
|
||||||
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
|
var d = time0 - await bind.mainGetMouseTime();
|
||||||
|
if (d < 120) {
|
||||||
|
block.value = true;
|
||||||
|
} else {
|
||||||
|
block.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
||||||
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
||||||
var block = false.obs;
|
var block = false.obs;
|
||||||
return Obx(() => MouseRegion(
|
return Obx(() => MouseRegion(
|
||||||
onEnter: (_) async {
|
onEnter: (_) async {
|
||||||
if (use != null && !await use()) {
|
await shouldBeBlocked(block, use);
|
||||||
block.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var time0 = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
await bind.mainCheckMouseTime();
|
|
||||||
Timer(const Duration(milliseconds: 120), () async {
|
|
||||||
var d = time0 - await bind.mainGetMouseTime();
|
|
||||||
if (d < 120) {
|
|
||||||
block.value = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onExit: (event) => block.value = false,
|
onExit: (event) => block.value = false,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
@@ -2745,12 +2780,10 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
required RxString err,
|
required RxString err,
|
||||||
required Function? retry,
|
required Function? retry,
|
||||||
required Function close}) {
|
required Function close}) {
|
||||||
const double height = 25;
|
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
offstage: !(!loading.value && err.value.isNotEmpty),
|
offstage: !(!loading.value && err.value.isNotEmpty),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: height,
|
|
||||||
color: MyTheme.color(context).errorBannerBg,
|
color: MyTheme.color(context).errorBannerBg,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -2769,7 +2802,6 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
message: translate(err.value),
|
message: translate(err.value),
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(err.value),
|
translate(err.value),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
)).marginSymmetric(vertical: 2),
|
)).marginSymmetric(vertical: 2),
|
||||||
),
|
),
|
||||||
@@ -2922,10 +2954,11 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
|||||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewConnectWindowFrame(int windowId, String peerId, Rect? screenRect) async {
|
setNewConnectWindowFrame(
|
||||||
|
int windowId, String peerId, int? display, Rect? screenRect) async {
|
||||||
if (screenRect == null) {
|
if (screenRect == null) {
|
||||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||||
windowId: windowId, peerId: peerId);
|
windowId: windowId, display: display, peerId: peerId);
|
||||||
} else {
|
} else {
|
||||||
await tryMoveToScreenAndSetFullscreen(screenRect);
|
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||||
}
|
}
|
||||||
@@ -3372,3 +3405,27 @@ get defaultOptionNo => isCustomClient ? 'N' : '';
|
|||||||
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
||||||
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||||
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
||||||
|
|
||||||
|
// `setMovable()` is only supported on macOS.
|
||||||
|
//
|
||||||
|
// On macOS, the window can be dragged by the tab bar by default.
|
||||||
|
// We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
|
||||||
|
//
|
||||||
|
// When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
|
||||||
|
//
|
||||||
|
// See the following code for more details:
|
||||||
|
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
|
||||||
|
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
|
||||||
|
//
|
||||||
|
// @platforms macos
|
||||||
|
disableWindowMovable(int? windowId) {
|
||||||
|
if (!isMacOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowId == null) {
|
||||||
|
windowManager.setMovable(false);
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(windowId).setMovable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddressBookMobile() {
|
Widget _buildAddressBookMobile() {
|
||||||
|
const padding = 8.0;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -120,7 +121,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.background)),
|
color: Theme.of(context).colorScheme.background)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding:
|
||||||
|
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -130,7 +132,6 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: _buildTags(),
|
child: _buildTags(),
|
||||||
),
|
),
|
||||||
_buildAbPermission(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -198,24 +199,28 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (contains) {
|
if (contains) {
|
||||||
names.insert(0, personalAddressBookName);
|
names.insert(0, personalAddressBookName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row buildItem(String e, {bool button = false}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: gFFI.abModel.translatedName(e),
|
||||||
|
child: Text(
|
||||||
|
gFFI.abModel.translatedName(e),
|
||||||
|
style: button ? null : TextStyle(fontSize: 14.0),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: button ? TextAlign.center : null,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final items = names
|
final items = names
|
||||||
.map((e) => DropdownMenuItem(
|
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
|
||||||
value: e,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Tooltip(
|
|
||||||
waitDuration: Duration(milliseconds: 500),
|
|
||||||
message: gFFI.abModel.translatedName(e),
|
|
||||||
child: Text(
|
|
||||||
gFFI.abModel.translatedName(e),
|
|
||||||
style: TextStyle(fontSize: 14.0),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)))
|
|
||||||
.toList();
|
.toList();
|
||||||
var menuItemStyleData = MenuItemStyleData(height: 36);
|
var menuItemStyleData = MenuItemStyleData(height: 36);
|
||||||
if (contains && items.length > 1) {
|
if (contains && items.length > 1) {
|
||||||
@@ -237,14 +242,22 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
customButton: Container(
|
||||||
|
height: isDesktop ? 48 : 40,
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||||
|
Icon(Icons.arrow_drop_down),
|
||||||
|
]),
|
||||||
|
),
|
||||||
underline: Container(
|
underline: Container(
|
||||||
height: 0.7,
|
height: 0.7,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
buttonStyleData: ButtonStyleData(height: 48),
|
|
||||||
menuItemStyleData: menuItemStyleData,
|
menuItemStyleData: menuItemStyleData,
|
||||||
items: items,
|
items: items,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
isDense: true,
|
||||||
dropdownSearchData: DropdownSearchData(
|
dropdownSearchData: DropdownSearchData(
|
||||||
searchController: textEditingController,
|
searchController: textEditingController,
|
||||||
searchInnerWidgetHeight: 50,
|
searchInnerWidgetHeight: 50,
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: This debounce logic is not working properly.
|
||||||
|
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
|
||||||
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
||||||
start(ScaleUpdateDetails d) {
|
start(ScaleUpdateDetails d) {
|
||||||
_currentState = GestureState.oneFingerPan;
|
_currentState = GestureState.oneFingerPan;
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ Future<bool?> loginDialog() async {
|
|||||||
}
|
}
|
||||||
if (isEmailVerification != null) {
|
if (isEmailVerification != null) {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (close != null) close(false);
|
if (close != null) close(null);
|
||||||
verificationCodeDialog(
|
verificationCodeDialog(
|
||||||
resp.user, resp.secret, isEmailVerification);
|
resp.user, resp.secret, isEmailVerification);
|
||||||
} else {
|
} else {
|
||||||
@@ -712,6 +712,11 @@ Future<bool?> verificationCodeDialog(
|
|||||||
dialogButton("Verify", onPressed: getOnSubmit()),
|
dialogButton("Verify", onPressed: getOnSubmit()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
|
||||||
|
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
|
||||||
|
if (isMobile && res == true) {
|
||||||
|
await UserModel.updateOtherModels();
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (draggablePositions.chatWindow.isInvalid()) {
|
||||||
|
draggablePositions.chatWindow.update(position);
|
||||||
|
}
|
||||||
return isIOS
|
return isIOS
|
||||||
? IOSDraggable(
|
? IOSDraggable(
|
||||||
position: position,
|
position: draggablePositions.chatWindow,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Draggable(
|
: Draggable(
|
||||||
checkKeyboard: true,
|
checkKeyboard: true,
|
||||||
position: SimpleWrapper(position),
|
position: draggablePositions.chatWindow,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
@@ -176,7 +181,7 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
required this.scale});
|
required this.scale});
|
||||||
|
|
||||||
final double scale;
|
final double scale;
|
||||||
final SimpleWrapper<Offset> position;
|
final DraggableKeyPosition position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
@@ -241,6 +246,92 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DraggableKeyPosition {
|
||||||
|
final String key;
|
||||||
|
Offset _pos;
|
||||||
|
late Debouncer<int> _debouncerStore;
|
||||||
|
DraggableKeyPosition(this.key)
|
||||||
|
: _pos = DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
|
||||||
|
get pos => _pos;
|
||||||
|
|
||||||
|
_loadPosition(String k) {
|
||||||
|
final value = bind.getLocalFlutterOption(k: k);
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
final parts = value.split(',');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
return Offset(double.parse(parts[0]), double.parse(parts[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
_pos = _loadPosition(key);
|
||||||
|
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
|
||||||
|
onChanged: (v) => _store(), initialValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(Offset pos) {
|
||||||
|
_pos = pos;
|
||||||
|
_triggerStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust position to keep it in the screen
|
||||||
|
// Only used for desktop and web desktop
|
||||||
|
tryAdjust(double w, double h, double scale) {
|
||||||
|
final size = MediaQuery.of(Get.context!).size;
|
||||||
|
w = w * scale;
|
||||||
|
h = h * scale;
|
||||||
|
double x = _pos.dx;
|
||||||
|
double y = _pos.dy;
|
||||||
|
if (x + w > size.width) {
|
||||||
|
x = size.width - w;
|
||||||
|
}
|
||||||
|
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
|
||||||
|
if (y + h > (size.height - tabBarHeight)) {
|
||||||
|
y = size.height - tabBarHeight - h;
|
||||||
|
}
|
||||||
|
if (x < 0) {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
if (x != _pos.dx || y != _pos.dy) {
|
||||||
|
update(Offset(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInvalid() {
|
||||||
|
return _pos == DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
|
||||||
|
_store() {
|
||||||
|
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraggablePositions {
|
||||||
|
static const kChatWindow = 'draggablePositionChat';
|
||||||
|
static const kMobileActions = 'draggablePositionMobile';
|
||||||
|
static const kIOSDraggable = 'draggablePositionIOS';
|
||||||
|
|
||||||
|
static const kInvalidDraggablePosition = Offset(-999999, -999999);
|
||||||
|
final chatWindow = DraggableKeyPosition(kChatWindow);
|
||||||
|
final mobileActions = DraggableKeyPosition(kMobileActions);
|
||||||
|
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
|
||||||
|
|
||||||
|
load() {
|
||||||
|
chatWindow.load();
|
||||||
|
mobileActions.load();
|
||||||
|
iOSDraggable.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DraggablePositions draggablePositions = DraggablePositions();
|
||||||
|
|
||||||
class Draggable extends StatefulWidget {
|
class Draggable extends StatefulWidget {
|
||||||
Draggable(
|
Draggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
@@ -255,7 +346,7 @@ class Draggable extends StatefulWidget {
|
|||||||
|
|
||||||
final bool checkKeyboard;
|
final bool checkKeyboard;
|
||||||
final bool checkScreenSize;
|
final bool checkScreenSize;
|
||||||
final SimpleWrapper<Offset> position;
|
final DraggableKeyPosition position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
@@ -277,7 +368,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
_chatModel = widget.chatModel;
|
_chatModel = widget.chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
get position => widget.position.value;
|
get position => widget.position.pos;
|
||||||
|
|
||||||
void onPanUpdate(DragUpdateDetails d) {
|
void onPanUpdate(DragUpdateDetails d) {
|
||||||
final offset = d.delta;
|
final offset = d.delta;
|
||||||
@@ -301,7 +392,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
y = position.dy + offset.dy;
|
y = position.dy + offset.dy;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.position.value = Offset(x, y);
|
widget.position.update(Offset(x, y));
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(position);
|
_chatModel?.setChatWindowPosition(position);
|
||||||
}
|
}
|
||||||
@@ -320,7 +411,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.position.value = Offset(position.dx, _saveHeight);
|
widget.position.update(Offset(position.dx, _saveHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +422,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
if (sumHeight + position.dy > contextHeight) {
|
if (sumHeight + position.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.position.value = Offset(position.dx, y);
|
widget.position.update(Offset(position.dx, y));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,14 +453,14 @@ class _DraggableState extends State<Draggable> {
|
|||||||
class IOSDraggable extends StatefulWidget {
|
class IOSDraggable extends StatefulWidget {
|
||||||
const IOSDraggable(
|
const IOSDraggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.position = Offset.zero,
|
|
||||||
this.chatModel,
|
this.chatModel,
|
||||||
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
required this.builder})
|
required this.builder})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final Offset position;
|
final DraggableKeyPosition position;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
@@ -380,7 +471,6 @@ class IOSDraggable extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class IOSDraggableState extends State<IOSDraggable> {
|
class IOSDraggableState extends State<IOSDraggable> {
|
||||||
late Offset _position;
|
|
||||||
late ChatModel? _chatModel;
|
late ChatModel? _chatModel;
|
||||||
late double _width;
|
late double _width;
|
||||||
late double _height;
|
late double _height;
|
||||||
@@ -391,25 +481,26 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_position = widget.position;
|
|
||||||
_chatModel = widget.chatModel;
|
_chatModel = widget.chatModel;
|
||||||
_width = widget.width;
|
_width = widget.width;
|
||||||
_height = widget.height;
|
_height = widget.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get position => widget.position;
|
||||||
|
|
||||||
checkKeyboard() {
|
checkKeyboard() {
|
||||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
final currentVisible = bottomHeight != 0;
|
final currentVisible = bottomHeight != 0;
|
||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = _position.dy;
|
_saveHeight = position.value.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, _saveHeight);
|
position.value = Offset(position.value.dx, _saveHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,10 +508,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + _height;
|
final sumHeight = bottomHeight + _height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + _position.dy > contextHeight) {
|
if (sumHeight + position.value.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, y);
|
position.value = Offset(position.value.dx, y);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
left: _position.dx,
|
left: position.value.dx,
|
||||||
top: _position.dy,
|
top: position.value.dy,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position += details.delta;
|
position.value += details.delta;
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(_position);
|
_chatModel?.setChatWindowPosition(position.value);
|
||||||
},
|
},
|
||||||
child: Material(
|
child: Material(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -498,8 +589,10 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
||||||
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
||||||
|
// let delay be 0 if fps is 0
|
||||||
_row(
|
_row(
|
||||||
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
|
"Delay",
|
||||||
|
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
|
||||||
rightColor: Colors.green),
|
rightColor: Colors.green),
|
||||||
_row("Target Bitrate",
|
_row("Target Bitrate",
|
||||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
menuItems.add(_createShortCutAction(peer.id));
|
menuItems.add(_createShortCutAction(peer.id));
|
||||||
}
|
}
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||||
@@ -943,7 +943,7 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
menuItems.add(_createShortCutAction(peer.id));
|
menuItems.add(_createShortCutAction(peer.id));
|
||||||
}
|
}
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||||
@@ -1048,7 +1048,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
}
|
}
|
||||||
if (gFFI.abModel.current.canWrite()) {
|
if (gFFI.abModel.current.canWrite()) {
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
||||||
|
|||||||
@@ -802,13 +802,20 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 36,
|
height: 36,
|
||||||
child: getRadio<PeerUiType>(
|
child: getRadio<PeerUiType>(
|
||||||
Text(
|
Tooltip(
|
||||||
translate(types.indexOf(e) == 0
|
message: translate(types.indexOf(e) == 0
|
||||||
? 'Big tiles'
|
? 'Big tiles'
|
||||||
: types.indexOf(e) == 1
|
: types.indexOf(e) == 1
|
||||||
? 'Small tiles'
|
? 'Small tiles'
|
||||||
: 'List'),
|
: 'List'),
|
||||||
style: style),
|
child: Icon(
|
||||||
|
e == PeerUiType.grid
|
||||||
|
? Icons.grid_view_rounded
|
||||||
|
: e == PeerUiType.list
|
||||||
|
? Icons.view_list_rounded
|
||||||
|
: Icons.view_agenda_rounded,
|
||||||
|
size: 18,
|
||||||
|
)),
|
||||||
e,
|
e,
|
||||||
peerCardUiType.value,
|
peerCardUiType.value,
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -838,7 +845,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
peerCardUiType.value == PeerUiType.grid
|
peerCardUiType.value == PeerUiType.grid
|
||||||
? Icons.grid_view_rounded
|
? Icons.grid_view_rounded
|
||||||
: peerCardUiType.value == PeerUiType.tile
|
: peerCardUiType.value == PeerUiType.list
|
||||||
? Icons.view_list_rounded
|
? Icons.view_list_rounded
|
||||||
: Icons.view_agenda_rounded,
|
: Icons.view_agenda_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
|
|||||||
class _RawTouchGestureDetectorRegionState
|
class _RawTouchGestureDetectorRegionState
|
||||||
extends State<RawTouchGestureDetectorRegion> {
|
extends State<RawTouchGestureDetectorRegion> {
|
||||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||||
|
// Timestamp of the last long press event.
|
||||||
|
int _cacheLongPressPositionTs = 0;
|
||||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||||
double _scale = 1;
|
double _scale = 1;
|
||||||
|
|
||||||
@@ -95,8 +97,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
// Desktop or mobile "Touch mode"
|
// Desktop or mobile "Touch mode"
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
inputModel.tapDown(MouseButtons.left);
|
inputModel.tapDown(MouseButtons.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +108,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
inputModel.tapUp(MouseButtons.left);
|
inputModel.tapUp(MouseButtons.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +138,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
inputModel.tap(MouseButtons.left);
|
inputModel.tap(MouseButtons.left);
|
||||||
inputModel.tap(MouseButtons.left);
|
inputModel.tap(MouseButtons.left);
|
||||||
}
|
}
|
||||||
@@ -146,6 +153,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
_cacheLongPressPosition = d.localPosition;
|
_cacheLongPressPosition = d.localPosition;
|
||||||
|
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,9 +230,22 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
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.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||||
|
}
|
||||||
inputModel.sendMouse('down', MouseButtons.left);
|
inputModel.sendMouse('down', MouseButtons.left);
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +265,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +305,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// mobile
|
// mobile
|
||||||
ffi.canvasModel.updateScale(d.scale / _scale);
|
ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
|
||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
||||||
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
||||||
|
|||||||
@@ -234,12 +234,12 @@ List<(String, String)> otherDefaultSettings() {
|
|||||||
('True color (4:4:4)', kOptionI444),
|
('True color (4:4:4)', kOptionI444),
|
||||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||||
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||||
if (isDesktop && bind.mainGetUseTextureRender())
|
if (isDesktop)
|
||||||
(
|
(
|
||||||
'Show displays as individual windows',
|
'Show displays as individual windows',
|
||||||
kKeyShowDisplaysAsIndividualWindows
|
kKeyShowDisplaysAsIndividualWindows
|
||||||
),
|
),
|
||||||
if (isDesktop && bind.mainGetUseTextureRender())
|
if (isDesktop)
|
||||||
(
|
(
|
||||||
'Use all my displays for the remote session',
|
'Use all my displays for the remote session',
|
||||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||||
|
|||||||
@@ -583,8 +583,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Lock after session end'))));
|
child: Text(translate('Lock after session end'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bind.mainGetUseTextureRender() &&
|
if (pi.isSupportMultiDisplay &&
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
PrivacyModeState.find(id).isEmpty &&
|
PrivacyModeState.find(id).isEmpty &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||||
@@ -596,15 +595,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionSetDisplaysAsIndividualWindows(
|
bind.sessionSetDisplaysAsIndividualWindows(
|
||||||
sessionId: sessionId, value: value ? 'Y' : '');
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
child: Text(translate('Show displays as individual windows'))));
|
child: Text(translate('Show displays as individual windows'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||||
if (bind.mainGetUseTextureRender() &&
|
if (pi.isSupportMultiDisplay && isMultiScreens) {
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
isMultiScreens) {
|
|
||||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
@@ -613,7 +610,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: sessionId, value: value ? 'Y' : '');
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
child: Text(translate('Use all my displays for the remote session'))));
|
child: Text(translate('Use all my displays for the remote session'))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,9 +133,17 @@ const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
|||||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||||
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
|
const String kOptionStopService = "stop-service";
|
||||||
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
|
|
||||||
const String kOptionToggleViewOnly = "view-only";
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
|
|
||||||
|
const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||||
|
|
||||||
|
const String kOptionKeepScreenOn = "keep-screen-on";
|
||||||
|
|
||||||
|
const String kOptionShowMobileAction = "showMobileActions";
|
||||||
|
|
||||||
const String kUrlActionClose = "close";
|
const String kUrlActionClose = "close";
|
||||||
|
|
||||||
const String kTabLabelHomePage = "Home";
|
const String kTabLabelHomePage = "Home";
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
systemError = error;
|
systemError = error;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
final v = await bind.mainGetOption(key: "stop-service") == "Y";
|
final v = await mainGetBoolOption(kOptionStopService);
|
||||||
if (v != svcStopped.value) {
|
if (v != svcStopped.value) {
|
||||||
svcStopped.value = v;
|
svcStopped.value = v;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|||||||
@@ -315,11 +315,11 @@ class _GeneralState extends State<_General> {
|
|||||||
children: [
|
children: [
|
||||||
service(),
|
service(),
|
||||||
theme(),
|
theme(),
|
||||||
|
_Card(title: 'Language', children: [language()]),
|
||||||
hwcodec(),
|
hwcodec(),
|
||||||
audio(context),
|
audio(context),
|
||||||
record(context),
|
record(context),
|
||||||
WaylandCard(),
|
WaylandCard(),
|
||||||
_Card(title: 'Language', children: [language()]),
|
|
||||||
other()
|
other()
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: _kListViewBottomMargin));
|
).marginOnly(bottom: _kListViewBottomMargin));
|
||||||
@@ -413,6 +413,12 @@ class _GeneralState extends State<_General> {
|
|||||||
'Check for software update on startup',
|
'Check for software update on startup',
|
||||||
kOptionEnableCheckUpdate,
|
kOptionEnableCheckUpdate,
|
||||||
isServer: false,
|
isServer: false,
|
||||||
|
),
|
||||||
|
if (isWindows && !bind.isOutgoingOnly())
|
||||||
|
_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Capture screen using DirectX',
|
||||||
|
kOptionDirectxCapture,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -504,9 +510,9 @@ class _GeneralState extends State<_General> {
|
|||||||
Widget record(BuildContext context) {
|
Widget record(BuildContext context) {
|
||||||
final showRootDir = isWindows && bind.mainIsInstalled();
|
final showRootDir = isWindows && bind.mainIsInstalled();
|
||||||
return futureBuilder(future: () async {
|
return futureBuilder(future: () async {
|
||||||
String user_dir = await bind.mainVideoSaveDirectory(root: false);
|
String user_dir = bind.mainVideoSaveDirectory(root: false);
|
||||||
String root_dir =
|
String root_dir =
|
||||||
showRootDir ? await bind.mainVideoSaveDirectory(root: true) : '';
|
showRootDir ? bind.mainVideoSaveDirectory(root: true) : '';
|
||||||
bool user_dir_exists = await Directory(user_dir).exists();
|
bool user_dir_exists = await Directory(user_dir).exists();
|
||||||
bool root_dir_exists =
|
bool root_dir_exists =
|
||||||
showRootDir ? await Directory(root_dir).exists() : false;
|
showRootDir ? await Directory(root_dir).exists() : false;
|
||||||
|
|||||||
@@ -36,12 +36,24 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
class _DesktopTabPageState extends State<DesktopTabPage>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
|
final RxBool _block = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
} else if (state == AppLifecycleState.inactive) {}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
Get.put<DesktopTabController>(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
@@ -68,8 +80,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
Get.delete<DesktopTabController>();
|
Get.delete<DesktopTabController>();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -89,12 +103,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
|
widget() => MouseRegion(
|
||||||
|
onEnter: (_) async {
|
||||||
|
await shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
},
|
||||||
|
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? Obx(() => widget())
|
||||||
: Obx(
|
: Obx(
|
||||||
() => DragToResizeArea(
|
() => DragToResizeArea(
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
child: tabWidget,
|
child: widget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,8 +141,9 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context)
|
data: Theme.of(context).copyWith(
|
||||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
colorScheme: Theme.of(context).colorScheme,
|
||||||
|
),
|
||||||
child: Obx(() => ListView.builder(
|
child: Obx(() => ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: pfs.length + 2,
|
itemCount: pfs.length + 2,
|
||||||
@@ -289,7 +290,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
).marginOnly(left: _kTextLeftMargin));
|
).marginOnly(left: _kTextLeftMargin));
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context)
|
data: Theme.of(context)
|
||||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
.copyWith(colorScheme: Theme.of(context).colorScheme),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: 2,
|
itemCount: 2,
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||||
|
|
||||||
|
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
||||||
|
// to identify the toolbar instance and its callback function.
|
||||||
|
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
||||||
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
@@ -131,6 +134,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
// Session option should be set after models.dart/FFI.start
|
// Session option should be set after models.dart/FFI.start
|
||||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
@@ -268,9 +272,18 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
id: widget.id,
|
id: widget.id,
|
||||||
ffi: _ffi,
|
ffi: _ffi,
|
||||||
state: widget.toolbarState,
|
state: widget.toolbarState,
|
||||||
onEnterOrLeaveImageSetter: (func) =>
|
onEnterOrLeaveImageSetter: (id, func) {
|
||||||
_onEnterOrLeaveImage4Toolbar = func,
|
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
|
||||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
|
_onEnterOrLeaveImage4Toolbar = func;
|
||||||
|
},
|
||||||
|
onEnterOrLeaveImageCleaner: (id) {
|
||||||
|
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
|
||||||
|
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
|
||||||
|
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
|
||||||
|
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
|
||||||
|
_onEnterOrLeaveImage4Toolbar = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
setRemoteState: setState,
|
setRemoteState: setState,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -310,22 +323,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (!_ffi.ffiModel.isPeerAndroid) {
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
} else {
|
} else {
|
||||||
if (_ffi.connType == ConnType.defaultConn &&
|
|
||||||
_ffi.ffiModel.permissions['keyboard'] != false) {
|
|
||||||
Timer(
|
|
||||||
Duration(milliseconds: 10),
|
|
||||||
() => _ffi.dialogManager
|
|
||||||
.mobileActionsOverlayVisible.value = true);
|
|
||||||
}
|
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
offstage: _ffi.dialogManager
|
offstage: _ffi.dialogManager
|
||||||
.mobileActionsOverlayVisible.isFalse,
|
.mobileActionsOverlayVisible.isFalse,
|
||||||
child: Overlay(initialEntries: [
|
child: Overlay(initialEntries: [
|
||||||
makeMobileActionsOverlayEntry(
|
makeMobileActionsOverlayEntry(
|
||||||
() => _ffi
|
() => _ffi.dialogManager
|
||||||
.dialogManager
|
.setMobileActionsOverlayVisible(false),
|
||||||
.mobileActionsOverlayVisible
|
|
||||||
.value = false,
|
|
||||||
ffi: _ffi,
|
ffi: _ffi,
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
@@ -484,6 +488,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||||
? Container(color: Colors.transparent)
|
? Container(color: Colors.transparent)
|
||||||
: Obx(() {
|
: Obx(() {
|
||||||
|
widget.toolbarState.initShow(sessionId);
|
||||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||||
return ImagePaint(
|
return ImagePaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
@@ -617,10 +622,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
final paintWidth = c.getDisplayWidth() * s;
|
final paintWidth = c.getDisplayWidth() * s;
|
||||||
final paintHeight = c.getDisplayHeight() * s;
|
final paintHeight = c.getDisplayHeight() * s;
|
||||||
final paintSize = Size(paintWidth, paintHeight);
|
final paintSize = Size(paintWidth, paintHeight);
|
||||||
final paintWidget = m.useTextureRender
|
final paintWidget =
|
||||||
? _BuildPaintTextureRender(
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
? _BuildPaintTextureRender(
|
||||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||||
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
c.updateScrollPercent();
|
c.updateScrollPercent();
|
||||||
@@ -638,17 +644,18 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
if (c.size.width > 0 && c.size.height > 0) {
|
if (c.size.width > 0 && c.size.height > 0) {
|
||||||
final paintWidget = m.useTextureRender
|
final paintWidget =
|
||||||
? _BuildPaintTextureRender(
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||||
c,
|
? _BuildPaintTextureRender(
|
||||||
s,
|
c,
|
||||||
Offset(
|
s,
|
||||||
isLinux ? c.x.toInt().toDouble() : c.x,
|
Offset(
|
||||||
isLinux ? c.y.toInt().toDouble() : c.y,
|
isLinux ? c.x.toInt().toDouble() : c.x,
|
||||||
),
|
isLinux ? c.y.toInt().toDouble() : c.y,
|
||||||
c.size,
|
),
|
||||||
isViewOriginal())
|
c.size,
|
||||||
: _buildScrollAuthNonTextureRender(m, c, s);
|
isViewOriginal())
|
||||||
|
: _buildScrollAutoNonTextureRender(m, c, s);
|
||||||
return mouseRegion(child: _buildListener(paintWidget));
|
return mouseRegion(child: _buildListener(paintWidget));
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -664,7 +671,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScrollAuthNonTextureRender(
|
Widget _buildScrollAutoNonTextureRender(
|
||||||
ImageModel m, CanvasModel c, double s) {
|
ImageModel m, CanvasModel c, double s) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
size: Size(c.size.width, c.size.height),
|
size: Size(c.size.width, c.size.height),
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
|
|
||||||
late ToolbarState _toolbarState;
|
|
||||||
String? peerId;
|
String? peerId;
|
||||||
bool _isScreenRectSet = false;
|
bool _isScreenRectSet = false;
|
||||||
int? _display;
|
int? _display;
|
||||||
@@ -54,7 +53,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||||
|
|
||||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||||
_toolbarState = ToolbarState();
|
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
peerId = params['id'];
|
peerId = params['id'];
|
||||||
final sessionId = params['session_id'];
|
final sessionId = params['session_id'];
|
||||||
@@ -91,7 +89,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
display: display,
|
display: display,
|
||||||
displays: displays?.cast<int>(),
|
displays: displays?.cast<int>(),
|
||||||
password: params['password'],
|
password: params['password'],
|
||||||
toolbarState: _toolbarState,
|
toolbarState: ToolbarState(),
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
switchUuid: params['switch_uuid'],
|
switchUuid: params['switch_uuid'],
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
@@ -126,7 +124,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_toolbarState.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -251,15 +248,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final pi = ffi.ffiModel.pi;
|
final pi = ffi.ffiModel.pi;
|
||||||
final perms = ffi.ffiModel.permissions;
|
final perms = ffi.ffiModel.permissions;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
final toolbarState = remotePage.toolbarState;
|
||||||
menu.addAll([
|
menu.addAll([
|
||||||
MenuEntryButton<String>(
|
MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||||
translate(
|
translate(
|
||||||
_toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
style: style,
|
style: style,
|
||||||
)),
|
)),
|
||||||
proc: () {
|
proc: () {
|
||||||
_toolbarState.switchShow();
|
toolbarState.switchShow(sessionId);
|
||||||
cancelFunc();
|
cancelFunc();
|
||||||
},
|
},
|
||||||
padding: padding,
|
padding: padding,
|
||||||
@@ -420,14 +418,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
}
|
}
|
||||||
await setNewConnectWindowFrame(windowId(), id!, screenRect);
|
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
|
||||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||||
await windowOnTop(windowId());
|
await windowOnTop(windowId());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ConnectionTypeState.init(id);
|
ConnectionTypeState.init(id);
|
||||||
_toolbarState.setShow(
|
|
||||||
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: id,
|
key: id,
|
||||||
label: id,
|
label: id,
|
||||||
@@ -442,7 +438,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
display: display,
|
display: display,
|
||||||
displays: displays?.cast<int>(),
|
displays: displays?.cast<int>(),
|
||||||
password: args['password'],
|
password: args['password'],
|
||||||
toolbarState: _toolbarState,
|
toolbarState: ToolbarState(),
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
switchUuid: switchUuid,
|
switchUuid: switchUuid,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
|||||||
/// The text style of the popup menu item.
|
/// The text style of the popup menu item.
|
||||||
///
|
///
|
||||||
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
||||||
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1]
|
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
|
||||||
/// of [ThemeData.textTheme] is used.
|
/// of [ThemeData.textTheme] is used.
|
||||||
final TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
TextStyle style = widget.textStyle ??
|
TextStyle style = widget.textStyle ??
|
||||||
popupMenuTheme.textStyle ??
|
popupMenuTheme.textStyle ??
|
||||||
theme.textTheme.subtitle1!;
|
theme.textTheme.titleMedium!;
|
||||||
|
|
||||||
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
|||||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
TextStyle style = widget.textStyle ??
|
TextStyle style = widget.textStyle ??
|
||||||
popupMenuTheme.textStyle ??
|
popupMenuTheme.textStyle ??
|
||||||
theme.textTheme.subtitle1!;
|
theme.textTheme.titleMedium!;
|
||||||
return Obx(() => mod_menu.PopupMenuButton<T>(
|
return Obx(() => mod_menu.PopupMenuButton<T>(
|
||||||
enabled: enabled.value,
|
enabled: enabled.value,
|
||||||
position: widget.position,
|
position: widget.position,
|
||||||
|
|||||||
@@ -26,45 +26,42 @@ import './popup_menu.dart';
|
|||||||
import './kb_layout_type_chooser.dart';
|
import './kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
class ToolbarState {
|
class ToolbarState {
|
||||||
late RxBool show;
|
|
||||||
late RxBool _pin;
|
late RxBool _pin;
|
||||||
|
|
||||||
|
bool isShowInited = false;
|
||||||
|
RxBool show = false.obs;
|
||||||
|
|
||||||
ToolbarState() {
|
ToolbarState() {
|
||||||
|
_pin = RxBool(false);
|
||||||
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||||
if (s.isEmpty) {
|
if (s.isEmpty) {
|
||||||
_initSet(false, false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final m = jsonDecode(s);
|
final m = jsonDecode(s);
|
||||||
if (m == null) {
|
if (m != null) {
|
||||||
_initSet(false, false);
|
_pin = RxBool(m['pin'] ?? false);
|
||||||
} else {
|
|
||||||
_initSet(m['pin'] ?? false, m['pin'] ?? false);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
||||||
_initSet(false, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initSet(bool s, bool p) {
|
|
||||||
// Show remubar when connection is established.
|
|
||||||
show = RxBool(
|
|
||||||
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
|
||||||
_pin = RxBool(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get pin => _pin.value;
|
bool get pin => _pin.value;
|
||||||
|
|
||||||
switchShow() async {
|
switchShow(SessionID sessionId) async {
|
||||||
|
bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: kOptionCollapseToolbar);
|
||||||
show.value = !show.value;
|
show.value = !show.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShow(bool v) async {
|
initShow(SessionID sessionId) async {
|
||||||
if (show.value != v) {
|
if (!isShowInited) {
|
||||||
show.value = v;
|
show.value = !(await bind.sessionGetToggleOption(
|
||||||
|
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
|
||||||
|
false);
|
||||||
|
isShowInited = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +83,6 @@ class ToolbarState {
|
|||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||||
}
|
}
|
||||||
|
|
||||||
save() async {
|
|
||||||
await _savePin();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ToolbarTheme {
|
class _ToolbarTheme {
|
||||||
@@ -332,8 +325,8 @@ class RemoteToolbar extends StatefulWidget {
|
|||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final ToolbarState state;
|
final ToolbarState state;
|
||||||
final Function(Function(bool)) onEnterOrLeaveImageSetter;
|
final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
|
||||||
final VoidCallback onEnterOrLeaveImageCleaner;
|
final Function(int) onEnterOrLeaveImageCleaner;
|
||||||
final Function(VoidCallback) setRemoteState;
|
final Function(VoidCallback) setRemoteState;
|
||||||
|
|
||||||
RemoteToolbar({
|
RemoteToolbar({
|
||||||
@@ -393,7 +386,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
widget.onEnterOrLeaveImageSetter((enter) {
|
widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
|
||||||
if (enter) {
|
if (enter) {
|
||||||
triggerAutoHide();
|
triggerAutoHide();
|
||||||
_isCursorOverImage = true;
|
_isCursorOverImage = true;
|
||||||
@@ -413,12 +406,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
dispose() {
|
dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
widget.onEnterOrLeaveImageCleaner();
|
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// No need to use future builder here.
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Obx(() => show.value
|
child: Obx(() => show.value
|
||||||
@@ -447,7 +439,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fractionX: _fractionX,
|
fractionX: _fractionX,
|
||||||
show: show,
|
toolbarState: widget.state,
|
||||||
setFullscreen: _setFullscreen,
|
setFullscreen: _setFullscreen,
|
||||||
setMinimize: _minimize,
|
setMinimize: _minimize,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
@@ -587,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
return Obx(() => _IconMenuButton(
|
return Obx(() => _IconMenuButton(
|
||||||
assetName: 'assets/actions_mobile.svg',
|
assetName: 'assets/actions_mobile.svg',
|
||||||
tooltip: 'Mobile Actions',
|
tooltip: 'Mobile Actions',
|
||||||
onPressed: () =>
|
onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
|
||||||
ffi.dialogManager.mobileActionsOverlayVisible.toggle(),
|
!ffi.dialogManager.mobileActionsOverlayVisible.value),
|
||||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||||
? _ToolbarTheme.blueColor
|
? _ToolbarTheme.blueColor
|
||||||
: _ToolbarTheme.inactiveColor,
|
: _ToolbarTheme.inactiveColor,
|
||||||
@@ -643,15 +635,12 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||||
final m = Provider.of<ImageModel>(context);
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(children: buildMonitorList(context, false)),
|
Row(children: buildMonitorList(context, false)),
|
||||||
supportIndividualWindows && m.useTextureRender ? Divider() : Offstage(),
|
supportIndividualWindows ? Divider() : Offstage(),
|
||||||
supportIndividualWindows && m.useTextureRender
|
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
||||||
? chooseDisplayBehavior()
|
|
||||||
: Offstage(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -665,7 +654,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionSetDisplaysAsIndividualWindows(
|
await bind.sessionSetDisplaysAsIndividualWindows(
|
||||||
sessionId: ffi.sessionId, value: value ? 'Y' : '');
|
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
child: Text(translate('Show displays as individual windows')));
|
child: Text(translate('Show displays as individual windows')));
|
||||||
@@ -737,10 +726,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
for (int i = 0; i < pi.displays.length; i++) {
|
for (int i = 0; i < pi.displays.length; i++) {
|
||||||
monitorList.add(buildMonitorButton(i));
|
monitorList.add(buildMonitorButton(i));
|
||||||
}
|
}
|
||||||
final m = Provider.of<ImageModel>(context);
|
if (supportIndividualWindows && pi.displays.length > 1) {
|
||||||
if (supportIndividualWindows &&
|
|
||||||
m.useTextureRender &&
|
|
||||||
pi.displays.length > 1) {
|
|
||||||
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
||||||
}
|
}
|
||||||
return monitorList;
|
return monitorList;
|
||||||
@@ -824,7 +810,6 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
RxInt display = CurrentDisplayState.find(id);
|
RxInt display = CurrentDisplayState.find(id);
|
||||||
if (display.value != i) {
|
if (display.value != i) {
|
||||||
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||||
bind.mainGetUseTextureRender() &&
|
|
||||||
bind.sessionGetDisplaysAsIndividualWindows(
|
bind.sessionGetDisplaysAsIndividualWindows(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
@@ -2351,7 +2336,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fractionX;
|
final RxDouble fractionX;
|
||||||
final RxBool dragging;
|
final RxBool dragging;
|
||||||
final RxBool show;
|
final ToolbarState toolbarState;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
|
|
||||||
final Function(bool) setFullscreen;
|
final Function(bool) setFullscreen;
|
||||||
@@ -2362,7 +2347,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fractionX,
|
required this.fractionX,
|
||||||
required this.dragging,
|
required this.dragging,
|
||||||
required this.show,
|
required this.toolbarState,
|
||||||
required this.setFullscreen,
|
required this.setFullscreen,
|
||||||
required this.setMinimize,
|
required this.setMinimize,
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
@@ -2378,6 +2363,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
double left = 0.0;
|
double left = 0.0;
|
||||||
double right = 1.0;
|
double right = 1.0;
|
||||||
|
|
||||||
|
RxBool get show => widget.toolbarState.show;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -2465,28 +2452,29 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
if (!isMacOS) Obx(() => Offstage(
|
if (!isMacOS)
|
||||||
offstage: isFullscreen.isFalse,
|
Obx(() => Offstage(
|
||||||
child: TextButton(
|
offstage: isFullscreen.isFalse,
|
||||||
onPressed: () => widget.setMinimize(),
|
child: TextButton(
|
||||||
child: Tooltip(
|
onPressed: () => widget.setMinimize(),
|
||||||
message: translate('Minimize'),
|
child: Tooltip(
|
||||||
child: Icon(
|
message: translate('Minimize'),
|
||||||
Icons.remove,
|
child: Icon(
|
||||||
size: iconSize,
|
Icons.remove,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)),
|
||||||
)),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
widget.show.value = !widget.show.value;
|
widget.toolbarState.switchShow(widget.sessionId);
|
||||||
}),
|
}),
|
||||||
child: Obx((() => Tooltip(
|
child: Obx((() => Tooltip(
|
||||||
message: translate(
|
message:
|
||||||
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
|
|||||||
@@ -320,16 +320,7 @@ class DesktopTab extends StatelessWidget {
|
|||||||
if (tabType != DesktopTabType.main) {
|
if (tabType != DesktopTabType.main) {
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
return buildRemoteBlock(
|
return buildRemoteBlock(child: child, use: canBeBlocked);
|
||||||
child: child,
|
|
||||||
use: () async {
|
|
||||||
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
|
||||||
var option = option2bool(
|
|
||||||
kOptionAllowRemoteConfigModification,
|
|
||||||
await bind.mainGetOption(
|
|
||||||
key: kOptionAllowRemoteConfigModification));
|
|
||||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
List<Widget> _tabWidgets = [];
|
||||||
@@ -397,6 +388,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
onPanStart: (_) => startDragging(isMainWindow),
|
onPanStart: (_) => startDragging(isMainWindow),
|
||||||
onPanCancel: () {
|
onPanCancel: () {
|
||||||
|
// We want to disable dragging of the tab area in the tab bar.
|
||||||
|
// Disable dragging is needed because macOS handles dragging by default.
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
setMovable(isMainWindow, false);
|
setMovable(isMainWindow, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart';
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/overlay.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/install_page.dart';
|
import 'package:flutter_hbb/desktop/pages/install_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
@@ -95,7 +96,9 @@ Future<void> main(List<String> args) async {
|
|||||||
desktopType = DesktopType.main;
|
desktopType = DesktopType.main;
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
windowManager.setPreventClose(true);
|
windowManager.setPreventClose(true);
|
||||||
windowManager.setMovable(false);
|
if (isMacOS) {
|
||||||
|
disableWindowMovable(kWindowId);
|
||||||
|
}
|
||||||
runMainApp(true);
|
runMainApp(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +157,7 @@ void runMobileApp() async {
|
|||||||
await initEnv(kAppTypeMain);
|
await initEnv(kAppTypeMain);
|
||||||
if (isAndroid) androidChannelInit();
|
if (isAndroid) androidChannelInit();
|
||||||
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||||
|
draggablePositions.load();
|
||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
runApp(App());
|
runApp(App());
|
||||||
@@ -168,10 +172,13 @@ void runMultiWindow(
|
|||||||
final title = getWindowName();
|
final title = getWindowName();
|
||||||
// set prevent close to true, we handle close event manually
|
// set prevent close to true, we handle close event manually
|
||||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||||
WindowController.fromWindowId(kWindowId!).setMovable(false);
|
if (isMacOS) {
|
||||||
|
disableWindowMovable(kWindowId);
|
||||||
|
}
|
||||||
late Widget widget;
|
late Widget widget;
|
||||||
switch (appType) {
|
switch (appType) {
|
||||||
case kAppTypeDesktopRemote:
|
case kAppTypeDesktopRemote:
|
||||||
|
draggablePositions.load();
|
||||||
widget = DesktopRemoteScreen(
|
widget = DesktopRemoteScreen(
|
||||||
params: argument,
|
params: argument,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,13 +82,14 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
|
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay();
|
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
||||||
gFFI.inputModel.listenToMouse(false);
|
gFFI.inputModel.listenToMouse(false);
|
||||||
gFFI.imageModel.disposeImage();
|
gFFI.imageModel.disposeImage();
|
||||||
gFFI.cursorModel.disposeImages();
|
gFFI.cursorModel.disposeImages();
|
||||||
@@ -105,11 +106,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
await keyboardSubscription.cancel();
|
await keyboardSubscription.cancel();
|
||||||
removeSharedStates(widget.id);
|
removeSharedStates(widget.id);
|
||||||
if (isAndroid) {
|
// `on_voice_call_closed` should be called when the connection is ended.
|
||||||
// Only one client is considered here for now.
|
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
|
||||||
// TODO: take into account the case where there are multiple clients
|
// Only one client is considered here for now.
|
||||||
gFFI.invokeMethod("on_voice_call_closed");
|
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
@@ -419,17 +419,21 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
(isWeb
|
(isWeb
|
||||||
? []
|
? []
|
||||||
: <Widget>[
|
: <Widget>[
|
||||||
IconButton(
|
futureBuilder(
|
||||||
color: Colors.white,
|
future: gFFI.invokeMethod(
|
||||||
icon: isAndroid
|
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
|
||||||
? SvgPicture.asset('assets/chat.svg',
|
hasData: (isSupportVoiceCall) => IconButton(
|
||||||
colorFilter: ColorFilter.mode(
|
color: Colors.white,
|
||||||
Colors.white, BlendMode.srcIn))
|
icon: isAndroid && isSupportVoiceCall
|
||||||
: Icon(Icons.message),
|
? SvgPicture.asset('assets/chat.svg',
|
||||||
onPressed: () => isAndroid
|
colorFilter: ColorFilter.mode(
|
||||||
? showChatOptions(widget.id)
|
Colors.white, BlendMode.srcIn))
|
||||||
: onPressedTextChat(widget.id),
|
: Icon(Icons.message),
|
||||||
)
|
onPressed: () =>
|
||||||
|
isAndroid && isSupportVoiceCall
|
||||||
|
? showChatOptions(widget.id)
|
||||||
|
: onPressedTextChat(widget.id),
|
||||||
|
))
|
||||||
]) +
|
]) +
|
||||||
[
|
[
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -479,7 +483,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
: TextFormField(
|
: TextFormField(
|
||||||
textInputAction: TextInputAction.newline,
|
textInputAction: TextInputAction.newline,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
// Flutter 3.16.9 Android.
|
||||||
|
// `enableSuggestions` causes secure keyboard to be shown.
|
||||||
|
// https://github.com/flutter/flutter/issues/139143
|
||||||
|
// https://github.com/flutter/flutter/issues/146540
|
||||||
|
// enableSuggestions: false,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
focusNode: _mobileFocusNode,
|
focusNode: _mobileFocusNode,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
@@ -679,6 +687,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
var _fn = false;
|
var _fn = false;
|
||||||
var _pin = false;
|
var _pin = false;
|
||||||
final _keyboardVisibilityController = KeyboardVisibilityController();
|
final _keyboardVisibilityController = KeyboardVisibilityController();
|
||||||
|
final _key = GlobalKey();
|
||||||
|
|
||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
|
|
||||||
@@ -703,6 +712,24 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
onPressed: onPressed);
|
onPressed: onPressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRect() {
|
||||||
|
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
||||||
|
if (renderObject == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (renderObject is RenderBox) {
|
||||||
|
final size = renderObject.size;
|
||||||
|
Offset pos = renderObject.localToGlobal(Offset.zero);
|
||||||
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
|
||||||
|
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasModifierOn = inputModel.ctrl ||
|
final hasModifierOn = inputModel.ctrl ||
|
||||||
@@ -711,6 +738,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
inputModel.command;
|
inputModel.command;
|
||||||
|
|
||||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||||
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -821,7 +849,12 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
final space = size.width > 320 ? 4.0 : 2.0;
|
final space = size.width > 320 ? 4.0 : 2.0;
|
||||||
|
// 500 ms is long enough for this widget to be built!
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
|
_updateRect();
|
||||||
|
});
|
||||||
return Container(
|
return Container(
|
||||||
|
key: _key,
|
||||||
color: Color(0xAA000000),
|
color: Color(0xAA000000),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
|
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
|
||||||
@@ -901,7 +934,7 @@ void showOptions(
|
|||||||
border: Border.all(color: Theme.of(context).hintColor),
|
border: Border.all(color: Theme.of(context).hintColor),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
color: i == cur
|
color: i == cur
|
||||||
? Theme.of(context).toggleableActiveColor.withOpacity(0.6)
|
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||||
: null),
|
: null),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text((i + 1).toString(),
|
child: Text((i + 1).toString(),
|
||||||
|
|||||||
@@ -854,6 +854,15 @@ void androidChannelInit() {
|
|||||||
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stop_service":
|
||||||
|
{
|
||||||
|
print(
|
||||||
|
"stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
|
||||||
|
if (gFFI.serverModel.isStart) {
|
||||||
|
gFFI.serverModel.stopService();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: "MethodCallHandler err:$e");
|
debugPrintStack(label: "MethodCallHandler err:$e");
|
||||||
|
|||||||
@@ -35,10 +35,41 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
|||||||
|
|
||||||
const url = 'https://rustdesk.com/';
|
const url = 'https://rustdesk.com/';
|
||||||
|
|
||||||
|
enum KeepScreenOn {
|
||||||
|
never,
|
||||||
|
duringControlled,
|
||||||
|
serviceOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
String _keepScreenOnToOption(KeepScreenOn value) {
|
||||||
|
switch (value) {
|
||||||
|
case KeepScreenOn.never:
|
||||||
|
return 'never';
|
||||||
|
case KeepScreenOn.duringControlled:
|
||||||
|
return 'during-controlled';
|
||||||
|
case KeepScreenOn.serviceOn:
|
||||||
|
return 'service-on';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeepScreenOn optionToKeepScreenOn(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'never':
|
||||||
|
return KeepScreenOn.never;
|
||||||
|
case 'service-on':
|
||||||
|
return KeepScreenOn.serviceOn;
|
||||||
|
default:
|
||||||
|
return KeepScreenOn.duringControlled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||||
final _hasIgnoreBattery = androidVersion >= 26;
|
final _hasIgnoreBattery =
|
||||||
|
false; //androidVersion >= 26; // remove because not work on every device
|
||||||
var _ignoreBatteryOpt = false;
|
var _ignoreBatteryOpt = false;
|
||||||
var _enableStartOnBoot = false;
|
var _enableStartOnBoot = false;
|
||||||
|
var _floatingWindowDisabled = false;
|
||||||
|
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
||||||
var _enableAbr = false;
|
var _enableAbr = false;
|
||||||
var _denyLANDiscovery = false;
|
var _denyLANDiscovery = false;
|
||||||
var _onlyWhiteList = false;
|
var _onlyWhiteList = false;
|
||||||
@@ -58,6 +89,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
_enableAbr = option2bool(
|
||||||
|
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||||
|
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
||||||
|
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
|
||||||
|
defaultOptionWhitelist;
|
||||||
|
_enableDirectIPAccess = option2bool(
|
||||||
|
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
||||||
|
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
|
||||||
|
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||||
|
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||||
|
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||||
|
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||||
|
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||||
|
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
|
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
||||||
|
_autoDisconnectTimeout =
|
||||||
|
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
var update = false;
|
var update = false;
|
||||||
|
|
||||||
@@ -86,67 +138,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_enableStartOnBoot = enableStartOnBoot;
|
_enableStartOnBoot = enableStartOnBoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableAbrRes = option2bool(
|
var floatingWindowDisabled =
|
||||||
kOptionEnableAbr, await bind.mainGetOption(key: kOptionEnableAbr));
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
if (enableAbrRes != _enableAbr) {
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
|
if (floatingWindowDisabled != _floatingWindowDisabled) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableAbr = enableAbrRes;
|
_floatingWindowDisabled = floatingWindowDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
final denyLanDiscovery = !option2bool('enable-lan-discovery',
|
final keepScreenOn = _floatingWindowDisabled
|
||||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
? KeepScreenOn.never
|
||||||
if (denyLanDiscovery != _denyLANDiscovery) {
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
||||||
|
if (keepScreenOn != _keepScreenOn) {
|
||||||
update = true;
|
update = true;
|
||||||
_denyLANDiscovery = denyLanDiscovery;
|
_keepScreenOn = keepScreenOn;
|
||||||
}
|
|
||||||
|
|
||||||
final onlyWhiteList = (await bind.mainGetOption(key: kOptionWhitelist)) !=
|
|
||||||
defaultOptionWhitelist;
|
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
|
||||||
update = true;
|
|
||||||
_onlyWhiteList = onlyWhiteList;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableDirectIPAccess = option2bool(kOptionDirectServer,
|
|
||||||
await bind.mainGetOption(key: kOptionDirectServer));
|
|
||||||
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
|
||||||
update = true;
|
|
||||||
_enableDirectIPAccess = enableDirectIPAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableRecordSession = option2bool(kOptionEnableRecordSession,
|
|
||||||
await bind.mainGetOption(key: kOptionEnableRecordSession));
|
|
||||||
if (enableRecordSession != _enableRecordSession) {
|
|
||||||
update = true;
|
|
||||||
_enableRecordSession = enableRecordSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
|
||||||
await bind.mainGetOption(key: kOptionEnableHwcodec));
|
|
||||||
if (_enableHardwareCodec != enableHardwareCodec) {
|
|
||||||
update = true;
|
|
||||||
_enableHardwareCodec = enableHardwareCodec;
|
|
||||||
}
|
|
||||||
|
|
||||||
final autoRecordIncomingSession = option2bool(
|
|
||||||
kOptionAllowAutoRecordIncoming,
|
|
||||||
await bind.mainGetOption(key: kOptionAllowAutoRecordIncoming));
|
|
||||||
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
|
||||||
update = true;
|
|
||||||
_autoRecordIncomingSession = autoRecordIncomingSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
final localIP = await bind.mainGetOption(key: 'local-ip-addr');
|
|
||||||
if (localIP != _localIP) {
|
|
||||||
update = true;
|
|
||||||
_localIP = localIP;
|
|
||||||
}
|
|
||||||
|
|
||||||
final directAccessPort =
|
|
||||||
await bind.mainGetOption(key: kOptionDirectAccessPort);
|
|
||||||
if (directAccessPort != _directAccessPort) {
|
|
||||||
update = true;
|
|
||||||
_directAccessPort = directAccessPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final fingerprint = await bind.mainGetFingerprint();
|
final fingerprint = await bind.mainGetFingerprint();
|
||||||
@@ -160,21 +166,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
update = true;
|
update = true;
|
||||||
_buildDate = buildDate;
|
_buildDate = buildDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
|
||||||
await bind.mainGetOption(key: kOptionAllowAutoDisconnect));
|
|
||||||
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
|
||||||
update = true;
|
|
||||||
_allowAutoDisconnect = allowAutoDisconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
final autoDisconnectTimeout =
|
|
||||||
await bind.mainGetOption(key: kOptionAutoDisconnectTimeout);
|
|
||||||
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
|
||||||
update = true;
|
|
||||||
_autoDisconnectTimeout = autoDisconnectTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
@@ -301,10 +292,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: isOptionFixed(kOptionEnableAbr)
|
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||||
? null
|
? null
|
||||||
: (v) async {
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await mainSetBoolOption(kOptionEnableAbr, v);
|
||||||
key: kOptionEnableAbr, value: v ? defaultOptionYes : "N");
|
final newValue = await mainGetBoolOption(kOptionEnableAbr);
|
||||||
final newValue =
|
|
||||||
await bind.mainGetOption(key: kOptionEnableAbr) != "N";
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableAbr = newValue;
|
_enableAbr = newValue;
|
||||||
});
|
});
|
||||||
@@ -316,12 +305,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
||||||
? null
|
? null
|
||||||
: (v) async {
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await mainSetBoolOption(kOptionEnableRecordSession, v);
|
||||||
key: kOptionEnableRecordSession,
|
|
||||||
value: v ? defaultOptionYes : "N");
|
|
||||||
final newValue =
|
final newValue =
|
||||||
await bind.mainGetOption(key: kOptionEnableRecordSession) !=
|
await mainGetBoolOption(kOptionEnableRecordSession);
|
||||||
"N";
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableRecordSession = newValue;
|
_enableRecordSession = newValue;
|
||||||
});
|
});
|
||||||
@@ -493,6 +479,56 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
onFloatingWindowChanged(bool toValue) async {
|
||||||
|
if (toValue) {
|
||||||
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
|
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final disable = !toValue;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionDisableFloatingWindow,
|
||||||
|
value: disable ? 'Y' : defaultOptionNo);
|
||||||
|
setState(() => _floatingWindowDisabled = disable);
|
||||||
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancementsTiles.add(SettingsTile.switchTile(
|
||||||
|
initialValue: !_floatingWindowDisabled,
|
||||||
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(translate('Floating window')),
|
||||||
|
Text('* ${translate('floating_window_tip')}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
]),
|
||||||
|
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
|
||||||
|
? null
|
||||||
|
: onFloatingWindowChanged));
|
||||||
|
|
||||||
|
enhancementsTiles.add(_getPopupDialogRadioEntry(
|
||||||
|
title: 'Keep screen on',
|
||||||
|
list: [
|
||||||
|
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
|
||||||
|
_RadioEntry('During controlled',
|
||||||
|
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
|
||||||
|
_RadioEntry('During service is on',
|
||||||
|
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
|
||||||
|
],
|
||||||
|
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
|
||||||
|
? KeepScreenOn.never
|
||||||
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
|
||||||
|
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionKeepScreenOn, value: value);
|
||||||
|
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
|
||||||
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
final disabledSettings = bind.isDisableSettings();
|
final disabledSettings = bind.isDisableSettings();
|
||||||
final settings = SettingsList(
|
final settings = SettingsList(
|
||||||
sections: [
|
sections: [
|
||||||
@@ -533,8 +569,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate(
|
title: Text(translate(
|
||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).brightness == Brightness.light
|
||||||
? 'Dark Theme'
|
? 'Light Theme'
|
||||||
: 'Light Theme')),
|
: 'Dark Theme')),
|
||||||
leading: Icon(Theme.of(context).brightness == Brightness.light
|
leading: Icon(Theme.of(context).brightness == Brightness.light
|
||||||
? Icons.dark_mode
|
? Icons.dark_mode
|
||||||
: Icons.light_mode),
|
: Icons.light_mode),
|
||||||
@@ -551,12 +587,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
||||||
? null
|
? null
|
||||||
: (v) async {
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await mainSetBoolOption(kOptionEnableHwcodec, v);
|
||||||
key: kOptionEnableHwcodec,
|
|
||||||
value: v ? defaultOptionYes : "N");
|
|
||||||
final newValue =
|
final newValue =
|
||||||
await bind.mainGetOption(key: kOptionEnableHwcodec) !=
|
await mainGetBoolOption(kOptionEnableHwcodec);
|
||||||
"N";
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableHardwareCodec = newValue;
|
_enableHardwareCodec = newValue;
|
||||||
});
|
});
|
||||||
@@ -571,11 +604,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
title:
|
title:
|
||||||
Text(translate('Automatically record incoming sessions')),
|
Text(translate('Automatically record incoming sessions')),
|
||||||
leading: Icon(Icons.videocam),
|
leading: Icon(Icons.videocam),
|
||||||
description: FutureBuilder(
|
description: Text(
|
||||||
builder: (ctx, data) => Offstage(
|
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
||||||
offstage: !data.hasData,
|
|
||||||
child: Text("${translate("Directory")}: ${data.data}")),
|
|
||||||
future: bind.mainVideoSaveDirectory(root: false)),
|
|
||||||
initialValue: _autoRecordIncomingSession,
|
initialValue: _autoRecordIncomingSession,
|
||||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
? null
|
? null
|
||||||
@@ -919,7 +949,7 @@ class _RadioEntry {
|
|||||||
typedef _RadioEntryGetter = String Function();
|
typedef _RadioEntryGetter = String Function();
|
||||||
typedef _RadioEntrySetter = Future<void> Function(String);
|
typedef _RadioEntrySetter = Future<void> Function(String);
|
||||||
|
|
||||||
_getPopupDialogRadioEntry({
|
SettingsTile _getPopupDialogRadioEntry({
|
||||||
required String title,
|
required String title,
|
||||||
required List<_RadioEntry> list,
|
required List<_RadioEntry> list,
|
||||||
required _RadioEntryGetter getter,
|
required _RadioEntryGetter getter,
|
||||||
@@ -973,7 +1003,7 @@ _getPopupDialogRadioEntry({
|
|||||||
|
|
||||||
return SettingsTile(
|
return SettingsTile(
|
||||||
title: Text(translate(title)),
|
title: Text(translate(title)),
|
||||||
onPressed: (context) => showDialog(),
|
onPressed: asyncSetter == null ? null : (context) => showDialog(),
|
||||||
value: Padding(
|
value: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Obx(() => Text(translate(valueText.value))),
|
child: Obx(() => Text(translate(valueText.value))),
|
||||||
|
|||||||
@@ -535,6 +535,8 @@ class ChatModel with ChangeNotifier {
|
|||||||
void onVoiceCallClosed(String reason) {
|
void onVoiceCallClosed(String reason) {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
|
// We can always invoke "on_voice_call_closed"
|
||||||
|
// no matter if the `_voiceCallStatus` was `VoiceCallStatus.notStarted` or not.
|
||||||
parent.target?.invokeMethod("on_voice_call_closed");
|
parent.target?.invokeMethod("on_voice_call_closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -917,10 +917,12 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (parent.target?.connType == ConnType.defaultConn &&
|
if (parent.target?.connType == ConnType.defaultConn &&
|
||||||
parent.target != null &&
|
parent.target != null &&
|
||||||
parent.target!.ffiModel.permissions['keyboard'] != false) {
|
parent.target!.ffiModel.permissions['keyboard'] != false) {
|
||||||
Timer(
|
Timer(Duration(milliseconds: delayMSecs), () {
|
||||||
Duration(milliseconds: delayMSecs),
|
if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) {
|
||||||
() => parent.target!.dialogManager
|
parent.target!.dialogManager
|
||||||
.showMobileActionsOverlay(ffi: parent.target!));
|
.showMobileActionsOverlay(ffi: parent.target!);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -973,7 +975,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLastCursorId(Map<String, dynamic> evt) {
|
updateLastCursorId(Map<String, dynamic> evt) {
|
||||||
parent.target?.cursorModel.id = int.parse(evt['id']);
|
// int.parse(evt['id']) may cause FormatException
|
||||||
|
// Unhandled Exception: FormatException: Positive input exceeds the limit of integer 18446744071749110741
|
||||||
|
parent.target?.cursorModel.id = evt['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCursorId(Map<String, dynamic> evt) {
|
handleCursorId(Map<String, dynamic> evt) {
|
||||||
@@ -1185,10 +1189,11 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
onRgba(int display, Uint8List rgba) {
|
onRgba(int display, Uint8List rgba) {
|
||||||
final pid = parent.target?.id;
|
final pid = parent.target?.id;
|
||||||
|
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
|
||||||
img.decodeImageFromPixels(
|
img.decodeImageFromPixels(
|
||||||
rgba,
|
rgba,
|
||||||
parent.target?.ffiModel.rect?.width.toInt() ?? 0,
|
rect?.width.toInt() ?? 0,
|
||||||
parent.target?.ffiModel.rect?.height.toInt() ?? 0,
|
rect?.height.toInt() ?? 0,
|
||||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||||
onPixelsCopied: () {
|
onPixelsCopied: () {
|
||||||
// Unlock the rgba memory from rust codes.
|
// Unlock the rgba memory from rust codes.
|
||||||
@@ -1573,22 +1578,24 @@ class CanvasModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScale(double v) {
|
updateScale(double v, Offset focalPoint) {
|
||||||
if (parent.target?.imageModel.image == null) return;
|
if (parent.target?.imageModel.image == null) return;
|
||||||
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
|
final s = _scale;
|
||||||
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
|
||||||
final px0 = (offset.dx - r.left) * _scale;
|
|
||||||
final py0 = (offset.dy - r.top) * _scale;
|
|
||||||
_scale *= v;
|
_scale *= v;
|
||||||
final maxs = parent.target?.imageModel.maxScale ?? 1;
|
final maxs = parent.target?.imageModel.maxScale ?? 1;
|
||||||
final mins = parent.target?.imageModel.minScale ?? 1;
|
final mins = parent.target?.imageModel.minScale ?? 1;
|
||||||
if (_scale > maxs) _scale = maxs;
|
if (_scale > maxs) _scale = maxs;
|
||||||
if (_scale < mins) _scale = mins;
|
if (_scale < mins) _scale = mins;
|
||||||
r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
|
||||||
final px1 = (offset.dx - r.left) * _scale;
|
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
|
||||||
final py1 = (offset.dy - r.top) * _scale;
|
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
|
||||||
_x -= px1 - px0;
|
final adjustForKeyboard =
|
||||||
_y -= py1 - py0;
|
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;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,7 +1626,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
// data for cursor
|
// data for cursor
|
||||||
class CursorData {
|
class CursorData {
|
||||||
final String peerId;
|
final String peerId;
|
||||||
final int id;
|
final String id;
|
||||||
final img2.Image image;
|
final img2.Image image;
|
||||||
double scale;
|
double scale;
|
||||||
Uint8List? data;
|
Uint8List? data;
|
||||||
@@ -1699,12 +1706,12 @@ const _forbiddenCursorPng =
|
|||||||
const _defaultCursorPng =
|
const _defaultCursorPng =
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
||||||
|
|
||||||
const kPreForbiddenCursorId = -2;
|
const kPreForbiddenCursorId = "-2";
|
||||||
final preForbiddenCursor = PredefinedCursor(
|
final preForbiddenCursor = PredefinedCursor(
|
||||||
png: _forbiddenCursorPng,
|
png: _forbiddenCursorPng,
|
||||||
id: kPreForbiddenCursorId,
|
id: kPreForbiddenCursorId,
|
||||||
);
|
);
|
||||||
const kPreDefaultCursorId = -1;
|
const kPreDefaultCursorId = "-1";
|
||||||
final preDefaultCursor = PredefinedCursor(
|
final preDefaultCursor = PredefinedCursor(
|
||||||
png: _defaultCursorPng,
|
png: _defaultCursorPng,
|
||||||
id: kPreDefaultCursorId,
|
id: kPreDefaultCursorId,
|
||||||
@@ -1717,7 +1724,7 @@ class PredefinedCursor {
|
|||||||
img2.Image? _image2;
|
img2.Image? _image2;
|
||||||
CursorData? _cache;
|
CursorData? _cache;
|
||||||
String png;
|
String png;
|
||||||
int id;
|
String id;
|
||||||
double Function(double)? hotxGetter;
|
double Function(double)? hotxGetter;
|
||||||
double Function(double)? hotyGetter;
|
double Function(double)? hotyGetter;
|
||||||
|
|
||||||
@@ -1775,13 +1782,15 @@ class PredefinedCursor {
|
|||||||
|
|
||||||
class CursorModel with ChangeNotifier {
|
class CursorModel with ChangeNotifier {
|
||||||
ui.Image? _image;
|
ui.Image? _image;
|
||||||
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
final _images = <String, Tuple3<ui.Image, double, double>>{};
|
||||||
CursorData? _cache;
|
CursorData? _cache;
|
||||||
final _cacheMap = <int, CursorData>{};
|
final _cacheMap = <String, CursorData>{};
|
||||||
final _cacheKeys = <String>{};
|
final _cacheKeys = <String>{};
|
||||||
double _x = -10000;
|
double _x = -10000;
|
||||||
double _y = -10000;
|
double _y = -10000;
|
||||||
int _id = -1;
|
// int.parse(evt['id']) may cause FormatException
|
||||||
|
// So we use String here.
|
||||||
|
String _id = "-1";
|
||||||
double _hotx = 0;
|
double _hotx = 0;
|
||||||
double _hoty = 0;
|
double _hoty = 0;
|
||||||
double _displayOriginX = 0;
|
double _displayOriginX = 0;
|
||||||
@@ -1795,6 +1804,33 @@ class CursorModel with ChangeNotifier {
|
|||||||
String peerId = '';
|
String peerId = '';
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
// Only for mobile, touch mode
|
||||||
|
// To block touch event above the KeyHelpTools
|
||||||
|
//
|
||||||
|
// A better way is to not listen events from the KeyHelpTools.
|
||||||
|
// But we're now using a Container(child: Stack(...)) to wrap the KeyHelpTools,
|
||||||
|
// and the listener is on the Container.
|
||||||
|
Rect? _keyHelpToolsRect;
|
||||||
|
// `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;
|
||||||
|
|
||||||
|
keyHelpToolsVisibilityChanged(Rect? r) {
|
||||||
|
_keyHelpToolsRect = r;
|
||||||
|
if (r == null) {
|
||||||
|
_lastIsBlocked = false;
|
||||||
|
} else {
|
||||||
|
// Block the touch event is safe here.
|
||||||
|
// `lastIsBlocked` is only used in onDoubleTap() to block the touch event from the KeyHelpTools.
|
||||||
|
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
|
||||||
|
_lastIsBlocked = true;
|
||||||
|
}
|
||||||
|
_yForKeyboardAdjust = _y;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastIsBlocked => _lastIsBlocked;
|
||||||
|
|
||||||
ui.Image? get image => _image;
|
ui.Image? get image => _image;
|
||||||
CursorData? get cache => _cache;
|
CursorData? get cache => _cache;
|
||||||
|
|
||||||
@@ -1808,7 +1844,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
double get hotx => _hotx;
|
double get hotx => _hotx;
|
||||||
double get hoty => _hoty;
|
double get hoty => _hoty;
|
||||||
|
|
||||||
set id(int id) => _id = id;
|
set id(String id) => _id = id;
|
||||||
|
|
||||||
bool get isPeerControlProtected =>
|
bool get isPeerControlProtected =>
|
||||||
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
|
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
|
||||||
@@ -1839,28 +1875,52 @@ class CursorModel with ChangeNotifier {
|
|||||||
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
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() {
|
double adjustForKeyboard() {
|
||||||
|
if (keyboardHeight < 100) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
final m = MediaQueryData.fromWindow(ui.window);
|
final m = MediaQueryData.fromWindow(ui.window);
|
||||||
var keyboardHeight = m.viewInsets.bottom;
|
|
||||||
final size = m.size;
|
final size = m.size;
|
||||||
if (keyboardHeight < 100) return 0;
|
|
||||||
final s = parent.target?.canvasModel.scale ?? 1.0;
|
|
||||||
final thresh = (size.height - keyboardHeight) / 2;
|
final thresh = (size.height - keyboardHeight) / 2;
|
||||||
var h = (_y - getVisibleRect().top) * s; // local physical display height
|
final h = (_yForKeyboardAdjust - getVisibleRect().top) *
|
||||||
|
scale; // local physical display height
|
||||||
return h - thresh;
|
return h - thresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
move(double x, double y) {
|
// mobile Soft keyboard, block touch event from the KeyHelpTools
|
||||||
moveLocal(x, y);
|
shouldBlock(double x, double y) {
|
||||||
parent.target?.inputModel.moveMouse(_x, _y);
|
if (!(parent.target?.ffiModel.touchMode ?? false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_keyHelpToolsRect == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLocal(double x, double y) {
|
move(double x, double y) {
|
||||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
if (shouldBlock(x, y)) {
|
||||||
|
_lastIsBlocked = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_lastIsBlocked = false;
|
||||||
|
moveLocal(x, y, adjust: adjustForKeyboard());
|
||||||
|
parent.target?.inputModel.moveMouse(_x, _y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLocal(double x, double y, {double adjust = 0}) {
|
||||||
final xoffset = parent.target?.canvasModel.x ?? 0;
|
final xoffset = parent.target?.canvasModel.x ?? 0;
|
||||||
final yoffset = parent.target?.canvasModel.y ?? 0;
|
final yoffset = parent.target?.canvasModel.y ?? 0;
|
||||||
_x = (x - xoffset) / scale + _displayOriginX;
|
_x = (x - xoffset) / scale + _displayOriginX;
|
||||||
_y = (y - yoffset) / scale + _displayOriginY;
|
_y = (y - yoffset + adjust) / scale + _displayOriginY;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1986,7 +2046,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCursorData(Map<String, dynamic> evt) async {
|
updateCursorData(Map<String, dynamic> evt) async {
|
||||||
final id = int.parse(evt['id']);
|
final id = evt['id'];
|
||||||
final hotx = double.parse(evt['hotx']);
|
final hotx = double.parse(evt['hotx']);
|
||||||
final hoty = double.parse(evt['hoty']);
|
final hoty = double.parse(evt['hoty']);
|
||||||
final width = int.parse(evt['width']);
|
final width = int.parse(evt['width']);
|
||||||
@@ -2011,7 +2071,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
Future<bool> _updateCache(
|
Future<bool> _updateCache(
|
||||||
Uint8List rgba,
|
Uint8List rgba,
|
||||||
ui.Image image,
|
ui.Image image,
|
||||||
int id,
|
String id,
|
||||||
double hotx,
|
double hotx,
|
||||||
double hoty,
|
double hoty,
|
||||||
int w,
|
int w,
|
||||||
@@ -2393,9 +2453,10 @@ class FFI {
|
|||||||
cursorModel.peerId = id;
|
cursorModel.peerId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isNewPeer = tabWindowId == null;
|
||||||
// If tabWindowId != null, this session is a "tab -> window" one.
|
// If tabWindowId != null, this session is a "tab -> window" one.
|
||||||
// Else this session is a new one.
|
// Else this session is a new one.
|
||||||
if (tabWindowId == null) {
|
if (isNewPeer) {
|
||||||
// ignore: unused_local_variable
|
// ignore: unused_local_variable
|
||||||
final addRes = bind.sessionAddSync(
|
final addRes = bind.sessionAddSync(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
@@ -2414,20 +2475,32 @@ class FFI {
|
|||||||
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId);
|
final addRes = bind.sessionAddExistedSync(
|
||||||
|
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||||
if (addRes != '') {
|
if (addRes != '') {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'Unreachable, failed to add existed session to $id, $addRes');
|
'Unreachable, failed to add existed session to $id, $addRes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.sessionTryAddDisplay(
|
|
||||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
|
||||||
ffiModel.pi.currentDisplay = display;
|
ffiModel.pi.currentDisplay = display;
|
||||||
}
|
}
|
||||||
if (isDesktop && connType == ConnType.defaultConn) {
|
if (isDesktop && connType == ConnType.defaultConn) {
|
||||||
textureModel.updateCurrentDisplay(display ?? 0);
|
textureModel.updateCurrentDisplay(display ?? 0);
|
||||||
}
|
}
|
||||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
|
||||||
|
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
|
||||||
|
// Though the stream is returned immediately, the stream may not be ready.
|
||||||
|
// Any operations that depend on the stream should be carefully handled.
|
||||||
|
late final Stream<EventToUI> stream;
|
||||||
|
if (isNewPeer || display == null || displays == null) {
|
||||||
|
stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||||
|
} else {
|
||||||
|
// We have to put displays in `sessionStart()` to make sure the stream is ready
|
||||||
|
// and then the displays' capturing requests can be sent.
|
||||||
|
stream = bind.sessionStartWithDisplays(
|
||||||
|
sessionId: sessionId, id: id, displays: Int32List.fromList(displays));
|
||||||
|
}
|
||||||
|
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
platformFFI.setRgbaCallback((int display, Uint8List data) {
|
platformFFI.setRgbaCallback((int display, Uint8List data) {
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
@@ -2438,14 +2511,6 @@ class FFI {
|
|||||||
|
|
||||||
final cb = ffiModel.startEventListener(sessionId, id);
|
final cb = ffiModel.startEventListener(sessionId, id);
|
||||||
|
|
||||||
// Force refresh displays.
|
|
||||||
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
|
|
||||||
if (displays != null) {
|
|
||||||
for (final display in displays) {
|
|
||||||
bind.sessionRefresh(sessionId: sessionId, display: display);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageModel.updateUserTextureRender();
|
imageModel.updateUserTextureRender();
|
||||||
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
||||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||||
@@ -2496,20 +2561,23 @@ class FFI {
|
|||||||
}
|
}
|
||||||
} else if (message is EventToUI_Rgba) {
|
} else if (message is EventToUI_Rgba) {
|
||||||
final display = message.field0;
|
final display = message.field0;
|
||||||
if (imageModel.useTextureRender) {
|
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
|
||||||
debugPrint("EventToUI_Rgba display:$display");
|
//debugPrint("EventToUI_Rgba display:$display");
|
||||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
} else {
|
} else {
|
||||||
// Fetch the image buffer from rust codes.
|
// Fetch the image buffer from rust codes.
|
||||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||||
if (sz == 0) {
|
if (sz == 0) {
|
||||||
|
platformFFI.nextRgba(sessionId, display);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||||
if (rgba != null) {
|
if (rgba != null) {
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
imageModel.onRgba(display, rgba);
|
imageModel.onRgba(display, rgba);
|
||||||
|
} else {
|
||||||
|
platformFFI.nextRgba(sessionId, display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (message is EventToUI_Texture) {
|
} else if (message is EventToUI_Texture) {
|
||||||
@@ -2685,6 +2753,7 @@ class PeerInfo with ChangeNotifier {
|
|||||||
|
|
||||||
bool get isSupportMultiDisplay =>
|
bool get isSupportMultiDisplay =>
|
||||||
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
||||||
|
bool get forceTextureRender => currentDisplay == kAllDisplayValue;
|
||||||
|
|
||||||
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
||||||
|
|
||||||
@@ -2693,30 +2762,32 @@ class PeerInfo with ChangeNotifier {
|
|||||||
bool get isAmyuniIdd =>
|
bool get isAmyuniIdd =>
|
||||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||||
|
|
||||||
Display? tryGetDisplay() {
|
Display? tryGetDisplay({int? display}) {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay == kAllDisplayValue) {
|
display ??= currentDisplay;
|
||||||
|
if (display == kAllDisplayValue) {
|
||||||
return displays[0];
|
return displays[0];
|
||||||
} else {
|
} else {
|
||||||
if (currentDisplay > 0 && currentDisplay < displays.length) {
|
if (display > 0 && display < displays.length) {
|
||||||
return displays[currentDisplay];
|
return displays[display];
|
||||||
} else {
|
} else {
|
||||||
return displays[0];
|
return displays[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Display? tryGetDisplayIfNotAllDisplay() {
|
Display? tryGetDisplayIfNotAllDisplay({int? display}) {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay == kAllDisplayValue) {
|
display ??= currentDisplay;
|
||||||
|
if (display == kAllDisplayValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay >= 0 && currentDisplay < displays.length) {
|
if (display >= 0 && display < displays.length) {
|
||||||
return displays[currentDisplay];
|
return displays[display];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2740,6 +2811,12 @@ class PeerInfo with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect? getDisplayRect(int display) {
|
||||||
|
final d = tryGetDisplayIfNotAllDisplay(display: display);
|
||||||
|
if (d == null) return null;
|
||||||
|
return Rect.fromLTWH(d.x, d.y, d.width.toDouble(), d.height.toDouble());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasKey = 'canvas';
|
const canvasKey = 'canvas';
|
||||||
|
|||||||
@@ -132,9 +132,10 @@ class PlatformFFI {
|
|||||||
_ffiBind = RustdeskImpl(dylib);
|
_ffiBind = RustdeskImpl(dylib);
|
||||||
|
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
// Start a dbus service, no need to await
|
if (isMain) {
|
||||||
_ffiBind.mainStartDbusServer();
|
// Start a dbus service for uri links, no need to await
|
||||||
_ffiBind.mainStartPa();
|
_ffiBind.mainStartDbusServer();
|
||||||
|
}
|
||||||
} else if (isMacOS && isMain) {
|
} else if (isMacOS && isMain) {
|
||||||
// Start ipc service for uri links.
|
// Start ipc service for uri links.
|
||||||
_ffiBind.mainStartIpcUrlServer();
|
_ffiBind.mainStartIpcUrlServer();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -226,8 +227,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_approveMode = approveMode;
|
_approveMode = approveMode;
|
||||||
update = true;
|
update = true;
|
||||||
}
|
}
|
||||||
var stopped = option2bool(
|
var stopped = await mainGetBoolOption(kOptionStopService);
|
||||||
"stop-service", await bind.mainGetOption(key: "stop-service"));
|
|
||||||
final oldPwdText = _serverPasswd.text;
|
final oldPwdText = _serverPasswd.text;
|
||||||
if (stopped ||
|
if (stopped ||
|
||||||
verificationMethod == kUsePermanentPassword ||
|
verificationMethod == kUsePermanentPassword ||
|
||||||
@@ -340,6 +340,20 @@ class ServerModel with ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> checkFloatingWindowPermission() async {
|
||||||
|
debugPrint("androidVersion $androidVersion");
|
||||||
|
if (androidVersion < 23) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
|
debugPrint("alert window permission already granted");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var res = await AndroidPermissionManager.request(kSystemAlertWindow);
|
||||||
|
debugPrint("alert window permission request result: $res");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle the screen sharing service.
|
/// Toggle the screen sharing service.
|
||||||
toggleService() async {
|
toggleService() async {
|
||||||
if (_isStart) {
|
if (_isStart) {
|
||||||
@@ -367,6 +381,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await checkRequestNotificationPermission();
|
await checkRequestNotificationPermission();
|
||||||
|
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
||||||
|
await checkFloatingWindowPermission();
|
||||||
|
}
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
}
|
}
|
||||||
@@ -405,7 +422,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
await bind.mainStartService();
|
await bind.mainStartService();
|
||||||
updateClientState();
|
updateClientState();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
WakelockPlus.enable();
|
androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +515,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (_clients.length != oldClientLenght) {
|
if (_clients.length != oldClientLenght) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +550,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isAndroid && !client.authorized) showLoginDialog(client);
|
if (isAndroid && !client.authorized) showLoginDialog(client);
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to call loginRequest,error:$e");
|
debugPrint("Failed to call loginRequest,error:$e");
|
||||||
}
|
}
|
||||||
@@ -652,6 +671,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
final index = _clients.indexOf(client);
|
final index = _clients.indexOf(client);
|
||||||
tabController.remove(index);
|
tabController.remove(index);
|
||||||
_clients.remove(client);
|
_clients.remove(client);
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +695,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (desktopType == DesktopType.cm && _clients.isEmpty) {
|
if (desktopType == DesktopType.cm && _clients.isEmpty) {
|
||||||
hideCmWindow();
|
hideCmWindow();
|
||||||
}
|
}
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("onClientRemove failed,error:$e");
|
debugPrint("onClientRemove failed,error:$e");
|
||||||
@@ -686,6 +707,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
tabController.state.value.tabs.clear();
|
tabController.state.value.tabs.clear();
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
void jumpTo(int id) {
|
void jumpTo(int id) {
|
||||||
@@ -723,6 +745,27 @@ class ServerModel with ChangeNotifier {
|
|||||||
debugPrint("updateVoiceCallState failed: $e");
|
debugPrint("updateVoiceCallState failed: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void androidUpdatekeepScreenOn() async {
|
||||||
|
if (!isAndroid) return;
|
||||||
|
var floatingWindowDisabled =
|
||||||
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
|
final keepScreenOn = floatingWindowDisabled
|
||||||
|
? KeepScreenOn.never
|
||||||
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
||||||
|
final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
|
||||||
|
(keepScreenOn == KeepScreenOn.duringControlled &&
|
||||||
|
_clients.map((e) => !e.disconnected).isNotEmpty);
|
||||||
|
if (on != await WakelockPlus.enabled) {
|
||||||
|
if (on) {
|
||||||
|
WakelockPlus.enable();
|
||||||
|
} else {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ClientType {
|
enum ClientType {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ Future<ui.Image?> decodeImageFromPixels(
|
|||||||
int? rowBytes,
|
int? rowBytes,
|
||||||
int? targetWidth,
|
int? targetWidth,
|
||||||
int? targetHeight,
|
int? targetHeight,
|
||||||
VoidCallback? onPixelsCopied,
|
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
|
||||||
bool allowUpscaling = true,
|
bool allowUpscaling = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (targetWidth != null) {
|
if (targetWidth != null) {
|
||||||
assert(allowUpscaling || targetWidth <= width);
|
assert(allowUpscaling || targetWidth <= width);
|
||||||
if (!(allowUpscaling || targetWidth <= width)) {
|
if (!(allowUpscaling || targetWidth <= width)) {
|
||||||
print("not allow upscaling but targetWidth > width");
|
print("not allow upscaling but targetWidth > width");
|
||||||
|
onPixelsCopied?.call();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,7 @@ Future<ui.Image?> decodeImageFromPixels(
|
|||||||
assert(allowUpscaling || targetHeight <= height);
|
assert(allowUpscaling || targetHeight <= height);
|
||||||
if (!(allowUpscaling || targetHeight <= height)) {
|
if (!(allowUpscaling || targetHeight <= height)) {
|
||||||
print("not allow upscaling but targetHeight > height");
|
print("not allow upscaling but targetHeight > height");
|
||||||
|
onPixelsCopied?.call();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +38,7 @@ Future<ui.Image?> decodeImageFromPixels(
|
|||||||
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||||
onPixelsCopied?.call();
|
onPixelsCopied?.call();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
onPixelsCopied?.call();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,15 +59,13 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String sessionAddExistedSync(
|
String sessionAddExistedSync(
|
||||||
{required String id, required UuidValue sessionId, dynamic hint}) {
|
{required String id,
|
||||||
|
required UuidValue sessionId,
|
||||||
|
required Int32List displays,
|
||||||
|
dynamic hint}) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void sessionTryAddDisplay(
|
|
||||||
{required UuidValue sessionId,
|
|
||||||
required Int32List displays,
|
|
||||||
dynamic hint}) {}
|
|
||||||
|
|
||||||
String sessionAddSync(
|
String sessionAddSync(
|
||||||
{required UuidValue sessionId,
|
{required UuidValue sessionId,
|
||||||
required String id,
|
required String id,
|
||||||
@@ -94,6 +92,14 @@ class RustdeskImpl {
|
|||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<EventToUI> sessionStartWithDisplays(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required String id,
|
||||||
|
required Int32List displays,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool?> sessionGetRemember(
|
Future<bool?> sessionGetRemember(
|
||||||
{required UuidValue sessionId, dynamic hint}) {
|
{required UuidValue sessionId, dynamic hint}) {
|
||||||
return Future(
|
return Future(
|
||||||
@@ -937,7 +943,7 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
String mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,10 +1412,6 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mainStartPa({dynamic hint}) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mainHideDocker({dynamic hint}) {
|
bool mainHideDocker({dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
|
resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3"
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -790,13 +790,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.18.1"
|
version: "0.19.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.2.5+39
|
version: 1.2.6+44
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -114,6 +114,9 @@ dev_dependencies:
|
|||||||
flutter_lints: ^2.0.2
|
flutter_lints: ^2.0.2
|
||||||
ffigen: ^8.0.2
|
ffigen: ^8.0.2
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
intl: ^0.19.0
|
||||||
|
|
||||||
# rerun: flutter pub run flutter_launcher_icons
|
# rerun: flutter pub run flutter_launcher_icons
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
image_path: "../res/icon.png"
|
image_path: "../res/icon.png"
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ flutter pub get
|
|||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
||||||
# call `flutter clean` if cargo build fails
|
# call `flutter clean` if cargo build fails
|
||||||
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
||||||
cargo build --features "flutter,flutter_texture_render"
|
cargo build --features flutter
|
||||||
flutter run $@
|
flutter run $@
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# RustDesk web
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
### Current and planned
|
|
||||||
|
|
||||||
- [x] Outgoing
|
|
||||||
- [ ] Address book
|
|
||||||
- [ ] Force relay
|
|
||||||
|
|
||||||
### Unsupported
|
|
||||||
|
|
||||||
1. Incoming
|
|
||||||
2. LAN
|
|
||||||
3. Gpu texture render. We use WebGL instead.
|
|
||||||
|
|
||||||
### No plans
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg viewBox="0 0 375 375" style="width:32px;height:32px;margin:0 4px 4px 0" xmlns="http://www.w3.org/2000/svg"><rect transform="matrix(.91553 0 0 .91553 -152.92 116.76)" x="167.03" y="-127.54" width="409.6" height="409.6" rx="64" ry="64" fill="#0071ff"></rect><path d="M150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,19 +0,0 @@
|
|||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY = 'Desktop session not ready';
|
|
||||||
export const LOGIN_MSG_DESKTOP_XSESSION_FAILED = 'Desktop xsession failed';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER = 'Desktop session another user login';
|
|
||||||
export const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND = 'Desktop xorg not found';
|
|
||||||
// ls /usr/share/xsessions/
|
|
||||||
export const LOGIN_MSG_DESKTOP_NO_DESKTOP = 'Desktop none';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY =
|
|
||||||
'Desktop session not ready, password empty';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG =
|
|
||||||
'Desktop session not ready, password wrong';
|
|
||||||
export const LOGIN_MSG_PASSWORD_EMPTY = 'Empty Password';
|
|
||||||
export const LOGIN_MSG_PASSWORD_WRONG = 'Wrong Password';
|
|
||||||
export const LOGIN_MSG_2FA_WRONG = 'Wrong 2FA Code';
|
|
||||||
export const REQUIRE_2FA = '2FA Required';
|
|
||||||
export const LOGIN_MSG_NO_PASSWORD_ACCESS = 'No Password Access';
|
|
||||||
export const LOGIN_MSG_OFFLINE = 'Offline';
|
|
||||||
export const LOGIN_SCREEN_WAYLAND = 'Wayland login screen is not supported';
|
|
||||||
export const SCRAP_X11_REQUIRED = 'x11 expected';
|
|
||||||
export const SCRAP_X11_REF_URL = 'https://rustdesk.com/docs/en/manual/linux/#x11-required';
|
|
||||||
@@ -1,792 +0,0 @@
|
|||||||
import Connection from "./connection";
|
|
||||||
import PORT from "./connection";
|
|
||||||
import _sodium from "libsodium-wrappers";
|
|
||||||
import { CursorData } from "./message";
|
|
||||||
import { loadVp9 } from "./codec";
|
|
||||||
import { checkIfRetry, version } from "./gen_js_from_hbb";
|
|
||||||
import { initZstd, translate } from "./common";
|
|
||||||
import PCMPlayer from "pcm-player";
|
|
||||||
|
|
||||||
window.curConn = undefined;
|
|
||||||
window.isMobile = () => {
|
|
||||||
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
|
||||||
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDesktop() {
|
|
||||||
return !isMobile();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function msgbox(type, title, text, link) {
|
|
||||||
if (!type || (type == 'error' && !text)) return;
|
|
||||||
const text2 = text.toLowerCase();
|
|
||||||
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
|
|
||||||
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, link: link ?? '', hasRetry }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function jsonfyForDart(payload) {
|
|
||||||
var tmp = {};
|
|
||||||
for (const [key, value] of Object.entries(payload)) {
|
|
||||||
if (!key) continue;
|
|
||||||
if (value instanceof String || typeof value == 'string') {
|
|
||||||
tmp[key] = value;
|
|
||||||
} else if (value instanceof Uint8Array) {
|
|
||||||
tmp[key] = '[' + value.toString() + ']';
|
|
||||||
} else {
|
|
||||||
tmp[key] = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pushEvent(name, payload) {
|
|
||||||
payload = jsonfyForDart(payload);
|
|
||||||
payload.name = name;
|
|
||||||
onGlobalEvent(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
let yuvWorker;
|
|
||||||
let yuvCanvas;
|
|
||||||
let gl;
|
|
||||||
let pixels;
|
|
||||||
let flipPixels;
|
|
||||||
let oldSize;
|
|
||||||
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
|
|
||||||
gl = canvas.getContext("webgl");
|
|
||||||
} else {
|
|
||||||
yuvWorker = new Worker("./yuv.js");
|
|
||||||
}
|
|
||||||
let testSpeed = [0, 0];
|
|
||||||
|
|
||||||
export function draw(display, frame) {
|
|
||||||
if (yuvWorker) {
|
|
||||||
// frame's (y/u/v).bytes already detached, can not transferrable any more.
|
|
||||||
yuvWorker.postMessage({ display, frame });
|
|
||||||
} else {
|
|
||||||
var tm0 = new Date().getTime();
|
|
||||||
yuvCanvas.drawFrame(frame);
|
|
||||||
var width = canvas.width;
|
|
||||||
var height = canvas.height;
|
|
||||||
var size = width * height * 4;
|
|
||||||
if (size != oldSize) {
|
|
||||||
pixels = new Uint8Array(size);
|
|
||||||
flipPixels = new Uint8Array(size);
|
|
||||||
oldSize = size;
|
|
||||||
}
|
|
||||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
||||||
const row = width * 4;
|
|
||||||
const end = (height - 1) * row;
|
|
||||||
for (let i = 0; i < size; i += row) {
|
|
||||||
flipPixels.set(pixels.subarray(i, i + row), end - i);
|
|
||||||
}
|
|
||||||
onRgba(display, flipPixels);
|
|
||||||
testSpeed[1] += new Date().getTime() - tm0;
|
|
||||||
testSpeed[0] += 1;
|
|
||||||
if (testSpeed[0] > 30) {
|
|
||||||
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
|
||||||
testSpeed = [0, 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
|
|
||||||
if (testCanvas && currentFrame) {
|
|
||||||
var ctx = testCanvas.getContext("2d");
|
|
||||||
testCanvas.width = frame.format.displayWidth;
|
|
||||||
testCanvas.height = frame.format.displayHeight;
|
|
||||||
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
|
|
||||||
img.data.set(currentFrame);
|
|
||||||
ctx.putImageData(img, 0, 0);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendOffCanvas(c) {
|
|
||||||
let canvas = c.transferControlToOffscreen();
|
|
||||||
yuvWorker.postMessage({ canvas }, [canvas]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setConn(conn) {
|
|
||||||
window.curConn = conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConn() {
|
|
||||||
return window.curConn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startConn(id) {
|
|
||||||
setByName('remote_id', id);
|
|
||||||
await curConn.start(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close() {
|
|
||||||
getConn()?.close();
|
|
||||||
setConn(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function newConn() {
|
|
||||||
window.curConn?.close();
|
|
||||||
const conn = new Connection();
|
|
||||||
setConn(conn);
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sodium;
|
|
||||||
export async function verify(signed, pk) {
|
|
||||||
if (!sodium) {
|
|
||||||
await _sodium.ready;
|
|
||||||
sodium = _sodium;
|
|
||||||
}
|
|
||||||
if (typeof pk == 'string') {
|
|
||||||
pk = decodeBase64(pk);
|
|
||||||
}
|
|
||||||
return sodium.crypto_sign_open(signed, pk);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeBase64(pk) {
|
|
||||||
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genBoxKeyPair() {
|
|
||||||
const pair = sodium.crypto_box_keypair();
|
|
||||||
const sk = pair.privateKey;
|
|
||||||
const pk = pair.publicKey;
|
|
||||||
return [sk, pk];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genSecretKey() {
|
|
||||||
return sodium.crypto_secretbox_keygen();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function seal(unsigned, theirPk, ourSk) {
|
|
||||||
const nonce = Uint8Array.from(Array(24).fill(0));
|
|
||||||
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeOnce(value) {
|
|
||||||
var byteArray = Array(24).fill(0);
|
|
||||||
|
|
||||||
for (var index = 0; index < byteArray.length && value > 0; index++) {
|
|
||||||
var byte = value & 0xff;
|
|
||||||
byteArray[index] = byte;
|
|
||||||
value = (value - byte) / 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uint8Array.from(byteArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function encrypt(unsigned, nonce, key) {
|
|
||||||
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrypt(signed, nonce, key) {
|
|
||||||
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.setByName = (name, value) => {
|
|
||||||
switch (name) {
|
|
||||||
case 'remote_id':
|
|
||||||
localStorage.setItem('remote-id', value);
|
|
||||||
break;
|
|
||||||
case 'connect':
|
|
||||||
newConn();
|
|
||||||
startConn(value);
|
|
||||||
break;
|
|
||||||
case 'login':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.setRemember(value.remember);
|
|
||||||
curConn.login({
|
|
||||||
os_login: {
|
|
||||||
username: value.os_username,
|
|
||||||
password: value.os_password,
|
|
||||||
},
|
|
||||||
password: value.password,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'close':
|
|
||||||
close();
|
|
||||||
break;
|
|
||||||
case 'refresh':
|
|
||||||
curConn.refresh();
|
|
||||||
break;
|
|
||||||
case 'reconnect':
|
|
||||||
curConn?.reconnect();
|
|
||||||
break;
|
|
||||||
case 'toggle_option':
|
|
||||||
curConn.toggleOption(value);
|
|
||||||
break;
|
|
||||||
case 'toggle_privacy_mode':
|
|
||||||
curConn.togglePrivacyMode(value);
|
|
||||||
break;
|
|
||||||
case 'image_quality':
|
|
||||||
curConn.setImageQuality(value);
|
|
||||||
break;
|
|
||||||
case 'lock_screen':
|
|
||||||
curConn.lockScreen();
|
|
||||||
break;
|
|
||||||
case 'ctrl_alt_del':
|
|
||||||
curConn.ctrlAltDel();
|
|
||||||
break;
|
|
||||||
case 'switch_display':
|
|
||||||
curConn.switchDisplay(value);
|
|
||||||
break;
|
|
||||||
case 'remove':
|
|
||||||
const peers = getPeers();
|
|
||||||
delete peers[value];
|
|
||||||
localStorage.setItem('peers', JSON.stringify(peers));
|
|
||||||
break;
|
|
||||||
case 'input_key':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
|
||||||
break;
|
|
||||||
case 'input_string':
|
|
||||||
curConn.inputString(value);
|
|
||||||
break;
|
|
||||||
case 'send_mouse':
|
|
||||||
if (!curConn) return;
|
|
||||||
let mask = 0;
|
|
||||||
value = JSON.parse(value);
|
|
||||||
switch (value.type) {
|
|
||||||
case 'down':
|
|
||||||
mask = 1;
|
|
||||||
break;
|
|
||||||
case 'up':
|
|
||||||
mask = 2;
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
mask = 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (value.buttons) {
|
|
||||||
case 'left':
|
|
||||||
mask |= 1 << 3;
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
mask |= 2 << 3;
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
mask |= 4 << 3;
|
|
||||||
}
|
|
||||||
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
|
||||||
break;
|
|
||||||
case 'send_2fa':
|
|
||||||
curConn.send2fa(value);
|
|
||||||
break;
|
|
||||||
case 'option':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
localStorage.setItem(value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'options':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
for (const [key, value] of Object.entries(value)) {
|
|
||||||
localStorage.setItem(key, value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'option:local':
|
|
||||||
case 'option:flutter:local':
|
|
||||||
case 'option:flutter:peer':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
localStorage.setItem(name + ':' + value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'option:user:default':
|
|
||||||
setUserDefaultOption(value);
|
|
||||||
break;
|
|
||||||
case 'option:session':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.setOption(value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'option:peer':
|
|
||||||
setPeerOption(value);
|
|
||||||
break;
|
|
||||||
case 'option:toggle':
|
|
||||||
return curConn.toggleOption(value);
|
|
||||||
case 'input_os_password':
|
|
||||||
curConn.inputOsPassword(value);
|
|
||||||
break;
|
|
||||||
case 'session_add_sync':
|
|
||||||
return sessionAdd(value);
|
|
||||||
case 'session_start':
|
|
||||||
sessionStart(value);
|
|
||||||
break;
|
|
||||||
case 'session_close':
|
|
||||||
sessionClose(value);
|
|
||||||
break;
|
|
||||||
case 'elevate_with_logon':
|
|
||||||
curConn.elevateWithLogon(value);
|
|
||||||
break;
|
|
||||||
case 'forget':
|
|
||||||
curConn.setRemember(false);
|
|
||||||
break;
|
|
||||||
case 'peer_has_password':
|
|
||||||
const options = getPeers()[value] || {};
|
|
||||||
return (options['password'] ?? '') !== '';
|
|
||||||
case 'peer_exists':
|
|
||||||
return !(!getPeers()[value]);
|
|
||||||
case 'restart':
|
|
||||||
curConn.restart();
|
|
||||||
break;
|
|
||||||
case 'fav':
|
|
||||||
return localStorage.setItem('fav', value);
|
|
||||||
case 'query_onlines':
|
|
||||||
queryOnlines(value);
|
|
||||||
break;
|
|
||||||
case 'change_prefer_codec':
|
|
||||||
curConn.changePreferCodec(value);
|
|
||||||
case 'cursor':
|
|
||||||
setCustomCursor(value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.getByName = (name, arg) => {
|
|
||||||
let v = _getByName(name, arg);
|
|
||||||
if (typeof v == 'string' || v instanceof String) return v;
|
|
||||||
if (v == undefined || v == null) return '';
|
|
||||||
return JSON.stringify(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getByName(name, arg) {
|
|
||||||
switch (name) {
|
|
||||||
case 'remote_id':
|
|
||||||
return localStorage.getItem('remote-id');
|
|
||||||
case 'remember':
|
|
||||||
return curConn.getRemember();
|
|
||||||
case 'toggle_option':
|
|
||||||
return curConn.getOption(arg) || false;
|
|
||||||
case 'option':
|
|
||||||
return localStorage.getItem(arg);
|
|
||||||
case 'options':
|
|
||||||
const keys = [
|
|
||||||
'custom-rendezvous-server',
|
|
||||||
'relay-server',
|
|
||||||
'api-server',
|
|
||||||
'key'
|
|
||||||
];
|
|
||||||
const obj = {};
|
|
||||||
keys.forEach(key => {
|
|
||||||
const v = localStorage.getItem(key);
|
|
||||||
if (v) obj[key] = v;
|
|
||||||
});
|
|
||||||
return JSON.stringify(obj);
|
|
||||||
case 'option:local':
|
|
||||||
case 'option:flutter:local':
|
|
||||||
case 'option:flutter:peer':
|
|
||||||
return localStorage.getItem(name + ':' + arg);
|
|
||||||
case 'image_quality':
|
|
||||||
return curConn.getImageQuality();
|
|
||||||
case 'translate':
|
|
||||||
arg = JSON.parse(arg);
|
|
||||||
return translate(arg.locale, arg.text);
|
|
||||||
case 'option:user:default':
|
|
||||||
return getUserDefaultOption(arg);
|
|
||||||
case 'option:session':
|
|
||||||
if (curConn) {
|
|
||||||
return curConn.getOption(arg);
|
|
||||||
} else {
|
|
||||||
return getUserDefaultOption(arg);
|
|
||||||
}
|
|
||||||
case 'option:peer':
|
|
||||||
return getPeerOption(arg);
|
|
||||||
case 'option:toggle':
|
|
||||||
return curConn.getToggleOption(arg);
|
|
||||||
case 'get_conn_status':
|
|
||||||
if (curConn) {
|
|
||||||
return curConn.getStatus();
|
|
||||||
} else {
|
|
||||||
return JSON.stringify({ status_num: 0 });
|
|
||||||
}
|
|
||||||
case 'test_if_valid_server':
|
|
||||||
break;
|
|
||||||
case 'version':
|
|
||||||
return version;
|
|
||||||
case 'load_recent_peers':
|
|
||||||
loadRecentPeers();
|
|
||||||
break;
|
|
||||||
case 'load_fav_peers':
|
|
||||||
loadFavPeers();
|
|
||||||
break;
|
|
||||||
case 'fav':
|
|
||||||
return localStorage.getItem('fav') ?? '[]';
|
|
||||||
case 'load_recent_peers_sync':
|
|
||||||
return JSON.stringify({
|
|
||||||
peers: JSON.stringify(getRecentPeers())
|
|
||||||
});
|
|
||||||
case 'api_server':
|
|
||||||
return getApiServer();
|
|
||||||
case 'is_using_public_server':
|
|
||||||
return !localStorage.getItem('custom-rendezvous-server');
|
|
||||||
case 'get_version_number':
|
|
||||||
return getVersionNumber(arg);
|
|
||||||
case 'audit_server':
|
|
||||||
return getAuditServer(arg);
|
|
||||||
case 'alternative_codecs':
|
|
||||||
return getAlternativeCodecs();
|
|
||||||
case 'screen_info':
|
|
||||||
return JSON.stringify({
|
|
||||||
frame: {
|
|
||||||
l: window.screenX,
|
|
||||||
t: window.screenY,
|
|
||||||
r: window.screenX + window.innerWidth,
|
|
||||||
b: window.screenY + window.innerHeight,
|
|
||||||
},
|
|
||||||
visibleFrame: {
|
|
||||||
l: window.screen.availLeft,
|
|
||||||
t: window.screen.availTop,
|
|
||||||
r: window.screen.availLeft + window.screen.availWidth,
|
|
||||||
b: window.screen.availTop + window.screen.availHeight,
|
|
||||||
},
|
|
||||||
scaleFactor: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
case 'main_display':
|
|
||||||
return JSON.stringify({
|
|
||||||
w: window.screen.availWidth,
|
|
||||||
h: window.screen.availHeight,
|
|
||||||
scaleFactor: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let opusWorker = new Worker("./libopus.js");
|
|
||||||
let pcmPlayer;
|
|
||||||
|
|
||||||
export function initAudio(channels, sampleRate) {
|
|
||||||
pcmPlayer = newAudioPlayer(channels, sampleRate);
|
|
||||||
opusWorker.postMessage({ channels, sampleRate });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function playAudio(packet) {
|
|
||||||
opusWorker.postMessage(packet, [packet.buffer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.init = async () => {
|
|
||||||
if (yuvWorker) {
|
|
||||||
yuvWorker.onmessage = (e) => {
|
|
||||||
onRgba(e.data.display, e.data.frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opusWorker.onmessage = (e) => {
|
|
||||||
pcmPlayer.feed(e.data);
|
|
||||||
}
|
|
||||||
loadVp9(() => { });
|
|
||||||
await initZstd();
|
|
||||||
console.log('init done');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPeers() {
|
|
||||||
return getJsonObj('peers');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getJsonObj(key) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(key)) || {};
|
|
||||||
} catch (e) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function newAudioPlayer(channels, sampleRate) {
|
|
||||||
return new PCMPlayer({
|
|
||||||
channels,
|
|
||||||
sampleRate,
|
|
||||||
flushingTime: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copyToClipboard(text) {
|
|
||||||
if (window.clipboardData && window.clipboardData.setData) {
|
|
||||||
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
|
|
||||||
return window.clipboardData.setData("Text", text);
|
|
||||||
}
|
|
||||||
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
|
|
||||||
var textarea = document.createElement("textarea");
|
|
||||||
textarea.textContent = text;
|
|
||||||
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
try {
|
|
||||||
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.warn("Copy to clipboard failed.", ex);
|
|
||||||
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dup to the function in hbb_common, lib.rs
|
|
||||||
// Maybe we need to move this function to js part.
|
|
||||||
export function getVersionNumber(v) {
|
|
||||||
try {
|
|
||||||
let versions = v.split('-');
|
|
||||||
|
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
// The first part is the version number.
|
|
||||||
// 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10
|
|
||||||
// to leave space for patch version.
|
|
||||||
if (versions.length > 0) {
|
|
||||||
let last = 0;
|
|
||||||
for (let x of versions[0].split('.')) {
|
|
||||||
last = parseInt(x) || 0;
|
|
||||||
n = n * 1000 + last;
|
|
||||||
}
|
|
||||||
n -= last;
|
|
||||||
n += last * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versions.length > 1) {
|
|
||||||
n += parseInt(versions[1]) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore the rest
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to parse version number: "' + v + '" ' + e.message);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cursor for the flutter-view element
|
|
||||||
function setCustomCursor(value) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
// document querySelector or evaluate can not find the custom element
|
|
||||||
var body = document.body;
|
|
||||||
for (var i = 0; i < body.children.length; i++) {
|
|
||||||
var child = body.children[i];
|
|
||||||
if (child.tagName == 'FLUTTER-VIEW') {
|
|
||||||
child.style.cursor = `url(${obj.url}) ${obj.hotx} ${obj.hoty}, auto`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to set custom cursor: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================== options begin ==========================
|
|
||||||
function setUserDefaultOption(value) {
|
|
||||||
try {
|
|
||||||
const ojb = JSON.parse(value);
|
|
||||||
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
|
|
||||||
userDefaultOptions[ojb.name] = ojb.value;
|
|
||||||
localStorage.setItem('user-default-options', JSON.stringify(userDefaultOptions));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to set user default options: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserDefaultOption(value) {
|
|
||||||
const defaultOptions = {
|
|
||||||
'view_style': 'original',
|
|
||||||
'scroll_style': 'scrollauto',
|
|
||||||
'image_quality': 'balanced',
|
|
||||||
'codec-preference': 'auto',
|
|
||||||
'custom_image_quality': '50',
|
|
||||||
'custom-fps': '30',
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
|
|
||||||
return userDefaultOptions[value] || defaultOptions[value] || '';
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to get user default options: ' + e.message);
|
|
||||||
return defaultOptions[value] || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPeerOption(value) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
const options = getPeers()[obj.id] || {};
|
|
||||||
return options[obj.name] ?? getUserDefaultOption(obj.name);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to get peer option: "' + value + '", ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPeerOption(param) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(param);
|
|
||||||
const id = obj.id;
|
|
||||||
const name = obj.name;
|
|
||||||
const value = obj.value;
|
|
||||||
const peers = getPeers();
|
|
||||||
const options = peers[id] || {};
|
|
||||||
|
|
||||||
if (value == undefined) {
|
|
||||||
delete options[name];
|
|
||||||
} else {
|
|
||||||
options[name] = value;
|
|
||||||
}
|
|
||||||
options["tm"] = new Date().getTime();
|
|
||||||
peers[id] = options;
|
|
||||||
localStorage.setItem("peers", JSON.stringify(peers));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to set peer option: "' + value + '", ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ========================= options end ===========================
|
|
||||||
|
|
||||||
// ========================== peers begin ==========================
|
|
||||||
function getRecentPeers() {
|
|
||||||
const peers = [];
|
|
||||||
for (const [id, value] of Object.entries(getPeers())) {
|
|
||||||
if (!id) continue;
|
|
||||||
const tm = value['tm'];
|
|
||||||
const info = value['info'];
|
|
||||||
const cardInfo = {
|
|
||||||
id: id,
|
|
||||||
username: info['username'] || '',
|
|
||||||
hostname: info['hostname'] || '',
|
|
||||||
platform: info['platform'] || '',
|
|
||||||
alias: value.alias || '',
|
|
||||||
};
|
|
||||||
if (!tm || !cardInfo) continue;
|
|
||||||
peers.push([tm, id, cardInfo]);
|
|
||||||
}
|
|
||||||
return peers.sort().reverse().map(x => x[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadRecentPeers() {
|
|
||||||
const peersRecent = getRecentPeers();
|
|
||||||
if (peersRecent) {
|
|
||||||
onRegisteredEvent(JSON.stringify({ name: 'load_recent_peers', peers: JSON.stringify(peersRecent) }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFavPeers() {
|
|
||||||
try {
|
|
||||||
const fav = localStorage.getItem('fav') ?? '[]';
|
|
||||||
const favs = JSON.parse(fav);
|
|
||||||
const peersFav = getRecentPeers().filter(x => favs.includes(x.id));
|
|
||||||
if (peersFav) {
|
|
||||||
onRegisteredEvent(JSON.stringify({ name: 'load_fav_peers', peers: JSON.stringify(peersFav) }));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load fav peers: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryOnlines(value) {
|
|
||||||
// TODO: implement this
|
|
||||||
}
|
|
||||||
// ========================== peers end ===========================
|
|
||||||
|
|
||||||
// ========================== session begin ==========================
|
|
||||||
function sessionAdd(value) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(value);
|
|
||||||
window.curConn?.close();
|
|
||||||
const conn = new Connection();
|
|
||||||
setConn(conn);
|
|
||||||
return '';
|
|
||||||
} catch (e) {
|
|
||||||
return e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionStart(value) {
|
|
||||||
try {
|
|
||||||
const conn = getConn();
|
|
||||||
if (!conn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(value);
|
|
||||||
if (data['id']) {
|
|
||||||
startConn(data['id']);
|
|
||||||
} else {
|
|
||||||
msgbox('error', 'Error', 'No id found in session data ' + value, '');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: better error handling
|
|
||||||
msgbox('error', 'Error', e.message, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionClose(value) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
// ========================== session end ===========================
|
|
||||||
|
|
||||||
// ========================== settings begin ==========================
|
|
||||||
function increasePort(host, offset) {
|
|
||||||
function isIPv6(str) {
|
|
||||||
const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$/;
|
|
||||||
return ipv6Pattern.test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isIPv6(host)) {
|
|
||||||
if (host.startsWith('[')) {
|
|
||||||
let tmp = host.split(']:');
|
|
||||||
if (tmp.length === 2) {
|
|
||||||
let port = parseInt(tmp[1]) || 0;
|
|
||||||
if (port > 0) {
|
|
||||||
return `${tmp[0]}]:${port + offset}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (host.includes(':')) {
|
|
||||||
let tmp = host.split(':');
|
|
||||||
if (tmp.length === 2) {
|
|
||||||
let port = parseInt(tmp[1]) || 0;
|
|
||||||
if (port > 0) {
|
|
||||||
return `${tmp[0]}:${port + offset}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlternativeCodecs() {
|
|
||||||
return JSON.stringify({
|
|
||||||
vp8: true,
|
|
||||||
av1: false,
|
|
||||||
h264: false,
|
|
||||||
h265: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ========================== settings end ===========================
|
|
||||||
|
|
||||||
// ========================== server begin ==========================
|
|
||||||
function getApiServer() {
|
|
||||||
const api_server = localStorage.getItem('api-server');
|
|
||||||
if (api_server) {
|
|
||||||
return api_server;
|
|
||||||
}
|
|
||||||
|
|
||||||
const custom_rendezvous_server = localStorage.getItem('custom-rendezvous-server');
|
|
||||||
if (custom_rendezvous_server) {
|
|
||||||
let s = increasePort(custom_rendezvous_server, -2);
|
|
||||||
if (s == custom_rendezvous_server) {
|
|
||||||
return `http://${s}:${PORT - 2}`;
|
|
||||||
} else {
|
|
||||||
return `http://${s}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'https://admin.rustdesk.com';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuditServer(typ) {
|
|
||||||
if (!localStorage.getItem("access_token")) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const api_server = getApiServer();
|
|
||||||
if (!api_server || api_server.includes('rustdesk.com')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return api_server + '/api/audit/' + typ;
|
|
||||||
}
|
|
||||||
// ========================== server end ============================
|
|
||||||
1
flutter/web/v1/README.md
Normal file
1
flutter/web/v1/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v1 is not compatible with current Flutter source code.
|
||||||
@@ -3,19 +3,19 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "python ./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && python ./ts_proto.py && tsc && vite build",
|
"build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4",
|
||||||
"vite": "2.8"
|
"vite": "^2.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-sha256": "^1.3.0",
|
"fast-sha256": "^1.3.0",
|
||||||
"libsodium": "^0.7.9",
|
"libsodium": "^0.7.9",
|
||||||
"libsodium-wrappers": "^0.7.9",
|
"libsodium-wrappers": "^0.7.9",
|
||||||
"pcm-player": "^0.0.11",
|
"pcm-player": "^0.0.11",
|
||||||
"ts-proto": "^1.169.1",
|
"ts-proto": "^1.101.0",
|
||||||
"wasm-feature-detect": "^1.2.11",
|
"wasm-feature-detect": "^1.2.11",
|
||||||
"zstddec": "^0.0.2"
|
"zstddec": "^0.0.2"
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export async function loadVp9(callback) {
|
|||||||
// Multithreading is used only if `options.threading` is true.
|
// Multithreading is used only if `options.threading` is true.
|
||||||
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
|
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
|
||||||
// currently available in Firefox and Chrome with experimental flags enabled.
|
// currently available in Firefox and Chrome with experimental flags enabled.
|
||||||
// All major browsers disabled SharedArrayBuffer by default on January 5, 2018
|
// 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer
|
||||||
const isSIMD = await simd();
|
const isSIMD = await simd();
|
||||||
console.log('isSIMD: ' + isSIMD);
|
console.log('isSIMD: ' + isSIMD);
|
||||||
window.OGVLoader.loadClass(
|
window.OGVLoader.loadClass(
|
||||||
@@ -4,10 +4,9 @@ import * as rendezvous from "./rendezvous.js";
|
|||||||
import { loadVp9 } from "./codec";
|
import { loadVp9 } from "./codec";
|
||||||
import * as sha256 from "fast-sha256";
|
import * as sha256 from "fast-sha256";
|
||||||
import * as globals from "./globals";
|
import * as globals from "./globals";
|
||||||
import * as consts from "./consts";
|
|
||||||
import { decompress, mapKey, sleep } from "./common";
|
import { decompress, mapKey, sleep } from "./common";
|
||||||
|
|
||||||
export const PORT = 21116;
|
const PORT = 21116;
|
||||||
const HOSTS = [
|
const HOSTS = [
|
||||||
"rs-sg.rustdesk.com",
|
"rs-sg.rustdesk.com",
|
||||||
"rs-cn.rustdesk.com",
|
"rs-cn.rustdesk.com",
|
||||||
@@ -16,8 +15,8 @@ const HOSTS = [
|
|||||||
let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
|
let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
|
||||||
const SCHEMA = "ws://";
|
const SCHEMA = "ws://";
|
||||||
|
|
||||||
type MsgboxCallback = (type: string, title: string, text: string, link: string) => void;
|
type MsgboxCallback = (type: string, title: string, text: string) => void;
|
||||||
type DrawCallback = (display: number, data: Uint8Array) => void;
|
type DrawCallback = (data: Uint8Array) => void;
|
||||||
//const cursorCanvas = document.createElement("canvas");
|
//const cursorCanvas = document.createElement("canvas");
|
||||||
|
|
||||||
export default class Connection {
|
export default class Connection {
|
||||||
@@ -67,7 +66,7 @@ export default class Connection {
|
|||||||
try {
|
try {
|
||||||
this._password = Uint8Array.from(JSON.parse("[" + p + "]"));
|
this._password = Uint8Array.from(JSON.parse("[" + p + "]"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to get password, ' + e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +170,7 @@ export default class Connection {
|
|||||||
pk = undefined;
|
pk = undefined;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to verify id pk, ', e);
|
console.error(e);
|
||||||
pk = undefined;
|
pk = undefined;
|
||||||
}
|
}
|
||||||
if (!pk)
|
if (!pk)
|
||||||
@@ -196,7 +195,7 @@ export default class Connection {
|
|||||||
try {
|
try {
|
||||||
signedId = await globals.verify(signedId.id, Uint8Array.from(pk!));
|
signedId = await globals.verify(signedId.id, Uint8Array.from(pk!));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to verify signed id pk, ', e);
|
console.error(e);
|
||||||
// fall back to non-secure connection in case pk mismatch
|
// fall back to non-secure connection in case pk mismatch
|
||||||
console.error("pk mismatch, fall back to non-secure");
|
console.error("pk mismatch, fall back to non-secure");
|
||||||
const public_key = message.PublicKey.fromPartial({});
|
const public_key = message.PublicKey.fromPartial({});
|
||||||
@@ -243,12 +242,26 @@ export default class Connection {
|
|||||||
this.login();
|
this.login();
|
||||||
} else if (msg?.test_delay) {
|
} else if (msg?.test_delay) {
|
||||||
const test_delay = msg?.test_delay;
|
const test_delay = msg?.test_delay;
|
||||||
console.log('test delay: ', test_delay);
|
console.log(test_delay);
|
||||||
if (!test_delay.from_client) {
|
if (!test_delay.from_client) {
|
||||||
this._ws?.sendMessage({ test_delay });
|
this._ws?.sendMessage({ test_delay });
|
||||||
}
|
}
|
||||||
} else if (msg?.login_response) {
|
} else if (msg?.login_response) {
|
||||||
this.handleLoginResponse(msg?.login_response);
|
const r = msg?.login_response;
|
||||||
|
if (r.error) {
|
||||||
|
if (r.error == "Wrong Password") {
|
||||||
|
this._password = undefined;
|
||||||
|
this.msgbox(
|
||||||
|
"re-input-password",
|
||||||
|
r.error,
|
||||||
|
"Do you want to enter again?"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.msgbox("error", "Login Error", r.error);
|
||||||
|
}
|
||||||
|
} else if (r.peer_info) {
|
||||||
|
this.handlePeerInfo(r.peer_info);
|
||||||
|
}
|
||||||
} else if (msg?.video_frame) {
|
} else if (msg?.video_frame) {
|
||||||
this.handleVideoFrame(msg?.video_frame!);
|
this.handleVideoFrame(msg?.video_frame!);
|
||||||
} else if (msg?.clipboard) {
|
} else if (msg?.clipboard) {
|
||||||
@@ -261,7 +274,7 @@ export default class Connection {
|
|||||||
try {
|
try {
|
||||||
globals.copyToClipboard(new TextDecoder().decode(cb.content));
|
globals.copyToClipboard(new TextDecoder().decode(cb.content));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to copy to clipboard, ', e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
// globals.pushEvent("clipboard", cb);
|
// globals.pushEvent("clipboard", cb);
|
||||||
} else if (msg?.cursor_data) {
|
} else if (msg?.cursor_data) {
|
||||||
@@ -305,110 +318,13 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoginResponse(response: message.LoginResponse) {
|
msgbox(type_: string, title: string, text: string) {
|
||||||
const loginErrorMap: Record<string, any> = {
|
this._msgbox?.(type_, title, text);
|
||||||
[consts.LOGIN_SCREEN_WAYLAND]: {
|
|
||||||
msgtype: "error",
|
|
||||||
title: "Login Error",
|
|
||||||
text: "Login screen using Wayland is not supported",
|
|
||||||
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY]: {
|
|
||||||
msgtype: "session-login",
|
|
||||||
title: "",
|
|
||||||
text: "",
|
|
||||||
link: "",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_XSESSION_FAILED]: {
|
|
||||||
msgtype: "session-re-login",
|
|
||||||
title: "",
|
|
||||||
text: "",
|
|
||||||
link: "",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER]: {
|
|
||||||
msgtype: "info-nocancel",
|
|
||||||
title: "another_user_login_title_tip",
|
|
||||||
text: "another_user_login_text_tip",
|
|
||||||
link: "",
|
|
||||||
try_again: false,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_XORG_NOT_FOUND]: {
|
|
||||||
msgtype: "info-nocancel",
|
|
||||||
title: "xorg_not_found_title_tip",
|
|
||||||
text: "xorg_not_found_text_tip",
|
|
||||||
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_NO_DESKTOP]: {
|
|
||||||
msgtype: "info-nocancel",
|
|
||||||
title: "no_desktop_title_tip",
|
|
||||||
text: "no_desktop_text_tip",
|
|
||||||
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY]: {
|
|
||||||
msgtype: "session-login-password",
|
|
||||||
title: "",
|
|
||||||
text: "",
|
|
||||||
link: "",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG]: {
|
|
||||||
msgtype: "session-login-re-password",
|
|
||||||
title: "",
|
|
||||||
text: "",
|
|
||||||
link: "",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
[consts.LOGIN_MSG_NO_PASSWORD_ACCESS]: {
|
|
||||||
msgtype: "wait-remote-accept-nook",
|
|
||||||
title: "Prompt",
|
|
||||||
text: "Please wait for the remote side to accept your session request...",
|
|
||||||
link: "",
|
|
||||||
try_again: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const err = response.error;
|
|
||||||
if (err) {
|
|
||||||
if (err == consts.LOGIN_MSG_PASSWORD_EMPTY) {
|
|
||||||
this._password = undefined;
|
|
||||||
this.msgbox("input-password", "Password Required", "", "");
|
|
||||||
}
|
|
||||||
if (err == consts.LOGIN_MSG_PASSWORD_WRONG) {
|
|
||||||
this._password = undefined;
|
|
||||||
this.msgbox(
|
|
||||||
"re-input-password",
|
|
||||||
err,
|
|
||||||
"Do you want to enter again?"
|
|
||||||
);
|
|
||||||
} else if (err == consts.LOGIN_MSG_2FA_WRONG || err == consts.REQUIRE_2FA) {
|
|
||||||
this.msgbox("input-2fa", err, "");
|
|
||||||
} else if (err in loginErrorMap) {
|
|
||||||
const m = loginErrorMap[err];
|
|
||||||
this.msgbox(m.msgtype, m.title, m.text, m.link);
|
|
||||||
} else {
|
|
||||||
if (err.includes(consts.SCRAP_X11_REQUIRED)) {
|
|
||||||
this.msgbox("error", "Login Error", err, consts.SCRAP_X11_REF_URL);
|
|
||||||
} else {
|
|
||||||
this.msgbox("error", "Login Error", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (response.peer_info) {
|
|
||||||
this.handlePeerInfo(response.peer_info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msgbox(type_: string, title: string, text: string, link: string = '') {
|
draw(frame: any) {
|
||||||
this._msgbox?.(type_, title, text, link);
|
this._draw?.(frame);
|
||||||
}
|
globals.draw(frame);
|
||||||
|
|
||||||
draw(display: number, frame: any) {
|
|
||||||
this._draw?.(display, frame);
|
|
||||||
globals.draw(display, frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -431,55 +347,38 @@ export default class Connection {
|
|||||||
this._draw = callback;
|
this._draw = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
login(info?: {
|
login(password: string | undefined = undefined) {
|
||||||
os_login?: message.OSLogin,
|
if (password) {
|
||||||
password?: Uint8Array
|
|
||||||
}) {
|
|
||||||
if (info?.password) {
|
|
||||||
const salt = this._hash?.salt;
|
const salt = this._hash?.salt;
|
||||||
let p = hash([info.password, salt!]);
|
let p = hash([password, salt!]);
|
||||||
this._password = p;
|
this._password = p;
|
||||||
const challenge = this._hash?.challenge;
|
const challenge = this._hash?.challenge;
|
||||||
p = hash([p, challenge!]);
|
p = hash([p, challenge!]);
|
||||||
this.msgbox("connecting", "Connecting...", "Logging in...");
|
this.msgbox("connecting", "Connecting...", "Logging in...");
|
||||||
this._sendLoginMessage({ os_login: info.os_login, password: p });
|
this._sendLoginMessage(p);
|
||||||
} else {
|
} else {
|
||||||
let p = this._password;
|
let p = this._password;
|
||||||
if (p) {
|
if (p) {
|
||||||
const challenge = this._hash?.challenge;
|
const challenge = this._hash?.challenge;
|
||||||
p = hash([p, challenge!]);
|
p = hash([p, challenge!]);
|
||||||
}
|
}
|
||||||
this._sendLoginMessage({ os_login: info?.os_login, password: p });
|
this._sendLoginMessage(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changePreferCodec() {
|
|
||||||
const supported_decoding = message.SupportedDecoding.fromPartial({
|
|
||||||
ability_vp9: 1,
|
|
||||||
ability_h264: 1,
|
|
||||||
});
|
|
||||||
const option = message.OptionMessage.fromPartial({ supported_decoding });
|
|
||||||
const misc = message.Misc.fromPartial({ option });
|
|
||||||
this._ws?.sendMessage({ misc });
|
|
||||||
}
|
|
||||||
|
|
||||||
async reconnect() {
|
async reconnect() {
|
||||||
this.close();
|
this.close();
|
||||||
await this.start(this._id);
|
await this.start(this._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendLoginMessage(login: {
|
_sendLoginMessage(password: Uint8Array | undefined = undefined) {
|
||||||
os_login?: message.OSLogin,
|
|
||||||
password?: Uint8Array,
|
|
||||||
}) {
|
|
||||||
const login_request = message.LoginRequest.fromPartial({
|
const login_request = message.LoginRequest.fromPartial({
|
||||||
username: this._id!,
|
username: this._id!,
|
||||||
my_id: "web", // to-do
|
my_id: "web", // to-do
|
||||||
my_name: "web", // to-do
|
my_name: "web", // to-do
|
||||||
password: login.password,
|
password,
|
||||||
option: this.getOptionMessage(),
|
option: this.getOptionMessage(),
|
||||||
video_ack_required: true,
|
video_ack_required: true,
|
||||||
os_login: login.os_login,
|
|
||||||
});
|
});
|
||||||
this._ws?.sendMessage({ login_request });
|
this._ws?.sendMessage({ login_request });
|
||||||
}
|
}
|
||||||
@@ -536,7 +435,7 @@ export default class Connection {
|
|||||||
i++;
|
i++;
|
||||||
if (i == n) this.sendVideoReceived();
|
if (i == n) this.sendVideoReceived();
|
||||||
if (ok && dec.frameBuffer && n == i) {
|
if (ok && dec.frameBuffer && n == i) {
|
||||||
this.draw(vf.display, dec.frameBuffer);
|
this.draw(dec.frameBuffer);
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
var elapsed = now - tm;
|
var elapsed = now - tm;
|
||||||
this._videoTestSpeed[1] += elapsed;
|
this._videoTestSpeed[1] += elapsed;
|
||||||
@@ -544,9 +443,9 @@ export default class Connection {
|
|||||||
if (this._videoTestSpeed[0] >= 30) {
|
if (this._videoTestSpeed[0] >= 30) {
|
||||||
console.log(
|
console.log(
|
||||||
"video decoder: " +
|
"video decoder: " +
|
||||||
parseInt(
|
parseInt(
|
||||||
"" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
|
"" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this._videoTestSpeed = [0, 0];
|
this._videoTestSpeed = [0, 0];
|
||||||
}
|
}
|
||||||
@@ -557,17 +456,8 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePeerInfo(pi: message.PeerInfo) {
|
handlePeerInfo(pi: message.PeerInfo) {
|
||||||
localStorage.setItem('last_remote_id', this._id);
|
|
||||||
this._peerInfo = pi;
|
this._peerInfo = pi;
|
||||||
if (pi.current_display > pi.displays.length) {
|
|
||||||
pi.current_display = 0;
|
|
||||||
}
|
|
||||||
if (globals.getVersionNumber(pi.version) < globals.getVersionNumber("1.1.10")) {
|
|
||||||
this.setPermission("restart", false);
|
|
||||||
}
|
|
||||||
if (pi.displays.length == 0) {
|
if (pi.displays.length == 0) {
|
||||||
this.setOption("info", pi);
|
|
||||||
globals.pushEvent("update_privacy_mode", {});
|
|
||||||
this.msgbox("error", "Remote Error", "No Display");
|
this.msgbox("error", "Remote Error", "No Display");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -577,7 +467,6 @@ export default class Connection {
|
|||||||
if (p) this.inputOsPassword(p);
|
if (p) this.inputOsPassword(p);
|
||||||
const username = this.getOption("info")?.username;
|
const username = this.getOption("info")?.username;
|
||||||
if (username && !pi.username) pi.username = username;
|
if (username && !pi.username) pi.username = username;
|
||||||
globals.pushEvent("update_privacy_mode", {});
|
|
||||||
this.setOption("info", pi);
|
this.setOption("info", pi);
|
||||||
if (this.getRemember()) {
|
if (this.getRemember()) {
|
||||||
if (this._password?.length) {
|
if (this._password?.length) {
|
||||||
@@ -592,10 +481,6 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPermission(name: string, value: Boolean) {
|
|
||||||
globals.pushEvent("permission", { [name]: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldAutoLogin(): string {
|
shouldAutoLogin(): string {
|
||||||
const l = this.getOption("lock-after-session-end");
|
const l = this.getOption("lock-after-session-end");
|
||||||
const a = !!this.getOption("auto-login");
|
const a = !!this.getOption("auto-login");
|
||||||
@@ -631,7 +516,7 @@ export default class Connection {
|
|||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setPermission(name, p.enabled);
|
globals.pushEvent("permission", { [name]: p.enabled });
|
||||||
} else if (misc.switch_display) {
|
} else if (misc.switch_display) {
|
||||||
this.loadVideoDecoder();
|
this.loadVideoDecoder();
|
||||||
globals.pushEvent("switch_display", misc.switch_display);
|
globals.pushEvent("switch_display", misc.switch_display);
|
||||||
@@ -652,27 +537,7 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOption(name: string): any {
|
getOption(name: string): any {
|
||||||
return this._options[name] ?? globals.getUserDefaultOption(name);
|
return this._options[name];
|
||||||
}
|
|
||||||
|
|
||||||
getToggleOption(name: string): Boolean {
|
|
||||||
// TODO: more default settings
|
|
||||||
const defaultToggleTrue = [
|
|
||||||
'show-remote-cursor',
|
|
||||||
'privacy-mode',
|
|
||||||
'enable-file-copy-paste',
|
|
||||||
'allow_swap_key',
|
|
||||||
];
|
|
||||||
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
getStatus(): String {
|
|
||||||
return JSON.stringify({ status_num: 10 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
checkConnStatus() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setOption(name: string, value: any) {
|
setOption(name: string, value: any) {
|
||||||
@@ -727,70 +592,17 @@ export default class Connection {
|
|||||||
this._ws?.sendMessage({ key_event });
|
this._ws?.sendMessage({ key_event });
|
||||||
}
|
}
|
||||||
|
|
||||||
restart() {
|
|
||||||
const misc = message.Misc.fromPartial({});
|
|
||||||
misc.restart_remote_device = true;
|
|
||||||
this._ws?.sendMessage({ misc });
|
|
||||||
}
|
|
||||||
|
|
||||||
inputString(seq: string) {
|
inputString(seq: string) {
|
||||||
const key_event = message.KeyEvent.fromPartial({ seq });
|
const key_event = message.KeyEvent.fromPartial({ seq });
|
||||||
this._ws?.sendMessage({ key_event });
|
this._ws?.sendMessage({ key_event });
|
||||||
}
|
}
|
||||||
|
|
||||||
send2fa(code: string) {
|
switchDisplay(display: number) {
|
||||||
const auth_2fa = message.Auth2FA.fromPartial({ code });
|
const switch_display = message.SwitchDisplay.fromPartial({ display });
|
||||||
this._ws?.sendMessage({ auth_2fa });
|
const misc = message.Misc.fromPartial({ switch_display });
|
||||||
}
|
|
||||||
|
|
||||||
_captureDisplays({ add, sub, set }: {
|
|
||||||
add?: number[], sub?: number[], set?: number[]
|
|
||||||
}) {
|
|
||||||
const capture_displays = message.CaptureDisplays.fromPartial({ add, sub, set });
|
|
||||||
const misc = message.Misc.fromPartial({ capture_displays });
|
|
||||||
this._ws?.sendMessage({ misc });
|
this._ws?.sendMessage({ misc });
|
||||||
}
|
}
|
||||||
|
|
||||||
switchDisplay(v: string) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(v);
|
|
||||||
const value = obj.value;
|
|
||||||
const isDesktop = obj.isDesktop;
|
|
||||||
if (value.length == 1) {
|
|
||||||
const switch_display = message.SwitchDisplay.fromPartial({ display: value[0] });
|
|
||||||
const misc = message.Misc.fromPartial({ switch_display });
|
|
||||||
this._ws?.sendMessage({ misc });
|
|
||||||
|
|
||||||
if (!isDesktop) {
|
|
||||||
this._captureDisplays({ set: value });
|
|
||||||
} else {
|
|
||||||
// If support merging images, check_remove_unused_displays() in ui_session_interface.rs
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._captureDisplays({ set: value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log('Failed to switch display, invalid param "' + v + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elevateWithLogon(value: string) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
const logon = message.ElevationRequestWithLogon.fromPartial({
|
|
||||||
username: obj.username,
|
|
||||||
password: obj.password
|
|
||||||
});
|
|
||||||
const elevation_request = message.ElevationRequest.fromPartial({ logon });
|
|
||||||
const misc = message.Misc.fromPartial({ elevation_request });
|
|
||||||
this._ws?.sendMessage({ misc });
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log('Failed to elevate with logon, invalid param "' + value + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async inputOsPassword(seq: string) {
|
async inputOsPassword(seq: string) {
|
||||||
this.inputMouse();
|
this.inputMouse();
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
@@ -839,52 +651,6 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleOption(name: string) {
|
toggleOption(name: string) {
|
||||||
|
|
||||||
// } else if name == "block-input" {
|
|
||||||
// option.block_input = BoolOption::Yes.into();
|
|
||||||
// } else if name == "unblock-input" {
|
|
||||||
// option.block_input = BoolOption::No.into();
|
|
||||||
// } else if name == "show-quality-monitor" {
|
|
||||||
// config.show_quality_monitor.v = !config.show_quality_monitor.v;
|
|
||||||
// } else if name == "allow_swap_key" {
|
|
||||||
// config.allow_swap_key.v = !config.allow_swap_key.v;
|
|
||||||
// } else if name == "view-only" {
|
|
||||||
// config.view_only.v = !config.view_only.v;
|
|
||||||
// let f = |b: bool| {
|
|
||||||
// if b {
|
|
||||||
// BoolOption::Yes.into()
|
|
||||||
// } else {
|
|
||||||
// BoolOption::No.into()
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// if config.view_only.v {
|
|
||||||
// option.disable_keyboard = f(true);
|
|
||||||
// option.disable_clipboard = f(true);
|
|
||||||
// option.show_remote_cursor = f(true);
|
|
||||||
// option.enable_file_transfer = f(false);
|
|
||||||
// option.lock_after_session_end = f(false);
|
|
||||||
// } else {
|
|
||||||
// option.disable_keyboard = f(false);
|
|
||||||
// option.disable_clipboard = f(self.get_toggle_option("disable-clipboard"));
|
|
||||||
// option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor"));
|
|
||||||
// option.enable_file_transfer = f(self.config.enable_file_transfer.v);
|
|
||||||
// option.lock_after_session_end = f(self.config.lock_after_session_end.v);
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// let is_set = self
|
|
||||||
// .options
|
|
||||||
// .get(&name)
|
|
||||||
// .map(|o| !o.is_empty())
|
|
||||||
// .unwrap_or(false);
|
|
||||||
// if is_set {
|
|
||||||
// self.config.options.remove(&name);
|
|
||||||
// } else {
|
|
||||||
// self.config.options.insert(name, "Y".to_owned());
|
|
||||||
// }
|
|
||||||
// self.config.store(&self.id);
|
|
||||||
// return None;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const v = !this._options[name];
|
const v = !this._options[name];
|
||||||
const option = message.OptionMessage.fromPartial({});
|
const option = message.OptionMessage.fromPartial({});
|
||||||
const v2 = v
|
const v2 = v
|
||||||
@@ -906,43 +672,13 @@ export default class Connection {
|
|||||||
case "privacy-mode":
|
case "privacy-mode":
|
||||||
option.privacy_mode = v2;
|
option.privacy_mode = v2;
|
||||||
break;
|
break;
|
||||||
case "enable-file-copy-paste":
|
|
||||||
option.enable_file_transfer = v2;
|
|
||||||
break;
|
|
||||||
case "block-input":
|
case "block-input":
|
||||||
option.block_input = message.OptionMessage_BoolOption.Yes;
|
option.block_input = message.OptionMessage_BoolOption.Yes;
|
||||||
break;
|
break;
|
||||||
case "unblock-input":
|
case "unblock-input":
|
||||||
option.block_input = message.OptionMessage_BoolOption.No;
|
option.block_input = message.OptionMessage_BoolOption.No;
|
||||||
break;
|
break;
|
||||||
case "show-quality-monitor":
|
|
||||||
case "allow-swap-key":
|
|
||||||
break;
|
|
||||||
case "view-only":
|
|
||||||
if (v) {
|
|
||||||
option.disable_keyboard = message.OptionMessage_BoolOption.Yes;
|
|
||||||
option.disable_clipboard = message.OptionMessage_BoolOption.Yes;
|
|
||||||
option.show_remote_cursor = message.OptionMessage_BoolOption.Yes;
|
|
||||||
option.enable_file_transfer = message.OptionMessage_BoolOption.No;
|
|
||||||
option.lock_after_session_end = message.OptionMessage_BoolOption.No;
|
|
||||||
} else {
|
|
||||||
option.disable_keyboard = message.OptionMessage_BoolOption.No;
|
|
||||||
option.disable_clipboard = this.getToggleOption("disable-clipboard")
|
|
||||||
? message.OptionMessage_BoolOption.Yes
|
|
||||||
: message.OptionMessage_BoolOption.No;
|
|
||||||
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
|
|
||||||
? message.OptionMessage_BoolOption.Yes
|
|
||||||
: message.OptionMessage_BoolOption.No;
|
|
||||||
option.enable_file_transfer = this.getToggleOption("enable-file-copy-paste")
|
|
||||||
? message.OptionMessage_BoolOption.Yes
|
|
||||||
: message.OptionMessage_BoolOption.No;
|
|
||||||
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
|
|
||||||
? message.OptionMessage_BoolOption.Yes
|
|
||||||
: message.OptionMessage_BoolOption.No;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
this.setOption(name, this._options[name] ? undefined : "Y");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name.indexOf("block-input") < 0) this.setOption(name, v);
|
if (name.indexOf("block-input") < 0) this.setOption(name, v);
|
||||||
@@ -950,20 +686,6 @@ export default class Connection {
|
|||||||
this._ws?.sendMessage({ misc });
|
this._ws?.sendMessage({ misc });
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePrivacyMode(value: string) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
const toggle_privacy_mode = message.TogglePrivacyMode.fromPartial({
|
|
||||||
impl_key: obj.impl_key,
|
|
||||||
on: obj.on,
|
|
||||||
});
|
|
||||||
const misc = message.Misc.fromPartial({ toggle_privacy_mode });
|
|
||||||
this._ws?.sendMessage({ misc });
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to toggle privacy mode, invalid param "' + value + '"')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageQuality() {
|
getImageQuality() {
|
||||||
return this.getOption("image-quality");
|
return this.getOption("image-quality");
|
||||||
}
|
}
|
||||||
@@ -998,7 +720,7 @@ export default class Connection {
|
|||||||
loadVp9((decoder: any) => {
|
loadVp9((decoder: any) => {
|
||||||
this._videoDecoder = decoder;
|
this._videoDecoder = decoder;
|
||||||
console.log("vp9 loaded");
|
console.log("vp9 loaded");
|
||||||
console.log('The decoder: ', decoder);
|
console.log(decoder);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
383
flutter/web/v1/js/src/globals.js
Normal file
383
flutter/web/v1/js/src/globals.js
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
import Connection from "./connection";
|
||||||
|
import _sodium from "libsodium-wrappers";
|
||||||
|
import { CursorData } from "./message";
|
||||||
|
import { loadVp9 } from "./codec";
|
||||||
|
import { checkIfRetry, version } from "./gen_js_from_hbb";
|
||||||
|
import { initZstd, translate } from "./common";
|
||||||
|
import PCMPlayer from "pcm-player";
|
||||||
|
|
||||||
|
window.curConn = undefined;
|
||||||
|
window.isMobile = () => {
|
||||||
|
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
||||||
|
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDesktop() {
|
||||||
|
return !isMobile();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function msgbox(type, title, text) {
|
||||||
|
if (!type || (type == 'error' && !text)) return;
|
||||||
|
const text2 = text.toLowerCase();
|
||||||
|
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
|
||||||
|
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, hasRetry }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonfyForDart(payload) {
|
||||||
|
var tmp = {};
|
||||||
|
for (const [key, value] of Object.entries(payload)) {
|
||||||
|
if (!key) continue;
|
||||||
|
tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushEvent(name, payload) {
|
||||||
|
payload = jsonfyForDart(payload);
|
||||||
|
payload.name = name;
|
||||||
|
onGlobalEvent(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
let yuvWorker;
|
||||||
|
let yuvCanvas;
|
||||||
|
let gl;
|
||||||
|
let pixels;
|
||||||
|
let flipPixels;
|
||||||
|
let oldSize;
|
||||||
|
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
|
||||||
|
gl = canvas.getContext("webgl");
|
||||||
|
} else {
|
||||||
|
yuvWorker = new Worker("./yuv.js");
|
||||||
|
}
|
||||||
|
let testSpeed = [0, 0];
|
||||||
|
|
||||||
|
export function draw(frame) {
|
||||||
|
if (yuvWorker) {
|
||||||
|
// frame's (y/u/v).bytes already detached, can not transferrable any more.
|
||||||
|
yuvWorker.postMessage(frame);
|
||||||
|
} else {
|
||||||
|
var tm0 = new Date().getTime();
|
||||||
|
yuvCanvas.drawFrame(frame);
|
||||||
|
var width = canvas.width;
|
||||||
|
var height = canvas.height;
|
||||||
|
var size = width * height * 4;
|
||||||
|
if (size != oldSize) {
|
||||||
|
pixels = new Uint8Array(size);
|
||||||
|
flipPixels = new Uint8Array(size);
|
||||||
|
oldSize = size;
|
||||||
|
}
|
||||||
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||||
|
const row = width * 4;
|
||||||
|
const end = (height - 1) * row;
|
||||||
|
for (let i = 0; i < size; i += row) {
|
||||||
|
flipPixels.set(pixels.subarray(i, i + row), end - i);
|
||||||
|
}
|
||||||
|
onRgba(flipPixels);
|
||||||
|
testSpeed[1] += new Date().getTime() - tm0;
|
||||||
|
testSpeed[0] += 1;
|
||||||
|
if (testSpeed[0] > 30) {
|
||||||
|
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
||||||
|
testSpeed = [0, 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
|
||||||
|
if (testCanvas && currentFrame) {
|
||||||
|
var ctx = testCanvas.getContext("2d");
|
||||||
|
testCanvas.width = frame.format.displayWidth;
|
||||||
|
testCanvas.height = frame.format.displayHeight;
|
||||||
|
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
|
||||||
|
img.data.set(currentFrame);
|
||||||
|
ctx.putImageData(img, 0, 0);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendOffCanvas(c) {
|
||||||
|
let canvas = c.transferControlToOffscreen();
|
||||||
|
yuvWorker.postMessage({ canvas }, [canvas]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setConn(conn) {
|
||||||
|
window.curConn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConn() {
|
||||||
|
return window.curConn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startConn(id) {
|
||||||
|
setByName('remote_id', id);
|
||||||
|
await curConn.start(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function close() {
|
||||||
|
getConn()?.close();
|
||||||
|
setConn(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newConn() {
|
||||||
|
window.curConn?.close();
|
||||||
|
const conn = new Connection();
|
||||||
|
setConn(conn);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sodium;
|
||||||
|
export async function verify(signed, pk) {
|
||||||
|
if (!sodium) {
|
||||||
|
await _sodium.ready;
|
||||||
|
sodium = _sodium;
|
||||||
|
}
|
||||||
|
if (typeof pk == 'string') {
|
||||||
|
pk = decodeBase64(pk);
|
||||||
|
}
|
||||||
|
return sodium.crypto_sign_open(signed, pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeBase64(pk) {
|
||||||
|
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genBoxKeyPair() {
|
||||||
|
const pair = sodium.crypto_box_keypair();
|
||||||
|
const sk = pair.privateKey;
|
||||||
|
const pk = pair.publicKey;
|
||||||
|
return [sk, pk];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genSecretKey() {
|
||||||
|
return sodium.crypto_secretbox_keygen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function seal(unsigned, theirPk, ourSk) {
|
||||||
|
const nonce = Uint8Array.from(Array(24).fill(0));
|
||||||
|
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeOnce(value) {
|
||||||
|
var byteArray = Array(24).fill(0);
|
||||||
|
|
||||||
|
for (var index = 0; index < byteArray.length && value > 0; index++) {
|
||||||
|
var byte = value & 0xff;
|
||||||
|
byteArray[index] = byte;
|
||||||
|
value = (value - byte) / 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uint8Array.from(byteArray);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function encrypt(unsigned, nonce, key) {
|
||||||
|
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(signed, nonce, key) {
|
||||||
|
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setByName = (name, value) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'remote_id':
|
||||||
|
localStorage.setItem('remote-id', value);
|
||||||
|
break;
|
||||||
|
case 'connect':
|
||||||
|
newConn();
|
||||||
|
startConn(value);
|
||||||
|
break;
|
||||||
|
case 'login':
|
||||||
|
value = JSON.parse(value);
|
||||||
|
curConn.setRemember(value.remember == 'true');
|
||||||
|
curConn.login(value.password);
|
||||||
|
break;
|
||||||
|
case 'close':
|
||||||
|
close();
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
curConn.refresh();
|
||||||
|
break;
|
||||||
|
case 'reconnect':
|
||||||
|
curConn.reconnect();
|
||||||
|
break;
|
||||||
|
case 'toggle_option':
|
||||||
|
curConn.toggleOption(value);
|
||||||
|
break;
|
||||||
|
case 'image_quality':
|
||||||
|
curConn.setImageQuality(value);
|
||||||
|
break;
|
||||||
|
case 'lock_screen':
|
||||||
|
curConn.lockScreen();
|
||||||
|
break;
|
||||||
|
case 'ctrl_alt_del':
|
||||||
|
curConn.ctrlAltDel();
|
||||||
|
break;
|
||||||
|
case 'switch_display':
|
||||||
|
curConn.switchDisplay(value);
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
const peers = getPeers();
|
||||||
|
delete peers[value];
|
||||||
|
localStorage.setItem('peers', JSON.stringify(peers));
|
||||||
|
break;
|
||||||
|
case 'input_key':
|
||||||
|
value = JSON.parse(value);
|
||||||
|
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
||||||
|
break;
|
||||||
|
case 'input_string':
|
||||||
|
curConn.inputString(value);
|
||||||
|
break;
|
||||||
|
case 'send_mouse':
|
||||||
|
let mask = 0;
|
||||||
|
value = JSON.parse(value);
|
||||||
|
switch (value.type) {
|
||||||
|
case 'down':
|
||||||
|
mask = 1;
|
||||||
|
break;
|
||||||
|
case 'up':
|
||||||
|
mask = 2;
|
||||||
|
break;
|
||||||
|
case 'wheel':
|
||||||
|
mask = 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (value.buttons) {
|
||||||
|
case 'left':
|
||||||
|
mask |= 1 << 3;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
mask |= 2 << 3;
|
||||||
|
break;
|
||||||
|
case 'wheel':
|
||||||
|
mask |= 4 << 3;
|
||||||
|
}
|
||||||
|
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
||||||
|
break;
|
||||||
|
case 'option':
|
||||||
|
value = JSON.parse(value);
|
||||||
|
localStorage.setItem(value.name, value.value);
|
||||||
|
break;
|
||||||
|
case 'peer_option':
|
||||||
|
value = JSON.parse(value);
|
||||||
|
curConn.setOption(value.name, value.value);
|
||||||
|
break;
|
||||||
|
case 'input_os_password':
|
||||||
|
curConn.inputOsPassword(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.getByName = (name, arg) => {
|
||||||
|
let v = _getByName(name, arg);
|
||||||
|
if (typeof v == 'string' || v instanceof String) return v;
|
||||||
|
if (v == undefined || v == null) return '';
|
||||||
|
return JSON.stringify(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPeersForDart() {
|
||||||
|
const peers = [];
|
||||||
|
for (const [id, value] of Object.entries(getPeers())) {
|
||||||
|
if (!id) continue;
|
||||||
|
const tm = value['tm'];
|
||||||
|
const info = value['info'];
|
||||||
|
if (!tm || !info) continue;
|
||||||
|
peers.push([tm, id, info]);
|
||||||
|
}
|
||||||
|
return peers.sort().reverse().map(x => x.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getByName(name, arg) {
|
||||||
|
switch (name) {
|
||||||
|
case 'peers':
|
||||||
|
return getPeersForDart();
|
||||||
|
case 'remote_id':
|
||||||
|
return localStorage.getItem('remote-id');
|
||||||
|
case 'remember':
|
||||||
|
return curConn.getRemember();
|
||||||
|
case 'toggle_option':
|
||||||
|
return curConn.getOption(arg) || false;
|
||||||
|
case 'option':
|
||||||
|
return localStorage.getItem(arg);
|
||||||
|
case 'image_quality':
|
||||||
|
return curConn.getImageQuality();
|
||||||
|
case 'translate':
|
||||||
|
arg = JSON.parse(arg);
|
||||||
|
return translate(arg.locale, arg.text);
|
||||||
|
case 'peer_option':
|
||||||
|
return curConn.getOption(arg);
|
||||||
|
case 'test_if_valid_server':
|
||||||
|
break;
|
||||||
|
case 'version':
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let opusWorker = new Worker("./libopus.js");
|
||||||
|
let pcmPlayer;
|
||||||
|
|
||||||
|
export function initAudio(channels, sampleRate) {
|
||||||
|
pcmPlayer = newAudioPlayer(channels, sampleRate);
|
||||||
|
opusWorker.postMessage({ channels, sampleRate });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playAudio(packet) {
|
||||||
|
opusWorker.postMessage(packet, [packet.buffer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.init = async () => {
|
||||||
|
if (yuvWorker) {
|
||||||
|
yuvWorker.onmessage = (e) => {
|
||||||
|
onRgba(e.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opusWorker.onmessage = (e) => {
|
||||||
|
pcmPlayer.feed(e.data);
|
||||||
|
}
|
||||||
|
loadVp9(() => { });
|
||||||
|
await initZstd();
|
||||||
|
console.log('init done');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPeers() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem('peers')) || {};
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function newAudioPlayer(channels, sampleRate) {
|
||||||
|
return new PCMPlayer({
|
||||||
|
channels,
|
||||||
|
sampleRate,
|
||||||
|
flushingTime: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyToClipboard(text) {
|
||||||
|
if (window.clipboardData && window.clipboardData.setData) {
|
||||||
|
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
|
||||||
|
return window.clipboardData.setData("Text", text);
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
|
||||||
|
var textarea = document.createElement("textarea");
|
||||||
|
textarea.textContent = text;
|
||||||
|
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
try {
|
||||||
|
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.warn("Copy to clipboard failed.", ex);
|
||||||
|
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
flutter/web/v2/README.md
Normal file
1
flutter/web/v2/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Under dev.
|
||||||
@@ -9,7 +9,7 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
flexi_logger = { version = "0.27", features = ["async"] }
|
flexi_logger = { version = "0.27", features = ["async"] }
|
||||||
protobuf = { version = "3.4", features = ["with-bytes"] }
|
protobuf = { version = "3.4", features = ["with-bytes"] }
|
||||||
tokio = { version = "1.37", features = ["full"] }
|
tokio = { version = "1.38", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["full"] }
|
tokio-util = { version = "0.7", features = ["full"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
bytes = { version = "1.6", features = ["serde"] }
|
bytes = { version = "1.6", features = ["serde"] }
|
||||||
|
|||||||
@@ -912,7 +912,7 @@ impl Config {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn purify_options(v: &mut HashMap<String, String>) {
|
fn purify_options(v: &mut HashMap<String, String>) {
|
||||||
v.retain(|k, _| is_option_can_save(&OVERWRITE_SETTINGS, k));
|
v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, k, &DEFAULT_SETTINGS, v));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_options(mut v: HashMap<String, String>) {
|
pub fn set_options(mut v: HashMap<String, String>) {
|
||||||
@@ -936,7 +936,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &k) {
|
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut config = CONFIG2.write().unwrap();
|
let mut config = CONFIG2.write().unwrap();
|
||||||
@@ -1284,6 +1284,7 @@ impl PeerConfig {
|
|||||||
keys::OPTION_TOUCH_MODE,
|
keys::OPTION_TOUCH_MODE,
|
||||||
keys::OPTION_I444,
|
keys::OPTION_I444,
|
||||||
keys::OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
keys::OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||||
|
keys::OPTION_COLLAPSE_TOOLBAR,
|
||||||
]
|
]
|
||||||
.map(|key| {
|
.map(|key| {
|
||||||
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
||||||
@@ -1449,7 +1450,7 @@ impl LocalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k) {
|
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||||
@@ -1553,40 +1554,6 @@ impl LanPeers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct HwCodecConfig {
|
|
||||||
#[serde(default, deserialize_with = "deserialize_string")]
|
|
||||||
pub ram: String,
|
|
||||||
#[serde(default, deserialize_with = "deserialize_string")]
|
|
||||||
pub vram: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HwCodecConfig {
|
|
||||||
pub fn load() -> HwCodecConfig {
|
|
||||||
Config::load_::<HwCodecConfig>("_hwcodec")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn store(&self) {
|
|
||||||
Config::store_(self, "_hwcodec");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear() {
|
|
||||||
HwCodecConfig::default().store();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_ram() {
|
|
||||||
let mut c = Self::load();
|
|
||||||
c.ram = Default::default();
|
|
||||||
c.store();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_vram() {
|
|
||||||
let mut c = Self::load();
|
|
||||||
c.vram = Default::default();
|
|
||||||
c.store();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct UserDefaultConfig {
|
pub struct UserDefaultConfig {
|
||||||
#[serde(default, deserialize_with = "deserialize_hashmap_string_string")]
|
#[serde(default, deserialize_with = "deserialize_hashmap_string_string")]
|
||||||
@@ -1636,7 +1603,12 @@ impl UserDefaultConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, key: String, value: String) {
|
pub fn set(&mut self, key: String, value: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_DISPLAY_SETTINGS, &key) {
|
if !is_option_can_save(
|
||||||
|
&OVERWRITE_DISPLAY_SETTINGS,
|
||||||
|
&key,
|
||||||
|
&DEFAULT_DISPLAY_SETTINGS,
|
||||||
|
&value,
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
@@ -1959,8 +1931,15 @@ fn get_or(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn is_option_can_save(overwrite: &RwLock<HashMap<String, String>>, k: &str) -> bool {
|
fn is_option_can_save(
|
||||||
if overwrite.read().unwrap().contains_key(k) {
|
overwrite: &RwLock<HashMap<String, String>>,
|
||||||
|
k: &str,
|
||||||
|
defaults: &RwLock<HashMap<String, String>>,
|
||||||
|
v: &str,
|
||||||
|
) -> bool {
|
||||||
|
if overwrite.read().unwrap().contains_key(k)
|
||||||
|
|| defaults.read().unwrap().get(k).map_or(false, |x| x == v)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -2018,6 +1997,23 @@ pub fn is_disable_installation() -> bool {
|
|||||||
is_some_hard_opton("disable-installation")
|
is_some_hard_opton("disable-installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function must be kept the same as the one in flutter and sciter code.
|
||||||
|
// flutter: flutter/lib/common.dart -> option2bool()
|
||||||
|
// sciter: Does not have the function, but it should be kept the same.
|
||||||
|
pub fn option2bool(option: &str, value: &str) -> bool {
|
||||||
|
if option.starts_with("enable-") {
|
||||||
|
value != "N"
|
||||||
|
} else if option.starts_with("allow-")
|
||||||
|
|| option == "stop-service"
|
||||||
|
|| option == keys::OPTION_DIRECT_SERVER
|
||||||
|
|| option == "force-always-relay"
|
||||||
|
{
|
||||||
|
value == "Y"
|
||||||
|
} else {
|
||||||
|
value != "N"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod keys {
|
pub mod keys {
|
||||||
pub const OPTION_VIEW_ONLY: &str = "view_only";
|
pub const OPTION_VIEW_ONLY: &str = "view_only";
|
||||||
pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar";
|
pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar";
|
||||||
@@ -2083,6 +2079,14 @@ pub mod keys {
|
|||||||
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
||||||
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
|
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
|
||||||
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
|
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
|
||||||
|
pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server";
|
||||||
|
pub const OPTION_API_SERVER: &str = "api-server";
|
||||||
|
pub const OPTION_KEY: &str = "key";
|
||||||
|
pub const OPTION_PRESET_ADDRESS_BOOK_NAME: &str = "preset-address-book-name";
|
||||||
|
pub const OPTION_PRESET_ADDRESS_BOOK_TAG: &str = "preset-address-book-tag";
|
||||||
|
pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture";
|
||||||
|
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
|
||||||
|
"enable-android-software-encoding-half-scale";
|
||||||
|
|
||||||
// flutter local options
|
// flutter local options
|
||||||
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||||
@@ -2093,6 +2097,19 @@ pub mod keys {
|
|||||||
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
|
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
|
||||||
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
|
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
|
||||||
|
|
||||||
|
// android floating window options
|
||||||
|
pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window";
|
||||||
|
pub const OPTION_FLOATING_WINDOW_SIZE: &str = "floating-window-size";
|
||||||
|
pub const OPTION_FLOATING_WINDOW_UNTOUCHABLE: &str = "floating-window-untouchable";
|
||||||
|
pub const OPTION_FLOATING_WINDOW_TRANSPARENCY: &str = "floating-window-transparency";
|
||||||
|
pub const OPTION_FLOATING_WINDOW_SVG: &str = "floating-window-svg";
|
||||||
|
|
||||||
|
// android keep screen on
|
||||||
|
pub const OPTION_KEEP_SCREEN_ON: &str = "keep-screen-on";
|
||||||
|
|
||||||
|
pub const OPTION_DISABLE_GROUP_PANEL: &str = "disable-group-panel";
|
||||||
|
pub const OPTION_PRE_ELEVATE_SERVICE: &str = "pre-elevate-service";
|
||||||
|
|
||||||
// proxy settings
|
// proxy settings
|
||||||
// The following options are not real keys, they are just used for custom client advanced settings.
|
// The following options are not real keys, they are just used for custom client advanced settings.
|
||||||
// The real keys are in Config2::socks.
|
// The real keys are in Config2::socks.
|
||||||
@@ -2148,6 +2165,14 @@ pub mod keys {
|
|||||||
OPTION_FLUTTER_PEER_TAB_VISIBLE,
|
OPTION_FLUTTER_PEER_TAB_VISIBLE,
|
||||||
OPTION_FLUTTER_PEER_CARD_UI_TYLE,
|
OPTION_FLUTTER_PEER_CARD_UI_TYLE,
|
||||||
OPTION_FLUTTER_CURRENT_AB_NAME,
|
OPTION_FLUTTER_CURRENT_AB_NAME,
|
||||||
|
OPTION_DISABLE_FLOATING_WINDOW,
|
||||||
|
OPTION_FLOATING_WINDOW_SIZE,
|
||||||
|
OPTION_FLOATING_WINDOW_UNTOUCHABLE,
|
||||||
|
OPTION_FLOATING_WINDOW_TRANSPARENCY,
|
||||||
|
OPTION_FLOATING_WINDOW_SVG,
|
||||||
|
OPTION_KEEP_SCREEN_ON,
|
||||||
|
OPTION_DISABLE_GROUP_PANEL,
|
||||||
|
OPTION_PRE_ELEVATE_SERVICE,
|
||||||
];
|
];
|
||||||
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||||
pub const KEYS_SETTINGS: &[&str] = &[
|
pub const KEYS_SETTINGS: &[&str] = &[
|
||||||
@@ -2179,9 +2204,28 @@ pub mod keys {
|
|||||||
OPTION_PROXY_URL,
|
OPTION_PROXY_URL,
|
||||||
OPTION_PROXY_USERNAME,
|
OPTION_PROXY_USERNAME,
|
||||||
OPTION_PROXY_PASSWORD,
|
OPTION_PROXY_PASSWORD,
|
||||||
|
OPTION_CUSTOM_RENDEZVOUS_SERVER,
|
||||||
|
OPTION_API_SERVER,
|
||||||
|
OPTION_KEY,
|
||||||
|
OPTION_PRESET_ADDRESS_BOOK_NAME,
|
||||||
|
OPTION_PRESET_ADDRESS_BOOK_TAG,
|
||||||
|
OPTION_ENABLE_DIRECTX_CAPTURE,
|
||||||
|
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn common_load<
|
||||||
|
T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug,
|
||||||
|
>(
|
||||||
|
suffix: &str,
|
||||||
|
) -> T {
|
||||||
|
Config::load_::<T>(suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn common_store<T: serde::Serialize>(config: &T, suffix: &str) {
|
||||||
|
Config::store_(config, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -2254,7 +2298,18 @@ mod tests {
|
|||||||
res.insert("c".to_owned(), "d".to_string());
|
res.insert("c".to_owned(), "d".to_string());
|
||||||
res.insert("d".to_owned(), "cc".to_string());
|
res.insert("d".to_owned(), "cc".to_string());
|
||||||
Config::purify_options(&mut res);
|
Config::purify_options(&mut res);
|
||||||
|
DEFAULT_SETTINGS
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert("f".to_string(), "c".to_string());
|
||||||
|
Config::purify_options(&mut res);
|
||||||
assert!(res.len() == 2);
|
assert!(res.len() == 2);
|
||||||
|
DEFAULT_SETTINGS
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert("f".to_string(), "a".to_string());
|
||||||
|
Config::purify_options(&mut res);
|
||||||
|
assert!(res.len() == 1);
|
||||||
let res = Config::get_options();
|
let res = Config::get_options();
|
||||||
assert!(res["a"] == "b");
|
assert!(res["a"] == "b");
|
||||||
assert!(res["c"] == "f");
|
assert!(res["c"] == "f");
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ pub fn approve_mode() -> ApproveMode {
|
|||||||
pub fn hide_cm() -> bool {
|
pub fn hide_cm() -> bool {
|
||||||
approve_mode() == ApproveMode::Password
|
approve_mode() == ApproveMode::Password
|
||||||
&& verification_method() == VerificationMethod::OnlyUsePermanentPassword
|
&& verification_method() == VerificationMethod::OnlyUsePermanentPassword
|
||||||
&& !Config::get_option("allow-hide-cm").is_empty()
|
&& crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION_LEN: usize = 2;
|
const VERSION_LEN: usize = 2;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.5"
|
version = "1.2.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -287,13 +287,17 @@ mod hw {
|
|||||||
let mut mid_data = Vec::new();
|
let mut mid_data = Vec::new();
|
||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
let mut time_sum = Duration::ZERO;
|
let mut time_sum = Duration::ZERO;
|
||||||
|
let start = std::time::Instant::now();
|
||||||
loop {
|
loop {
|
||||||
match c.frame(std::time::Duration::from_millis(30)) {
|
match c.frame(std::time::Duration::from_millis(30)) {
|
||||||
Ok(frame) => {
|
Ok(frame) => {
|
||||||
let tmp_timer = Instant::now();
|
let tmp_timer = Instant::now();
|
||||||
let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data).unwrap();
|
let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data).unwrap();
|
||||||
let yuv = frame.yuv().unwrap();
|
let yuv = frame.yuv().unwrap();
|
||||||
for ref frame in encoder.encode(&yuv).unwrap() {
|
for ref frame in encoder
|
||||||
|
.encode(&yuv, start.elapsed().as_millis() as _)
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
size += frame.data.len();
|
size += frame.data.len();
|
||||||
|
|
||||||
h26xs.push(frame.data.to_vec());
|
h26xs.push(frame.data.to_vec());
|
||||||
|
|||||||
@@ -209,19 +209,38 @@ pub fn clear_codec_info() {
|
|||||||
*MEDIA_CODEC_INFOS.write().unwrap() = None;
|
*MEDIA_CODEC_INFOS.write().unwrap() = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// another way to fix "reference table overflow" error caused by new_string and call_main_service_pointer_input frequently calld
|
||||||
|
// is below, but here I change kind from string to int for performance
|
||||||
|
/*
|
||||||
|
env.with_local_frame(10, || {
|
||||||
|
let kind = env.new_string(kind)?;
|
||||||
|
env.call_method(
|
||||||
|
ctx,
|
||||||
|
"rustPointerInput",
|
||||||
|
"(Ljava/lang/String;III)V",
|
||||||
|
&[
|
||||||
|
JValue::Object(&JObject::from(kind)),
|
||||||
|
JValue::Int(mask),
|
||||||
|
JValue::Int(x),
|
||||||
|
JValue::Int(y),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(JObject::null())
|
||||||
|
})?;
|
||||||
|
*/
|
||||||
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
|
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
|
||||||
if let (Some(jvm), Some(ctx)) = (
|
if let (Some(jvm), Some(ctx)) = (
|
||||||
JVM.read().unwrap().as_ref(),
|
JVM.read().unwrap().as_ref(),
|
||||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||||
) {
|
) {
|
||||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||||
let kind = env.new_string(kind)?;
|
let kind = if kind == "touch" { 0 } else { 1 };
|
||||||
env.call_method(
|
env.call_method(
|
||||||
ctx,
|
ctx,
|
||||||
"rustPointerInput",
|
"rustPointerInput",
|
||||||
"(Ljava/lang/String;III)V",
|
"(IIII)V",
|
||||||
&[
|
&[
|
||||||
JValue::Object(&JObject::from(kind)),
|
JValue::Int(kind),
|
||||||
JValue::Int(mask),
|
JValue::Int(mask),
|
||||||
JValue::Int(x),
|
JValue::Int(x),
|
||||||
JValue::Int(y),
|
JValue::Int(y),
|
||||||
@@ -259,19 +278,22 @@ pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
|
|||||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||||
) {
|
) {
|
||||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||||
let name = env.new_string(name)?;
|
let res = env.with_local_frame(10, |env| -> JniResult<String> {
|
||||||
let res = env
|
let name = env.new_string(name)?;
|
||||||
.call_method(
|
let res = env
|
||||||
ctx,
|
.call_method(
|
||||||
"rustGetByName",
|
ctx,
|
||||||
"(Ljava/lang/String;)Ljava/lang/String;",
|
"rustGetByName",
|
||||||
&[JValue::Object(&JObject::from(name))],
|
"(Ljava/lang/String;)Ljava/lang/String;",
|
||||||
)?
|
&[JValue::Object(&JObject::from(name))],
|
||||||
.l()?;
|
)?
|
||||||
let res = JString::from(res);
|
.l()?;
|
||||||
let res = env.get_string(&res)?;
|
let res = JString::from(res);
|
||||||
let res = res.to_string_lossy().to_string();
|
let res = env.get_string(&res)?;
|
||||||
return Ok(res);
|
let res = res.to_string_lossy().to_string();
|
||||||
|
Ok(res)
|
||||||
|
})?;
|
||||||
|
Ok(res)
|
||||||
} else {
|
} else {
|
||||||
return Err(JniError::ThrowFailed(-1));
|
return Err(JniError::ThrowFailed(-1));
|
||||||
}
|
}
|
||||||
@@ -287,20 +309,23 @@ pub fn call_main_service_set_by_name(
|
|||||||
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
|
||||||
) {
|
) {
|
||||||
let mut env = jvm.attach_current_thread_as_daemon()?;
|
let mut env = jvm.attach_current_thread_as_daemon()?;
|
||||||
let name = env.new_string(name)?;
|
env.with_local_frame(10, |env| -> JniResult<()> {
|
||||||
let arg1 = env.new_string(arg1.unwrap_or(""))?;
|
let name = env.new_string(name)?;
|
||||||
let arg2 = env.new_string(arg2.unwrap_or(""))?;
|
let arg1 = env.new_string(arg1.unwrap_or(""))?;
|
||||||
|
let arg2 = env.new_string(arg2.unwrap_or(""))?;
|
||||||
|
|
||||||
env.call_method(
|
env.call_method(
|
||||||
ctx,
|
ctx,
|
||||||
"rustSetByName",
|
"rustSetByName",
|
||||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
|
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
|
||||||
&[
|
&[
|
||||||
JValue::Object(&JObject::from(name)),
|
JValue::Object(&JObject::from(name)),
|
||||||
JValue::Object(&JObject::from(arg1)),
|
JValue::Object(&JObject::from(arg1)),
|
||||||
JValue::Object(&JObject::from(arg2)),
|
JValue::Object(&JObject::from(arg2)),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
return Err(JniError::ThrowFailed(-1));
|
return Err(JniError::ThrowFailed(-1));
|
||||||
|
|||||||
@@ -182,3 +182,8 @@ fn get_size() -> Option<(u16, u16, u16)> {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_start() -> Option<bool> {
|
||||||
|
let res = call_main_service_get_by_name("is_start").ok()?;
|
||||||
|
Some(res == "true")
|
||||||
|
}
|
||||||
|
|||||||
@@ -308,6 +308,8 @@ impl EncoderApi for AomEncoder {
|
|||||||
fn is_hardware(&self) -> bool {
|
fn is_hardware(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn disable(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AomEncoder {
|
impl AomEncoder {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ use crate::{
|
|||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
anyhow::anyhow,
|
anyhow::anyhow,
|
||||||
bail,
|
bail,
|
||||||
config::PeerConfig,
|
config::{keys::OPTION_ENABLE_HWCODEC, option2bool, Config, PeerConfig},
|
||||||
log,
|
lazy_static, log,
|
||||||
message_proto::{
|
message_proto::{
|
||||||
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
|
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
|
||||||
SupportedDecoding, SupportedEncoding, VideoFrame,
|
SupportedDecoding, SupportedEncoding, VideoFrame,
|
||||||
@@ -31,8 +31,6 @@ use hbb_common::{
|
|||||||
tokio::time::Instant,
|
tokio::time::Instant,
|
||||||
ResultType,
|
ResultType,
|
||||||
};
|
};
|
||||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec", feature = "vram"))]
|
|
||||||
use hbb_common::{config::Config2, lazy_static};
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
|
static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
|
||||||
@@ -76,6 +74,8 @@ pub trait EncoderApi {
|
|||||||
fn latency_free(&self) -> bool;
|
fn latency_free(&self) -> bool;
|
||||||
|
|
||||||
fn is_hardware(&self) -> bool;
|
fn is_hardware(&self) -> bool;
|
||||||
|
|
||||||
|
fn disable(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Encoder {
|
pub struct Encoder {
|
||||||
@@ -144,11 +144,7 @@ impl Encoder {
|
|||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("new hw encoder failed: {e:?}, clear config");
|
log::error!("new hw encoder failed: {e:?}, clear config");
|
||||||
#[cfg(target_os = "android")]
|
HwCodecConfig::clear(false, true);
|
||||||
crate::android::ffi::clear_codec_info();
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
hbb_common::config::HwCodecConfig::clear_ram();
|
|
||||||
Self::update(EncodingUpdate::Check);
|
|
||||||
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
|
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
@@ -160,8 +156,7 @@ impl Encoder {
|
|||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("new vram encoder failed: {e:?}, clear config");
|
log::error!("new vram encoder failed: {e:?}, clear config");
|
||||||
hbb_common::config::HwCodecConfig::clear_vram();
|
HwCodecConfig::clear(true, true);
|
||||||
Self::update(EncodingUpdate::Check);
|
|
||||||
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
|
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
@@ -204,7 +199,7 @@ impl Encoder {
|
|||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut h265vram_encoding = false;
|
let mut h265vram_encoding = false;
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
if enable_vram_option() {
|
if enable_vram_option(true) {
|
||||||
if _all_support_h264_decoding {
|
if _all_support_h264_decoding {
|
||||||
if VRamEncoder::available(CodecFormat::H264).len() > 0 {
|
if VRamEncoder::available(CodecFormat::H264).len() > 0 {
|
||||||
h264vram_encoding = true;
|
h264vram_encoding = true;
|
||||||
@@ -343,7 +338,7 @@ impl Encoder {
|
|||||||
encoding.h265 |= HwRamEncoder::try_get(CodecFormat::H265).is_some();
|
encoding.h265 |= HwRamEncoder::try_get(CodecFormat::H265).is_some();
|
||||||
}
|
}
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
if enable_vram_option() {
|
if enable_vram_option(true) {
|
||||||
encoding.h264 |= VRamEncoder::available(CodecFormat::H264).len() > 0;
|
encoding.h264 |= VRamEncoder::available(CodecFormat::H264).len() > 0;
|
||||||
encoding.h265 |= VRamEncoder::available(CodecFormat::H265).len() > 0;
|
encoding.h265 |= VRamEncoder::available(CodecFormat::H265).len() > 0;
|
||||||
}
|
}
|
||||||
@@ -454,7 +449,7 @@ impl Decoder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
if enable_vram_option() && _use_texture_render {
|
if enable_vram_option(false) && _use_texture_render {
|
||||||
decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 {
|
decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
@@ -533,7 +528,7 @@ impl Decoder {
|
|||||||
}
|
}
|
||||||
CodecFormat::H264 => {
|
CodecFormat::H264 => {
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
if !valid && enable_vram_option() && _luid.clone().unwrap_or_default() != 0 {
|
if !valid && enable_vram_option(false) && _luid.clone().unwrap_or_default() != 0 {
|
||||||
match VRamDecoder::new(format, _luid) {
|
match VRamDecoder::new(format, _luid) {
|
||||||
Ok(v) => h264_vram = Some(v),
|
Ok(v) => h264_vram = Some(v),
|
||||||
Err(e) => log::error!("create H264 vram decoder failed: {}", e),
|
Err(e) => log::error!("create H264 vram decoder failed: {}", e),
|
||||||
@@ -559,7 +554,7 @@ impl Decoder {
|
|||||||
}
|
}
|
||||||
CodecFormat::H265 => {
|
CodecFormat::H265 => {
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
if !valid && enable_vram_option() && _luid.clone().unwrap_or_default() != 0 {
|
if !valid && enable_vram_option(false) && _luid.clone().unwrap_or_default() != 0 {
|
||||||
match VRamDecoder::new(format, _luid) {
|
match VRamDecoder::new(format, _luid) {
|
||||||
Ok(v) => h265_vram = Some(v),
|
Ok(v) => h265_vram = Some(v),
|
||||||
Err(e) => log::error!("create H265 vram decoder failed: {}", e),
|
Err(e) => log::error!("create H265 vram decoder failed: {}", e),
|
||||||
@@ -842,19 +837,37 @@ impl Decoder {
|
|||||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||||
pub fn enable_hwcodec_option() -> bool {
|
pub fn enable_hwcodec_option() -> bool {
|
||||||
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android") {
|
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android") {
|
||||||
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
|
return option2bool(
|
||||||
return v != "N";
|
OPTION_ENABLE_HWCODEC,
|
||||||
}
|
&Config::get_option(OPTION_ENABLE_HWCODEC),
|
||||||
return true; // default is true
|
);
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn enable_vram_option() -> bool {
|
pub fn enable_vram_option(encode: bool) -> bool {
|
||||||
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
|
if cfg!(windows) {
|
||||||
return v != "N";
|
let enable = option2bool(
|
||||||
|
OPTION_ENABLE_HWCODEC,
|
||||||
|
&Config::get_option(OPTION_ENABLE_HWCODEC),
|
||||||
|
);
|
||||||
|
if encode {
|
||||||
|
enable && enable_directx_capture()
|
||||||
|
} else {
|
||||||
|
enable
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
return true; // default is true
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn enable_directx_capture() -> bool {
|
||||||
|
use hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE as OPTION;
|
||||||
|
option2bool(
|
||||||
|
OPTION,
|
||||||
|
&Config::get_option(hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user