mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-19 23:29:32 +08:00
Compare commits
382 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d56717cf5 | ||
|
|
32ef5f47f8 | ||
|
|
32346c23e0 | ||
|
|
b6ebf61d6c | ||
|
|
d79efcedef | ||
|
|
18464ec570 | ||
|
|
ed5487a1fc | ||
|
|
df36580451 | ||
|
|
bd7790c1eb | ||
|
|
67d66c6750 | ||
|
|
1c00d7aa1a | ||
|
|
68cabe596d | ||
|
|
8919ea65e3 | ||
|
|
d4dda94e2a | ||
|
|
ee58b37d1e | ||
|
|
06cb49ec71 | ||
|
|
caca7e5860 | ||
|
|
3ddb4c9799 | ||
|
|
74c24caae9 | ||
|
|
48e8a25f6e | ||
|
|
fd045043a1 | ||
|
|
0e97696b47 | ||
|
|
804764d529 | ||
|
|
98a38754d4 | ||
|
|
837382349e | ||
|
|
d2f119b85e | ||
|
|
89d855d085 | ||
|
|
1bfafaf07c | ||
|
|
cf4f073153 | ||
|
|
aca9ba1a49 | ||
|
|
399e20a14a | ||
|
|
7ca5a0b977 | ||
|
|
74d4505b3d | ||
|
|
df74a38b90 | ||
|
|
e8d02905fe | ||
|
|
123a45149d | ||
|
|
72ec86b58d | ||
|
|
010b17509a | ||
|
|
278d593580 | ||
|
|
f997a1ff52 | ||
|
|
036d10cfbe | ||
|
|
b0c21e927b | ||
|
|
fcece3732c | ||
|
|
c7308dbbc9 | ||
|
|
9ce62dc584 | ||
|
|
0442f7012b | ||
|
|
e7f0f0ff8d | ||
|
|
4fd4b24fa2 | ||
|
|
17d18f1dd8 | ||
|
|
902e166f0c | ||
|
|
1efce51222 | ||
|
|
7da09f6296 | ||
|
|
b8d9c4c378 | ||
|
|
49b0630752 | ||
|
|
91f07b4b03 | ||
|
|
b2f4ba0882 | ||
|
|
1acd7bd19c | ||
|
|
534fc9c40c | ||
|
|
e192f10c56 | ||
|
|
676ee99709 | ||
|
|
6e5622a97a | ||
|
|
d8c9250aab | ||
|
|
986b9fb0e0 | ||
|
|
3c502c6fc2 | ||
|
|
72d59af7b0 | ||
|
|
d3eaa6600d | ||
|
|
96f41fcc02 | ||
|
|
c2b7810c33 | ||
|
|
44d4e13fa7 | ||
|
|
1dd7cd9384 | ||
|
|
8357d4675a | ||
|
|
3a4390e0c7 | ||
|
|
68fa688c96 | ||
|
|
42428261d7 | ||
|
|
e01b1ed04d | ||
|
|
4e5dcd827b | ||
|
|
e8003510ef | ||
|
|
da23e26a70 | ||
|
|
c5b781fb02 | ||
|
|
3bb1c22f49 | ||
|
|
53647fd58e | ||
|
|
0500bf070e | ||
|
|
d70b0cdd4f | ||
|
|
7e09809ad8 | ||
|
|
a7499c2de8 | ||
|
|
4c99b8c70e | ||
|
|
8b6913d31f | ||
|
|
97f0642a8b | ||
|
|
a04dd6ad31 | ||
|
|
9827c76514 | ||
|
|
712bfbae92 | ||
|
|
0152e937ec | ||
|
|
c066dc8c24 | ||
|
|
0b96b3f345 | ||
|
|
b5781933b6 | ||
|
|
d851bf8b69 | ||
|
|
b6d6a4360f | ||
|
|
69b11e8dc6 | ||
|
|
6e78037770 | ||
|
|
bed0375f45 | ||
|
|
42d723f82f | ||
|
|
fb0ce8c974 | ||
|
|
d55770f12b | ||
|
|
125c275623 | ||
|
|
c640f7ed12 | ||
|
|
73662ed7d9 | ||
|
|
a595d83232 | ||
|
|
6743b4f290 | ||
|
|
e8ecb738d0 | ||
|
|
9c20968e05 | ||
|
|
9f80690d8a | ||
|
|
e58a95e7d9 | ||
|
|
d9b96d2f31 | ||
|
|
bbe9017318 | ||
|
|
a31a68ba17 | ||
|
|
9d56e29a09 | ||
|
|
32fca6f9b3 | ||
|
|
72c2de575a | ||
|
|
93d4987dcc | ||
|
|
2a25034039 | ||
|
|
5ba1c2587d | ||
|
|
1b0a3e610e | ||
|
|
2820f3f798 | ||
|
|
09f3850250 | ||
|
|
35832f8f7f | ||
|
|
7466d8cb3a | ||
|
|
ea3786457b | ||
|
|
01322146c0 | ||
|
|
26adc557bf | ||
|
|
36e52d8165 | ||
|
|
4c0211fa23 | ||
|
|
a3b4dcf762 | ||
|
|
5537c525ca | ||
|
|
3746fd88b5 | ||
|
|
e373144350 | ||
|
|
3be4bfc821 | ||
|
|
2c1595d0d5 | ||
|
|
f08933f93c | ||
|
|
51813b7d7e | ||
|
|
4e30418f79 | ||
|
|
0af370d736 | ||
|
|
ac04a032ad | ||
|
|
f6223a6f71 | ||
|
|
937cea5a01 | ||
|
|
846522037f | ||
|
|
b5a88d00af | ||
|
|
7391271107 | ||
|
|
991694aca5 | ||
|
|
29b13d19d6 | ||
|
|
b0ff74c799 | ||
|
|
75c60660e7 | ||
|
|
eacc840c54 | ||
|
|
6d09c0adcf | ||
|
|
fe1cf1105a | ||
|
|
9454334ddd | ||
|
|
cc482c589c | ||
|
|
b387bc1038 | ||
|
|
890b735985 | ||
|
|
eb81e00d20 | ||
|
|
c619bfc05c | ||
|
|
b5935eb4c5 | ||
|
|
73b3023c2d | ||
|
|
4cf709fc26 | ||
|
|
b471b934f2 | ||
|
|
abbd4c8934 | ||
|
|
ed8c852d35 | ||
|
|
c3598b3513 | ||
|
|
b5f6a9c91a | ||
|
|
d18810b612 | ||
|
|
3c17d713b3 | ||
|
|
d46b2d52ce | ||
|
|
e07d0b3b25 | ||
|
|
2ff3b24b33 | ||
|
|
115fb05c85 | ||
|
|
1e7e72883d | ||
|
|
1edb7639f6 | ||
|
|
33c5ceecec | ||
|
|
a8a7f306fe | ||
|
|
9888e51b93 | ||
|
|
c3124314ef | ||
|
|
975cd183bc | ||
|
|
aa9da0b0c8 | ||
|
|
d83b106607 | ||
|
|
1a0a6cedc7 | ||
|
|
d4e1a83a5a | ||
|
|
03e93d546a | ||
|
|
a1b957b0f7 | ||
|
|
eeffb11841 | ||
|
|
860dc7dbc2 | ||
|
|
220f267976 | ||
|
|
b5bd6e008b | ||
|
|
9d20bc6441 | ||
|
|
1c36734092 | ||
|
|
9102b6e6b8 | ||
|
|
cd78fa9762 | ||
|
|
407e2d4b7b | ||
|
|
62d6ee46c4 | ||
|
|
f15bf8166f | ||
|
|
7ce0b225ef | ||
|
|
f853b29fd9 | ||
|
|
f9cc8de93e | ||
|
|
a924a876ae | ||
|
|
1f15d2c66e | ||
|
|
ffd8354208 | ||
|
|
b3b1f4664c | ||
|
|
03410ded9a | ||
|
|
09c9671376 | ||
|
|
736b3b93e9 | ||
|
|
4a71ca6b81 | ||
|
|
a828f9b1f9 | ||
|
|
7ce5f8e8b6 | ||
|
|
08b7e036b4 | ||
|
|
b1722a085e | ||
|
|
9d985585b9 | ||
|
|
de057b7234 | ||
|
|
04ec49e18e | ||
|
|
34c7c25908 | ||
|
|
f74374e759 | ||
|
|
cce74f5631 | ||
|
|
003b9e48e1 | ||
|
|
28bf73cd5a | ||
|
|
511b982dce | ||
|
|
dcd176f95c | ||
|
|
d394aa8a15 | ||
|
|
c8b35d5ce1 | ||
|
|
6cd107c3e3 | ||
|
|
a884e32816 | ||
|
|
45d2d3cbb5 | ||
|
|
382f75d249 | ||
|
|
e90010f0c0 | ||
|
|
d6aaed1d82 | ||
|
|
5082ed3b9e | ||
|
|
a858e07db0 | ||
|
|
df8f785731 | ||
|
|
332674a4a1 | ||
|
|
a686849bb6 | ||
|
|
54d041701c | ||
|
|
496eec17a1 | ||
|
|
25eb67c582 | ||
|
|
a26fab3cce | ||
|
|
7cb67cf8fb | ||
|
|
96ec1e937f | ||
|
|
b6616ed2cc | ||
|
|
7e263af75f | ||
|
|
b4c51f3d41 | ||
|
|
1dfbaa1e02 | ||
|
|
bd717349a7 | ||
|
|
99d7752e25 | ||
|
|
22e58d7623 | ||
|
|
30ad142868 | ||
|
|
e084ff4f7b | ||
|
|
3bbec4081a | ||
|
|
45137d5506 | ||
|
|
e9d9a656ab | ||
|
|
ade458b820 | ||
|
|
b022dcbb70 | ||
|
|
b403a7a25d | ||
|
|
a6632632fa | ||
|
|
b863ea51ad | ||
|
|
796b66b057 | ||
|
|
2626dcbc5f | ||
|
|
105a758914 | ||
|
|
98cce186c7 | ||
|
|
474e13f8b9 | ||
|
|
09f87238dc | ||
|
|
2b3f87d6f2 | ||
|
|
9d3c823603 | ||
|
|
7e00d70f4d | ||
|
|
0828f747e3 | ||
|
|
d7de0c2578 | ||
|
|
4dfc82f684 | ||
|
|
92748de7d4 | ||
|
|
0e7e27f99d | ||
|
|
23147f2328 | ||
|
|
3811f41076 | ||
|
|
43a0a4f8e0 | ||
|
|
66d1af63b0 | ||
|
|
bc68f400f6 | ||
|
|
67ee3a5a67 | ||
|
|
b407893db5 | ||
|
|
1b4a41b522 | ||
|
|
ac7f8a6447 | ||
|
|
da57fcb641 | ||
|
|
f11c332cb4 | ||
|
|
3b4006b821 | ||
|
|
4c62d8c1b2 | ||
|
|
4f47d4482b | ||
|
|
ad062486ff | ||
|
|
33c8bdfabf | ||
|
|
0365c94407 | ||
|
|
c81c4f9114 | ||
|
|
d920953df1 | ||
|
|
f02a2e44d8 | ||
|
|
fb1aa9c028 | ||
|
|
d4a1d4cd7e | ||
|
|
8a6d9a1496 | ||
|
|
a8eefbc9f0 | ||
|
|
75521fe363 | ||
|
|
c75778943f | ||
|
|
0e6fa37ae4 | ||
|
|
819eea9456 | ||
|
|
25eb8dc9b0 | ||
|
|
40067f5aa2 | ||
|
|
1ffc10e44f | ||
|
|
5d5547ffef | ||
|
|
e83c28bf54 | ||
|
|
a3c0911529 | ||
|
|
ae23f0de03 | ||
|
|
edb5529df3 | ||
|
|
4d3fb77786 | ||
|
|
24ac7c1626 | ||
|
|
9b2ec62be9 | ||
|
|
4252b5e273 | ||
|
|
4e8cbe3db1 | ||
|
|
bdf8bbe26f | ||
|
|
990c05fc3d | ||
|
|
7bb4e22a77 | ||
|
|
736503df1b | ||
|
|
414455a8fb | ||
|
|
bc0ab88e74 | ||
|
|
01ec539065 | ||
|
|
b9792fc17d | ||
|
|
c656c3c087 | ||
|
|
a6c1d2d486 | ||
|
|
44bce59777 | ||
|
|
9b5e5aa474 | ||
|
|
a5d02998ad | ||
|
|
1d4c129e9c | ||
|
|
e9a6ca8ebc | ||
|
|
28340c80dd | ||
|
|
05f6fde467 | ||
|
|
56d353cb64 | ||
|
|
260d0cdc67 | ||
|
|
cdd92303b8 | ||
|
|
0dba37f4f7 | ||
|
|
7f3775a061 | ||
|
|
a7a10f4eaa | ||
|
|
5166083406 | ||
|
|
5c4d95ac0f | ||
|
|
71c4d74759 | ||
|
|
725e8221a5 | ||
|
|
92ae41cc13 | ||
|
|
333b9130fe | ||
|
|
60ae903cf3 | ||
|
|
24ea55b010 | ||
|
|
8231d07706 | ||
|
|
98df2b111e | ||
|
|
d8875f381b | ||
|
|
6e3d16173a | ||
|
|
a88b189664 | ||
|
|
48da00eb66 | ||
|
|
5322332c5d | ||
|
|
c92cfbb20a | ||
|
|
f673647072 | ||
|
|
7ea5a9bba3 | ||
|
|
aa002c5d60 | ||
|
|
64020758d9 | ||
|
|
a4f357fd80 | ||
|
|
4c6fdfd76a | ||
|
|
c972452310 | ||
|
|
6ba6a16836 | ||
|
|
c63f1dfc53 | ||
|
|
ac79c45529 | ||
|
|
7f58737f1f | ||
|
|
ead8a48436 | ||
|
|
178d33155f | ||
|
|
07ab8e508c | ||
|
|
ea1d4adfa6 | ||
|
|
f34a8ef0e5 | ||
|
|
0df4b39bcc | ||
|
|
07eca00bd5 | ||
|
|
cf8386aa50 | ||
|
|
44de6a5549 | ||
|
|
a41a9bcbf7 | ||
|
|
a679fae9c0 | ||
|
|
0f906f3937 | ||
|
|
c907daa741 | ||
|
|
df579de147 | ||
|
|
e884bdbbc4 | ||
|
|
84fc70e9a9 | ||
|
|
4fc5d3f03b | ||
|
|
9402516acd |
7
.github/workflows/bridge.yml
vendored
7
.github/workflows/bridge.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
env:
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
|
||||
jobs:
|
||||
generate_bridge:
|
||||
@@ -37,10 +38,8 @@ jobs:
|
||||
git \
|
||||
g++ \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libgtk-3-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
@@ -49,9 +48,9 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: ''
|
||||
components: "rustfmt"
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
|
||||
243
.github/workflows/build-macos-arm64.yml
vendored
243
.github/workflows/build-macos-arm64.yml
vendored
@@ -7,55 +7,224 @@ on:
|
||||
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.16.9"
|
||||
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"
|
||||
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2023.10.19
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
||||
VERSION: "1.2.4"
|
||||
NDK_VERSION: "r26b"
|
||||
# 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 }}'
|
||||
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: true
|
||||
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-for-macOS-arm64:
|
||||
name: build-for-macOS-arm64
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
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: Import the codesign cert
|
||||
# if: env.MACOS_P12_BASE64 != null
|
||||
# uses: apple-actions/import-codesign-certs@v1
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||
# p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||
# keychain: rustdesk
|
||||
|
||||
#- name: Check sign and import sign key
|
||||
# if: env.MACOS_P12_BASE64 != null
|
||||
# run: |
|
||||
# security default-keychain -s rustdesk.keychain
|
||||
# security find-identity -v
|
||||
|
||||
- name: Run
|
||||
- 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: |
|
||||
cd /opt/build
|
||||
#./update_mac_template.sh
|
||||
#security default-keychain -s rustdesk.keychain
|
||||
#security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||
./agent.sh
|
||||
# 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
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -21,6 +21,9 @@ on:
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- "res/**"
|
||||
- "appimage/**"
|
||||
- "flatpak/**"
|
||||
|
||||
jobs:
|
||||
# ensure_cargo_fmt:
|
||||
@@ -71,12 +74,12 @@ jobs:
|
||||
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
@@ -103,6 +106,7 @@ jobs:
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libpam0g-dev \
|
||||
libasound2-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
|
||||
1487
.github/workflows/flutter-build.yml
vendored
1487
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.github/workflows/flutter-ci.yml
vendored
3
.github/workflows/flutter-ci.yml
vendored
@@ -13,6 +13,9 @@ on:
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- "res/**"
|
||||
- "appimage/**"
|
||||
- "flatpak/**"
|
||||
|
||||
jobs:
|
||||
run-ci:
|
||||
|
||||
22
.github/workflows/flutter-tag.yml
vendored
22
.github/workflows/flutter-tag.yml
vendored
@@ -15,24 +15,4 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
upload-artifact: true
|
||||
upload-tag: ${{ env.GITHUB_REF_NAME }}
|
||||
|
||||
update-fdroid-version-file:
|
||||
name: Publish RustDesk version file for F-Droid updater
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate RustDesk version file
|
||||
run: |
|
||||
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
|
||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
|
||||
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||
shell: bash
|
||||
|
||||
- name: Publish RustDesk version file
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: "fdroid-version"
|
||||
files: |
|
||||
./rustdesk-version.txt
|
||||
upload-tag: ${{ github.ref_name }}
|
||||
4
.github/workflows/history.yml
vendored
4
.github/workflows/history.yml
vendored
@@ -8,7 +8,7 @@ env:
|
||||
TAG_NAME: "tmp"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VERSION: "1.2.4"
|
||||
VERSION: "1.2.5"
|
||||
|
||||
jobs:
|
||||
build-for-history-windows:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
description: 'Target'
|
||||
required: true
|
||||
type: string
|
||||
default: 'windows-2019'
|
||||
default: 'windows-2022'
|
||||
configuration:
|
||||
description: 'Configuration'
|
||||
required: true
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -51,4 +51,6 @@ lib/generated_bridge.dart
|
||||
# build cache in examples
|
||||
examples/**/target/
|
||||
# ===
|
||||
vcpkg_installed
|
||||
vcpkg_installed
|
||||
flutter/lib/generated_plugin_registrant.dart
|
||||
libsciter.dylib
|
||||
|
||||
441
Cargo.lock
generated
441
Cargo.lock
generated
@@ -115,17 +115,6 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "amf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
"gpu_common",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -311,9 +300,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.5"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
@@ -528,6 +517,12 @@ version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@@ -768,9 +763,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
dependencies = [
|
||||
"serde 1.0.190",
|
||||
]
|
||||
@@ -1106,7 +1101,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "confy"
|
||||
version = "0.4.0-2"
|
||||
source = "git+https://github.com/open-trade/confy#7855cd3c32b1a60b44e5076ee8f6b4131da10350"
|
||||
source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479"
|
||||
dependencies = [
|
||||
"directories-next",
|
||||
"serde 1.0.190",
|
||||
@@ -1836,9 +1831,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
version = "0.8.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
||||
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
@@ -2097,7 +2092,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
dependencies = [
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2649,36 +2644,6 @@ dependencies = [
|
||||
"system-deps 6.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpu_common"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
"log",
|
||||
"serde 1.0.190",
|
||||
"serde_derive",
|
||||
"serde_json 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpucodec"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
||||
dependencies = [
|
||||
"amf",
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
"gpu_common",
|
||||
"log",
|
||||
"nv",
|
||||
"serde 1.0.190",
|
||||
"serde_derive",
|
||||
"serde_json 1.0.107",
|
||||
"vpl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer"
|
||||
version = "0.16.7"
|
||||
@@ -2866,9 +2831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.24"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -2917,6 +2882,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace",
|
||||
"base64 0.22.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"confy",
|
||||
@@ -2928,6 +2894,7 @@ dependencies = [
|
||||
"flexi_logger",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"httparse",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -2936,19 +2903,24 @@ dependencies = [
|
||||
"osascript",
|
||||
"protobuf",
|
||||
"protobuf-codegen",
|
||||
"quinn",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde 1.0.190",
|
||||
"serde_derive",
|
||||
"serde_json 1.0.107",
|
||||
"socket2 0.3.19",
|
||||
"sodiumoxide",
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-socks",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-socks 0.5.2",
|
||||
"tokio-util",
|
||||
"toml 0.7.8",
|
||||
"url",
|
||||
"uuid",
|
||||
"winapi 0.3.9",
|
||||
"zstd 0.13.0",
|
||||
@@ -3025,9 +2997,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -3036,9 +3008,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
@@ -3065,8 +3037,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/21pages/hwcodec?branch=stable#52e1da2aae86acec5f374bc065f5921945b55e7b"
|
||||
version = "0.4.15"
|
||||
source = "git+https://github.com/21pages/hwcodec#1d504ee590c15472813fecc22cee4b8149b2b8cd"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -3078,9 +3050,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.27"
|
||||
version = "0.14.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
|
||||
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -3093,7 +3065,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa 1.0.9",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.10",
|
||||
"socket2 0.5.5",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -3111,7 +3083,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"rustls 0.21.10",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3791,15 +3763,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mouce"
|
||||
version = "0.2.1"
|
||||
@@ -4134,17 +4097,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nv"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
"gpu_common",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@@ -4432,12 +4384,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-tokio-ipc"
|
||||
version = "0.7.3-3"
|
||||
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#f2d1fcf8fb002335d9a62bec308559d40698694d"
|
||||
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#e8448ade10d6d972d0b2307646424b36ab202ff5"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"rand 0.8.5",
|
||||
"tokio",
|
||||
"winapi 0.3.9",
|
||||
@@ -4616,7 +4567,7 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.5",
|
||||
"indexmap 1.9.3",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
@@ -4762,9 +4713,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190"
|
||||
checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"once_cell",
|
||||
@@ -4774,9 +4725,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf-codegen"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78"
|
||||
checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"once_cell",
|
||||
@@ -4789,9 +4740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf-parse"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba"
|
||||
checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 1.9.3",
|
||||
@@ -4805,9 +4756,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf-support"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c"
|
||||
checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@@ -4860,56 +4811,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e8b432585672228923edbbf64b8b12c14e1112f62e88737655b4a083dbcd78e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.20.9",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94b0b33c13a79f669c85defaf4c275dc86a0c0372807d0ca3d78e0bb87274863"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring 0.16.20",
|
||||
"rustc-hash",
|
||||
"rustls 0.20.9",
|
||||
"rustls-native-certs",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"quinn-proto",
|
||||
"socket2 0.4.10",
|
||||
"tracing",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.13"
|
||||
@@ -5224,10 +5125,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.23"
|
||||
source = "git+https://github.com/rustdesk-org/reqwest"
|
||||
source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
@@ -5247,8 +5148,8 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.10",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"rustls-native-certs 0.6.3",
|
||||
"rustls-pemfile 1.0.3",
|
||||
"serde 1.0.190",
|
||||
"serde_json 1.0.107",
|
||||
"serde_urlencoded",
|
||||
@@ -5256,32 +5157,18 @@ dependencies = [
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-socks 0.5.1",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.25.4",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin 0.5.2",
|
||||
"untrusted 0.7.1",
|
||||
"web-sys",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.5"
|
||||
@@ -5291,8 +5178,8 @@ dependencies = [
|
||||
"cc",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"spin 0.9.8",
|
||||
"untrusted 0.9.0",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -5402,14 +5289,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
"arboard",
|
||||
"async-process",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -5500,7 +5386,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -5551,17 +5437,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
|
||||
dependencies = [
|
||||
"ring 0.16.20",
|
||||
"sct",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.10"
|
||||
@@ -5569,11 +5444,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring 0.17.5",
|
||||
"rustls-webpki",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.3"
|
||||
@@ -5581,7 +5471,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 1.0.3",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
@@ -5592,17 +5495,71 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"core-foundation-sys 0.8.4",
|
||||
"jni 0.19.0",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.4",
|
||||
"rustls-native-certs 0.7.0",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.102.2",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-roots 0.26.1",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring 0.17.5",
|
||||
"untrusted 0.9.0",
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5683,7 +5640,6 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"dbus",
|
||||
"docopt",
|
||||
"gpucodec",
|
||||
"gstreamer",
|
||||
"gstreamer-app",
|
||||
"gstreamer-video",
|
||||
@@ -5712,28 +5668,29 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring 0.17.5",
|
||||
"untrusted 0.9.0",
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.9.2"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
||||
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"core-foundation-sys 0.8.4",
|
||||
"libc",
|
||||
"num-bigint",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.9.1"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
|
||||
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
|
||||
dependencies = [
|
||||
"core-foundation-sys 0.8.4",
|
||||
"libc",
|
||||
@@ -5971,12 +5928,6 @@ dependencies = [
|
||||
"serde 1.0.190",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -6408,9 +6359,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
version = "1.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -6456,10 +6407,33 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls 0.23.4",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-socks"
|
||||
version = "0.5.1-2"
|
||||
source = "git+https://github.com/open-trade/tokio-socks#14a5c2564fa20a2765ea53d03c573ee2b7e20421"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
|
||||
dependencies = [
|
||||
"either",
|
||||
"futures-util",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-socks"
|
||||
version = "0.5.2"
|
||||
source = "git+https://github.com/rustdesk-org/tokio-socks#51037c93f8be34196fd2b6de9f674e8dfae3d01e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"either",
|
||||
@@ -6689,9 +6663,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
@@ -6782,12 +6756,6 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -6900,17 +6868,6 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vpl"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
"gpu_common",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.1"
|
||||
@@ -7133,20 +7090,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.4"
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
|
||||
dependencies = [
|
||||
"ring 0.17.5",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.3"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
|
||||
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
@@ -7370,21 +7326,6 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -7919,6 +7860,12 @@ dependencies = [
|
||||
"syn 2.0.55",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
|
||||
33
Cargo.toml
33
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -19,19 +19,14 @@ path = "src/naming.rs"
|
||||
[features]
|
||||
inline = []
|
||||
cli = []
|
||||
flutter_texture_render = []
|
||||
appimage = []
|
||||
flatpak = []
|
||||
use_samplerate = ["samplerate"]
|
||||
use_rubato = ["rubato"]
|
||||
use_dasp = ["dasp"]
|
||||
flutter = ["flutter_rust_bridge"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
gpucodec = ["scrap/gpucodec"]
|
||||
vram = ["scrap/vram"]
|
||||
mediacodec = ["scrap/mediacodec"]
|
||||
linux_headless = ["pam" ]
|
||||
virtual_display_driver = ["virtual_display"]
|
||||
plugin_framework = []
|
||||
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
||||
unix-file-copy-paste = [
|
||||
@@ -65,7 +60,6 @@ samplerate = { version = "0.2", optional = true }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
clap = "4.2"
|
||||
rpassword = "7.2"
|
||||
base64 = "0.21"
|
||||
num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
default-net = "0.14"
|
||||
@@ -100,10 +94,23 @@ system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
|
||||
winapi = { version = "0.3", features = [
|
||||
"winuser",
|
||||
"wincrypt",
|
||||
"shellscalingapi",
|
||||
"pdh",
|
||||
"synchapi",
|
||||
"memoryapi",
|
||||
"shellapi",
|
||||
"devguid",
|
||||
"setupapi",
|
||||
"cguid",
|
||||
"cfgmgr32",
|
||||
"ioapiset",
|
||||
] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
virtual_display = { path = "libs/virtual_display", optional = true }
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
@@ -132,10 +139,10 @@ wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false }
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
@@ -146,7 +153,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
|
||||
evdev = { git="https://github.com/fufesou/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
pam = { git="https://github.com/fufesou/pam" }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
|
||||
@@ -18,6 +18,7 @@ RUN apt update -y && \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libasound2-dev \
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
make \
|
||||
cmake \
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- bsdtar -zxvf rustdesk.deb
|
||||
- tar -xvf ./data.tar.xz
|
||||
- mkdir ./AppDir
|
||||
- mv ./usr ./AppDir/usr
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.2.4
|
||||
version: 1.2.5
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -26,18 +26,18 @@ AppDir:
|
||||
- arm64
|
||||
allow_unauthenticated: true
|
||||
sources:
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
include:
|
||||
- libc6
|
||||
- libc6:arm64
|
||||
- libgtk-3-0
|
||||
- libxcb-randr0
|
||||
- libxdo3
|
||||
@@ -51,9 +51,15 @@ AppDir:
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
- packagekit-gtk3-module
|
||||
- libcanberra-gtk3-module
|
||||
- libpam0g
|
||||
- libdrm2
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
@@ -69,8 +75,11 @@ AppDir:
|
||||
- usr/share/doc/*/TODO.*
|
||||
runtime:
|
||||
env:
|
||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
||||
GDK_BACKEND: x11
|
||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
|
||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||
test:
|
||||
fedora-30:
|
||||
image: appimagecrafters/tests-env:fedora-30
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- bsdtar -zxvf rustdesk.deb
|
||||
- tar -xvf ./data.tar.xz
|
||||
- mkdir ./AppDir
|
||||
- mv ./usr ./AppDir/usr
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.2.4
|
||||
version: 1.2.5
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -26,18 +26,16 @@ AppDir:
|
||||
- amd64
|
||||
allow_unauthenticated: true
|
||||
sources:
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
|
||||
bionic main
|
||||
include:
|
||||
- libc6:amd64
|
||||
- libgtk-3-0
|
||||
@@ -54,9 +52,14 @@ AppDir:
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
- packagekit-gtk3-module
|
||||
- libcanberra-gtk3-module
|
||||
- libpam0g
|
||||
- libdrm2
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
@@ -72,8 +75,11 @@ AppDir:
|
||||
- usr/share/doc/*/TODO.*
|
||||
runtime:
|
||||
env:
|
||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
||||
GDK_BACKEND: x11
|
||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
|
||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
test:
|
||||
fedora-30:
|
||||
image: appimagecrafters/tests-env:fedora-30
|
||||
|
||||
29
build.py
29
build.py
@@ -33,9 +33,9 @@ def get_arch() -> str:
|
||||
|
||||
|
||||
def system2(cmd):
|
||||
err = os.system(cmd)
|
||||
if err != 0:
|
||||
print(f"Error occurred when executing: {cmd}. Exiting.")
|
||||
exit_code = os.system(cmd)
|
||||
if exit_code != 0:
|
||||
sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
@@ -118,9 +118,9 @@ def make_parser():
|
||||
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--gpucodec',
|
||||
'--vram',
|
||||
action='store_true',
|
||||
help='Enable feature gpucodec, only available on windows now.'
|
||||
help='Enable feature vram, only available on windows now.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--portable',
|
||||
@@ -132,16 +132,6 @@ def make_parser():
|
||||
action='store_true',
|
||||
help='Build with unix file copy paste feature'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--flatpak',
|
||||
action='store_true',
|
||||
help='Build rustdesk libs with the flatpak feature enabled'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--appimage',
|
||||
action='store_true',
|
||||
help='Build rustdesk libs with the appimage feature enabled'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-cargo',
|
||||
action='store_true',
|
||||
@@ -282,15 +272,10 @@ def get_features(args):
|
||||
features = ['inline'] if not args.flutter else []
|
||||
if args.hwcodec:
|
||||
features.append('hwcodec')
|
||||
if args.gpucodec:
|
||||
features.append('gpucodec')
|
||||
if args.vram:
|
||||
features.append('vram')
|
||||
if args.flutter:
|
||||
features.append('flutter')
|
||||
features.append('flutter_texture_render')
|
||||
if args.flatpak:
|
||||
features.append('flatpak')
|
||||
if args.appimage:
|
||||
features.append('appimage')
|
||||
if args.unix_file_copy_paste:
|
||||
features.append('unix-file-copy-paste')
|
||||
print("features:", features)
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br>
|
||||
<a href="#free-public-servers">Máy chủ</a> •
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Server</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Cấu trúc tệp tin</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
|
||||
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
|
||||
</p>
|
||||
|
||||
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
**RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
|
||||
|
||||
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
|
||||
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
@@ -29,28 +31,25 @@ Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để
|
||||
|
||||
## Dependencies
|
||||
|
||||
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bạn cần tự tải về thư viện sciter.
|
||||
Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bản Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
|
||||
|
||||
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính.
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Cách để build
|
||||
## Các bước build cơ bản
|
||||
|
||||
- Chuẩn bị môi trường phát triển Rust và môi trường build C++
|
||||
- Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
|
||||
|
||||
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng.
|
||||
|
||||
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
||||
- Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `VCPKG_ROOT`.
|
||||
|
||||
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
|
||||
- Linux/MacOS: `vcpkg install libvpx libyuv opus aom`
|
||||
- Chạy lệnh `cargo run`
|
||||
|
||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Cách để build cho Linux
|
||||
## Cách build cho Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
@@ -70,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Cách cài vcpkg
|
||||
### Cách cài đặt `vcpkg`
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
@@ -82,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora)
|
||||
### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
@@ -95,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Cách build
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
@@ -108,9 +107,9 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Cách để build sử dụng Docker
|
||||
## Cách build bằng Docker
|
||||
|
||||
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:
|
||||
Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@@ -118,37 +117,37 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này:
|
||||
Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, những lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh ‘--release’. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||
Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||
Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Hãy đảm bảo là bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn.
|
||||
Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
|
||||
|
||||
## Cấu trúc tệp tin
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed.
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
||||
|
||||
## Snapshot
|
||||
|
||||
@@ -8,17 +8,31 @@
|
||||
"modules": [
|
||||
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
||||
"xdotool.json",
|
||||
{
|
||||
"name": "pam",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"./configure --disable-selinux --prefix=/app && make -j4 install"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
|
||||
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rustdesk",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"bsdtar -zxvf rustdesk-1.2.4.deb",
|
||||
"bsdtar -zxvf rustdesk.deb",
|
||||
"tar -xvf ./data.tar.xz",
|
||||
"cp -r ./usr/* /app/",
|
||||
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
||||
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
|
||||
"mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
|
||||
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
|
||||
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
|
||||
],
|
||||
@@ -26,7 +40,7 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../rustdesk-1.2.4.deb"
|
||||
"path": "./rustdesk.deb"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -108,4 +108,3 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "768133699366",
|
||||
"firebase_url": "https://rustdesk.firebaseio.com",
|
||||
"project_id": "rustdesk",
|
||||
"storage_bucket": "rustdesk.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
|
||||
"android_client_info": {
|
||||
"package_name": "com.carriez.flutter_hbb"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package="com.carriez.flutter_hbb">
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.media.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.projection.MediaProjection
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.ActivityCompat
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||
const val AUDIO_SAMPLE_RATE = 48000
|
||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||
|
||||
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
||||
|
||||
private var audioRecorder: AudioRecord? = null
|
||||
private var audioReader: AudioReader? = null
|
||||
private var minBufferSize = 0
|
||||
private var audioRecordStat = false
|
||||
private var audioThread: Thread? = null
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return false
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission")
|
||||
return false
|
||||
}
|
||||
|
||||
var builder = AudioRecord.Builder()
|
||||
.setAudioFormat(
|
||||
AudioFormat.Builder()
|
||||
.setEncoding(AUDIO_ENCODING)
|
||||
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||
);
|
||||
if (inVoiceCall) {
|
||||
builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
|
||||
} else {
|
||||
mediaProjection?.let {
|
||||
var apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();
|
||||
builder.setAudioPlaybackCaptureConfig(apcc);
|
||||
} ?: let {
|
||||
Log.d(logTag, "createAudioRecorder failed, mediaProjection null")
|
||||
return false
|
||||
}
|
||||
}
|
||||
audioRecorder = builder.build()
|
||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun checkAudioReader() {
|
||||
if (audioReader != null && minBufferSize != 0) {
|
||||
return
|
||||
}
|
||||
// read f32 to byte , length * 4
|
||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||
AUDIO_SAMPLE_RATE,
|
||||
AUDIO_CHANNEL_MASK,
|
||||
AUDIO_ENCODING
|
||||
)
|
||||
if (minBufferSize == 0) {
|
||||
Log.d(logTag, "get min buffer size fail!")
|
||||
return
|
||||
}
|
||||
audioReader = AudioReader(minBufferSize, 4)
|
||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun startAudioRecorder() {
|
||||
checkAudioReader()
|
||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||
try {
|
||||
FFI.setFrameRawEnable("audio", true)
|
||||
audioRecorder!!.startRecording()
|
||||
audioRecordStat = true
|
||||
audioThread = thread {
|
||||
while (audioRecordStat) {
|
||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||
FFI.onAudioFrameUpdate(it)
|
||||
}
|
||||
}
|
||||
// let's release here rather than onDestroy to avoid threading issue
|
||||
audioRecorder?.release()
|
||||
audioRecorder = null
|
||||
minBufferSize = 0
|
||||
FFI.setFrameRawEnable("audio", false)
|
||||
Log.d(logTag, "Exit audio thread")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||
}
|
||||
} else {
|
||||
Log.d(logTag, "startAudioRecorder fail")
|
||||
}
|
||||
}
|
||||
|
||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return false
|
||||
}
|
||||
if (isVideoStart() || isAudioStart()) {
|
||||
if (!switchToVoiceCall(mediaProjection)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (!switchToVoiceCall(mediaProjection)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return false
|
||||
}
|
||||
if (isVideoStart()) {
|
||||
switchOutVoiceCall(mediaProjection)
|
||||
}
|
||||
tryReleaseAudio()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||
audioRecorder?.let {
|
||||
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
audioThread = null
|
||||
|
||||
if (!createAudioRecorder(true, mediaProjection)) {
|
||||
Log.e(logTag, "createAudioRecorder fail")
|
||||
return false
|
||||
}
|
||||
startAudioRecorder()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||
audioRecorder?.let {
|
||||
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
|
||||
if (!createAudioRecorder(false, mediaProjection)) {
|
||||
Log.e(logTag, "createAudioRecorder fail")
|
||||
return false
|
||||
}
|
||||
startAudioRecorder()
|
||||
return true
|
||||
}
|
||||
|
||||
fun tryReleaseAudio() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return
|
||||
}
|
||||
if (isAudioStart() || isVideoStart()) {
|
||||
return
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
audioThread = null
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Log.d(logTag, "destroy audio record handle")
|
||||
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ package com.carriez.flutter_hbb
|
||||
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
||||
*/
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -15,10 +17,20 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaFormat
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
@@ -30,6 +42,9 @@ class MainActivity : FlutterActivity() {
|
||||
private val logTag = "mMainActivity"
|
||||
private var mainService: MainService? = null
|
||||
|
||||
private var isAudioStart = false
|
||||
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
if (MainService.isReady) {
|
||||
@@ -42,6 +57,7 @@ class MainActivity : FlutterActivity() {
|
||||
channelTag
|
||||
)
|
||||
initFlutterChannel(flutterMethodChannel!!)
|
||||
thread { setCodecInfo() }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -217,10 +233,145 @@ class MainActivity : FlutterActivity() {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
"on_voice_call_started" -> {
|
||||
onVoiceCallStarted()
|
||||
}
|
||||
"on_voice_call_closed" -> {
|
||||
onVoiceCallClosed()
|
||||
}
|
||||
else -> {
|
||||
result.error("-1", "No such method", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCodecInfo() {
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
val codecs = codecList.codecInfos
|
||||
val codecArray = JSONArray()
|
||||
|
||||
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
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
|
||||
}
|
||||
val align = 64
|
||||
w = (w + align - 1) / align * align
|
||||
h = (h + align - 1) / align * align
|
||||
codecs.forEach { codec ->
|
||||
val codecObject = JSONObject()
|
||||
codecObject.put("name", codec.name)
|
||||
codecObject.put("is_encoder", codec.isEncoder)
|
||||
var hw: Boolean? = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
hw = codec.isHardwareAccelerated
|
||||
} else {
|
||||
// https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29
|
||||
// https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229
|
||||
if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) {
|
||||
hw = false
|
||||
} else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) {
|
||||
hw = true
|
||||
}
|
||||
}
|
||||
if (hw != true) {
|
||||
return@forEach
|
||||
}
|
||||
codecObject.put("hw", hw)
|
||||
var mime_type = ""
|
||||
codec.supportedTypes.forEach { type ->
|
||||
if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01"
|
||||
mime_type = type;
|
||||
}
|
||||
}
|
||||
if (mime_type.isNotEmpty()) {
|
||||
codecObject.put("mime_type", mime_type)
|
||||
val caps = codec.getCapabilitiesForType(mime_type)
|
||||
if (codec.isEncoder) {
|
||||
// Encoder‘s max_height and max_width are interchangeable
|
||||
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower)
|
||||
codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper)
|
||||
codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower)
|
||||
codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper)
|
||||
val surface = caps.colorFormats.contains(COLOR_FormatSurface);
|
||||
codecObject.put("surface", surface)
|
||||
val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar)
|
||||
codecObject.put("nv12", nv12)
|
||||
if (!(nv12 || surface)) {
|
||||
return@forEach
|
||||
}
|
||||
codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000)
|
||||
codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000)
|
||||
if (!codec.isEncoder) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency))
|
||||
}
|
||||
}
|
||||
if (!codec.isEncoder) {
|
||||
return@forEach
|
||||
}
|
||||
codecArray.put(codecObject)
|
||||
}
|
||||
}
|
||||
val result = JSONObject()
|
||||
result.put("version", Build.VERSION.SDK_INT)
|
||||
result.put("w", w)
|
||||
result.put("h", h)
|
||||
result.put("codecs", codecArray)
|
||||
FFI.setCodecInfo(result.toString())
|
||||
}
|
||||
|
||||
private fun onVoiceCallStarted() {
|
||||
var ok = false
|
||||
mainService?.let {
|
||||
ok = it.onVoiceCallStarted()
|
||||
} ?: let {
|
||||
isAudioStart = true
|
||||
ok = audioRecordHandle.onVoiceCallStarted(null)
|
||||
}
|
||||
if (!ok) {
|
||||
// Rarely happens, So we just add log and msgbox here.
|
||||
Log.e(logTag, "onVoiceCallStarted fail")
|
||||
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to start voice call."))
|
||||
} else {
|
||||
Log.d(logTag, "onVoiceCallStarted success")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVoiceCallClosed() {
|
||||
var ok = false
|
||||
mainService?.let {
|
||||
ok = it.onVoiceCallClosed()
|
||||
} ?: let {
|
||||
isAudioStart = false
|
||||
ok = audioRecordHandle.onVoiceCallClosed(null)
|
||||
}
|
||||
if (!ok) {
|
||||
// Rarely happens, So we just add log and msgbox here.
|
||||
Log.e(logTag, "onVoiceCallClosed fail")
|
||||
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to stop voice call."))
|
||||
} else {
|
||||
Log.d(logTag, "onVoiceCallClosed success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
/**
|
||||
* Capture screen,get video and audio,send to rust.
|
||||
* Dispatch notifications
|
||||
@@ -52,22 +54,14 @@ const val NOTIFY_ID_OFFSET = 100
|
||||
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
||||
|
||||
// video const
|
||||
|
||||
const val MAX_SCREEN_SIZE = 1200
|
||||
|
||||
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||
const val VIDEO_KEY_FRAME_RATE = 30
|
||||
|
||||
// audio const
|
||||
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||
const val AUDIO_SAMPLE_RATE = 48000
|
||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||
|
||||
class MainService : Service() {
|
||||
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
@Keep
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
||||
@@ -141,10 +135,51 @@ class MainService : Service() {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
"update_voice_call_state" -> {
|
||||
try {
|
||||
val jsonObject = JSONObject(arg1)
|
||||
val id = jsonObject["id"] as Int
|
||||
val username = jsonObject["name"] as String
|
||||
val peerId = jsonObject["peer_id"] as String
|
||||
val inVoiceCall = jsonObject["in_voice_call"] as Boolean
|
||||
val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean
|
||||
if (!inVoiceCall) {
|
||||
if (incomingVoiceCall) {
|
||||
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||
} else {
|
||||
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
||||
Log.e(logTag, "switchOutVoiceCall fail")
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to switch out voice call."))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
||||
Log.e(logTag, "switchToVoiceCall fail")
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to switch to voice call."))
|
||||
}
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
"stop_capture" -> {
|
||||
Log.d(logTag, "from rust:stop_capture")
|
||||
stopCapture()
|
||||
}
|
||||
"is_hardware_codec" -> {
|
||||
val isHwCodec = arg1.toBoolean()
|
||||
if (isHardwareCodec != isHwCodec) {
|
||||
isHardwareCodec = isHwCodec
|
||||
updateScreenInfo(resources.configuration.orientation)
|
||||
}
|
||||
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
@@ -156,38 +191,28 @@ class MainService : Service() {
|
||||
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")}
|
||||
|
||||
// jvm call rust
|
||||
private external fun init(ctx: Context)
|
||||
|
||||
/// When app start on boot, app_dir will not be passed from flutter
|
||||
/// so pass a app_dir here to rust server
|
||||
private external fun startServer(app_dir: String)
|
||||
private external fun startService()
|
||||
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
private external fun translateLocale(localeName: String, input: String): String
|
||||
private external fun refreshScreen()
|
||||
private external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
// private external fun sendVp9(data: ByteArray)
|
||||
|
||||
private fun translate(input: String): String {
|
||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
||||
return translateLocale(LOCAL_NAME, input)
|
||||
return FFI.translateLocale(LOCAL_NAME, input)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _isReady = false // media permission ready status
|
||||
private var _isStart = false // screen capture start status
|
||||
private var _isAudioStart = false // audio capture start status
|
||||
val isReady: Boolean
|
||||
get() = _isReady
|
||||
val isStart: Boolean
|
||||
get() = _isStart
|
||||
val isAudioStart: Boolean
|
||||
get() = _isAudioStart
|
||||
}
|
||||
|
||||
private val logTag = "LOG_SERVICE"
|
||||
private val useVP9 = false
|
||||
private val binder = LocalBinder()
|
||||
|
||||
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
|
||||
|
||||
// video
|
||||
private var mediaProjection: MediaProjection? = null
|
||||
@@ -198,10 +223,7 @@ class MainService : Service() {
|
||||
private var virtualDisplay: VirtualDisplay? = null
|
||||
|
||||
// audio
|
||||
private var audioRecorder: AudioRecord? = null
|
||||
private var audioReader: AudioReader? = null
|
||||
private var minBufferSize = 0
|
||||
private var audioRecordStat = false
|
||||
private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
|
||||
|
||||
// notification
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
@@ -210,8 +232,8 @@ class MainService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(logTag,"MainService onCreate")
|
||||
init(this)
|
||||
Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
|
||||
FFI.init(this)
|
||||
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
@@ -223,7 +245,7 @@ class MainService : Service() {
|
||||
// keep the config dir same with flutter
|
||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||
startServer(configPath)
|
||||
FFI.startServer(configPath, "")
|
||||
|
||||
createForegroundNotification()
|
||||
}
|
||||
@@ -233,6 +255,7 @@ class MainService : Service() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private var isHardwareCodec: Boolean? = null;
|
||||
private fun updateScreenInfo(orientation: Int) {
|
||||
var w: Int
|
||||
var h: Int
|
||||
@@ -265,7 +288,7 @@ class MainService : Service() {
|
||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||
var scale = 1
|
||||
if (w != 0 && h != 0) {
|
||||
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
|
||||
if (isHardwareCodec == false && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||
scale = 2
|
||||
w /= scale
|
||||
h /= scale
|
||||
@@ -278,7 +301,7 @@ class MainService : Service() {
|
||||
SCREEN_INFO.dpi = dpi
|
||||
if (isStart) {
|
||||
stopCapture()
|
||||
refreshScreen()
|
||||
FFI.refreshScreen()
|
||||
startCapture()
|
||||
}
|
||||
}
|
||||
@@ -306,7 +329,7 @@ class MainService : Service() {
|
||||
createForegroundNotification()
|
||||
|
||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||
startService()
|
||||
FFI.startService()
|
||||
}
|
||||
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
|
||||
val mediaProjectionManager =
|
||||
@@ -354,12 +377,13 @@ class MainService : Service() {
|
||||
).apply {
|
||||
setOnImageAvailableListener({ imageReader: ImageReader ->
|
||||
try {
|
||||
// If not call acquireLatestImage, listener will not be called again
|
||||
imageReader.acquireLatestImage().use { image ->
|
||||
if (image == null) return@setOnImageAvailableListener
|
||||
if (image == null || !isStart) return@setOnImageAvailableListener
|
||||
val planes = image.planes
|
||||
val buffer = planes[0].buffer
|
||||
buffer.rewind()
|
||||
onVideoFrameUpdate(buffer)
|
||||
FFI.onVideoFrameUpdate(buffer)
|
||||
}
|
||||
} catch (ignored: java.lang.Exception) {
|
||||
}
|
||||
@@ -370,6 +394,14 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onVoiceCallStarted(): Boolean {
|
||||
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||
}
|
||||
|
||||
fun onVoiceCallClosed(): Boolean {
|
||||
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||
}
|
||||
|
||||
fun startCapture(): Boolean {
|
||||
if (isStart) {
|
||||
return true
|
||||
@@ -378,6 +410,7 @@ class MainService : Service() {
|
||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||
return false
|
||||
}
|
||||
|
||||
updateScreenInfo(resources.configuration.orientation)
|
||||
Log.d(logTag, "Start Capture")
|
||||
surface = createSurface()
|
||||
@@ -389,47 +422,66 @@ class MainService : Service() {
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
startAudioRecorder()
|
||||
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
||||
Log.d(logTag, "createAudioRecorder fail")
|
||||
} else {
|
||||
Log.d(logTag, "audio recorder start")
|
||||
audioRecordHandle.startAudioRecorder()
|
||||
}
|
||||
}
|
||||
checkMediaPermission()
|
||||
_isStart = true
|
||||
setFrameRawEnable("video",true)
|
||||
setFrameRawEnable("audio",true)
|
||||
FFI.setFrameRawEnable("video",true)
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopCapture() {
|
||||
Log.d(logTag, "Stop Capture")
|
||||
setFrameRawEnable("video",false)
|
||||
setFrameRawEnable("audio",false)
|
||||
FFI.setFrameRawEnable("video",false)
|
||||
_isStart = false
|
||||
// release video
|
||||
virtualDisplay?.release()
|
||||
surface?.release()
|
||||
if (reuseVirtualDisplay) {
|
||||
// The virtual display video projection can be paused by calling `setSurface(null)`.
|
||||
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
|
||||
virtualDisplay?.setSurface(null)
|
||||
} else {
|
||||
virtualDisplay?.release()
|
||||
}
|
||||
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
videoEncoder?.let {
|
||||
it.signalEndOfInputStream()
|
||||
it.stop()
|
||||
it.release()
|
||||
}
|
||||
virtualDisplay = null
|
||||
if (!reuseVirtualDisplay) {
|
||||
virtualDisplay = null
|
||||
}
|
||||
videoEncoder = null
|
||||
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||
surface?.release()
|
||||
|
||||
// release audio
|
||||
audioRecordStat = false
|
||||
audioRecorder?.release()
|
||||
audioRecorder = null
|
||||
minBufferSize = 0
|
||||
_isAudioStart = false
|
||||
audioRecordHandle.tryReleaseAudio()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Log.d(logTag, "destroy service")
|
||||
_isReady = false
|
||||
_isAudioStart = false
|
||||
|
||||
stopCapture()
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
|
||||
if (reuseVirtualDisplay) {
|
||||
virtualDisplay?.release()
|
||||
virtualDisplay = null
|
||||
}
|
||||
|
||||
mediaProjection = null
|
||||
checkMediaPermission()
|
||||
@@ -459,11 +511,7 @@ class MainService : Service() {
|
||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||
return
|
||||
}
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
)
|
||||
createOrSetVirtualDisplay(mp, surface!!)
|
||||
}
|
||||
|
||||
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
||||
@@ -475,11 +523,28 @@ class MainService : Service() {
|
||||
}
|
||||
it.setCallback(cb)
|
||||
it.start()
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
)
|
||||
createOrSetVirtualDisplay(mp, surface!!)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
||||
try {
|
||||
virtualDisplay?.let {
|
||||
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||
it.setSurface(s)
|
||||
} ?: let {
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
s, null, null
|
||||
)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
||||
// This initiates a prompt dialog for the user to confirm screen projection.
|
||||
requestMediaProjection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +572,6 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createMediaCodec() {
|
||||
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
||||
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
||||
@@ -527,76 +591,6 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun startAudioRecorder() {
|
||||
checkAudioRecorder()
|
||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||
try {
|
||||
audioRecorder!!.startRecording()
|
||||
audioRecordStat = true
|
||||
thread {
|
||||
while (audioRecordStat) {
|
||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||
onAudioFrameUpdate(it)
|
||||
}
|
||||
}
|
||||
Log.d(logTag, "Exit audio thread")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||
}
|
||||
} else {
|
||||
Log.d(logTag, "startAudioRecorder fail")
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun checkAudioRecorder() {
|
||||
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
|
||||
return
|
||||
}
|
||||
// read f32 to byte , length * 4
|
||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||
AUDIO_SAMPLE_RATE,
|
||||
AUDIO_CHANNEL_MASK,
|
||||
AUDIO_ENCODING
|
||||
)
|
||||
if (minBufferSize == 0) {
|
||||
Log.d(logTag, "get min buffer size fail!")
|
||||
return
|
||||
}
|
||||
audioReader = AudioReader(minBufferSize, 4)
|
||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
mediaProjection?.let {
|
||||
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
audioRecorder = AudioRecord.Builder()
|
||||
.setAudioFormat(
|
||||
AudioFormat.Builder()
|
||||
.setEncoding(AUDIO_ENCODING)
|
||||
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||
)
|
||||
.setAudioPlaybackCaptureConfig(apcc)
|
||||
.setBufferSizeInBytes(minBufferSize).build()
|
||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.d(logTag, "createAudioRecorder fail")
|
||||
}
|
||||
|
||||
private fun initNotification() {
|
||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -681,6 +675,21 @@ class MainService : Service() {
|
||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||
}
|
||||
|
||||
private fun voiceCallRequestNotification(
|
||||
clientID: Int,
|
||||
type: String,
|
||||
username: String,
|
||||
peerId: String
|
||||
) {
|
||||
val notification = notificationBuilder
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setContentTitle(translate("Do you accept?"))
|
||||
.setContentText("$type:$username-$peerId")
|
||||
.build()
|
||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||
}
|
||||
|
||||
private fun getClientNotifyID(clientID: Int): Int {
|
||||
return clientID + NOTIFY_ID_OFFSET
|
||||
}
|
||||
|
||||
22
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
22
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
// ffi.kt
|
||||
|
||||
package ffi
|
||||
|
||||
import android.content.Context
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object FFI {
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
external fun translateLocale(localeName: String, input: String): String
|
||||
external fun refreshScreen()
|
||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
external fun setCodecInfo(info: String)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -139,4 +139,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -12,13 +12,13 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -55,6 +55,12 @@ var isMobile = isAndroid || isIOS;
|
||||
var version = '';
|
||||
int androidVersion = 0;
|
||||
|
||||
// Only used on Linux.
|
||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
|
||||
// https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
|
||||
// So we need to use this flag to enable/disable resizable.
|
||||
bool _linuxWindowResizable = true;
|
||||
|
||||
/// only available for Windows target
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
@@ -544,7 +550,8 @@ class MyTheme {
|
||||
Get.changeThemeMode(mode);
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||
if (mode == ThemeMode.system) {
|
||||
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
|
||||
await bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||
} else {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||
@@ -852,30 +859,10 @@ class OverlayDialogManager {
|
||||
final overlayState = _overlayKeyState.state;
|
||||
if (overlayState == null) return;
|
||||
|
||||
// compute overlay position
|
||||
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
||||
final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
|
||||
const double overlayW = 200;
|
||||
const double overlayH = 45;
|
||||
final left = (screenW - overlayW) / 2;
|
||||
final top = screenH - overlayH - 80;
|
||||
|
||||
final overlay = OverlayEntry(builder: (context) {
|
||||
final session = ffi ?? gFFI;
|
||||
return DraggableMobileActions(
|
||||
position: Offset(left, top),
|
||||
width: overlayW,
|
||||
height: overlayH,
|
||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
||||
onRecentPressed: () async {
|
||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
||||
},
|
||||
onHidePressed: () => hideMobileActionsOverlay(),
|
||||
);
|
||||
});
|
||||
final overlay = makeMobileActionsOverlayEntry(
|
||||
() => hideMobileActionsOverlay(),
|
||||
ffi: ffi,
|
||||
);
|
||||
overlayState.insert(overlay);
|
||||
_mobileActionsOverlayEntry = overlay;
|
||||
mobileActionsOverlayVisible.value = true;
|
||||
@@ -903,6 +890,45 @@ class OverlayDialogManager {
|
||||
}
|
||||
}
|
||||
|
||||
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||
final position = SimpleWrapper(Offset(0, 0));
|
||||
makeMobileActions(BuildContext context, double s) {
|
||||
final scale = s < 0.85 ? 0.85 : s;
|
||||
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 overlayH = 45;
|
||||
final left = (screenW - overlayW * scale) / 2;
|
||||
final top = screenH - (overlayH + 80) * scale;
|
||||
position.value = Offset(left, top);
|
||||
return DraggableMobileActions(
|
||||
scale: scale,
|
||||
position: position,
|
||||
width: overlayW,
|
||||
height: overlayH,
|
||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
||||
onRecentPressed: () async {
|
||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
||||
},
|
||||
onHidePressed: onHide,
|
||||
);
|
||||
}
|
||||
|
||||
return OverlayEntry(builder: (context) {
|
||||
if (isDesktop) {
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
return makeMobileActions(context, c.scale * 2.0);
|
||||
} else {
|
||||
return makeMobileActions(globalKey.currentContext!, 1.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
||||
final overlayState = globalKey.currentState?.overlay;
|
||||
if (overlayState == null) return;
|
||||
@@ -982,7 +1008,7 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||
} else if (!tabTapped &&
|
||||
onSubmit != null &&
|
||||
key.logicalKey == LogicalKeyboardKey.enter) {
|
||||
(key.logicalKey == LogicalKeyboardKey.enter || key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||
return KeyEventResult.handled;
|
||||
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
||||
@@ -1401,7 +1427,7 @@ bool option2bool(String option, String value) {
|
||||
res = value != "N";
|
||||
} else if (option.startsWith("allow-") ||
|
||||
option == "stop-service" ||
|
||||
option == "direct-server" ||
|
||||
option == kOptionDirectServer ||
|
||||
option == "stop-rendezvous-service" ||
|
||||
option == kOptionForceAlwaysRelay) {
|
||||
res = value == "Y";
|
||||
@@ -1415,13 +1441,13 @@ bool option2bool(String option, String value) {
|
||||
String bool2option(String option, bool b) {
|
||||
String res;
|
||||
if (option.startsWith('enable-')) {
|
||||
res = b ? '' : 'N';
|
||||
res = b ? defaultOptionYes : 'N';
|
||||
} else if (option.startsWith('allow-') ||
|
||||
option == "stop-service" ||
|
||||
option == "direct-server" ||
|
||||
option == kOptionDirectServer ||
|
||||
option == "stop-rendezvous-service" ||
|
||||
option == kOptionForceAlwaysRelay) {
|
||||
res = b ? 'Y' : '';
|
||||
res = b ? 'Y' : defaultOptionNo;
|
||||
} else {
|
||||
assert(false);
|
||||
res = b ? 'Y' : 'N';
|
||||
@@ -1493,6 +1519,12 @@ Widget getPlatformImage(String platform, {double size = 50}) {
|
||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||
}
|
||||
|
||||
class OffsetDevicePixelRatio {
|
||||
Offset offset;
|
||||
final double devicePixelRatio;
|
||||
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
|
||||
}
|
||||
|
||||
class LastWindowPosition {
|
||||
double? width;
|
||||
double? height;
|
||||
@@ -1554,16 +1586,13 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
late Offset position;
|
||||
late Size sz;
|
||||
late bool isMaximized;
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
|
||||
(isMacOS && stateGlobal.closeOnFullscreen == true);
|
||||
setFrameIfMaximized() {
|
||||
if (isMaximized) {
|
||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
position = Offset(
|
||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||
}
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||
setPreFrame() {
|
||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
position = Offset(
|
||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@@ -1572,26 +1601,33 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
// `await windowManager.isMaximized()` will always return true
|
||||
// if is not resizable. The reason is unknown.
|
||||
//
|
||||
// `windowManager.setResizable(!bind.isIncomingOnly());` in main.dart
|
||||
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
||||
isMaximized =
|
||||
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
||||
position = await windowManager.getPosition();
|
||||
sz = await windowManager.getSize();
|
||||
setFrameIfMaximized();
|
||||
if (isFullscreen || isMaximized) {
|
||||
setPreFrame();
|
||||
} else {
|
||||
position = await windowManager.getPosition();
|
||||
sz = await windowManager.getSize();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
isMaximized = await wc.isMaximized();
|
||||
final Rect frame;
|
||||
try {
|
||||
frame = await wc.getFrame();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to get frame of window $windowId, it may be hidden");
|
||||
return;
|
||||
if (isFullscreen || isMaximized) {
|
||||
setPreFrame();
|
||||
} else {
|
||||
final Rect frame;
|
||||
try {
|
||||
frame = await wc.getFrame();
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"Failed to get frame of window $windowId, it may be hidden");
|
||||
return;
|
||||
}
|
||||
position = frame.topLeft;
|
||||
sz = frame.size;
|
||||
}
|
||||
position = frame.topLeft;
|
||||
sz = frame.size;
|
||||
setFrameIfMaximized();
|
||||
break;
|
||||
}
|
||||
if (isWindows) {
|
||||
@@ -1625,7 +1661,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
|
||||
final remoteList = await DesktopMultiWindow.invokeMethod(
|
||||
windowId, kWindowEventGetRemoteList, null);
|
||||
getPeerPos(String peerId) {
|
||||
if (isMaximized) {
|
||||
if (isMaximized || isFullscreen) {
|
||||
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||
id: peerId, k: windowFramePrefix + windowType.name);
|
||||
var lpos = LastWindowPosition.loadFromString(peerPos);
|
||||
@@ -1682,8 +1718,15 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
|
||||
return Size(restoreWidth, restoreHeight);
|
||||
}
|
||||
|
||||
bool isPointInRect(Offset point, Rect rect) {
|
||||
return point.dx >= rect.left &&
|
||||
point.dx <= rect.right &&
|
||||
point.dy >= rect.top &&
|
||||
point.dy <= rect.bottom;
|
||||
}
|
||||
|
||||
/// return null means center
|
||||
Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||
double? left,
|
||||
double? top,
|
||||
double? width,
|
||||
@@ -1697,9 +1740,13 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
double? frameTop;
|
||||
double? frameRight;
|
||||
double? frameBottom;
|
||||
double devicePixelRatio = 1.0;
|
||||
|
||||
if (isDesktop || isWebDesktop) {
|
||||
for (final screen in await window_size.getScreenList()) {
|
||||
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
|
||||
devicePixelRatio = screen.scaleFactor;
|
||||
}
|
||||
frameLeft = frameLeft == null
|
||||
? screen.visibleFrame.left
|
||||
: min(screen.visibleFrame.left, frameLeft);
|
||||
@@ -1733,7 +1780,7 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
top < frameTop!) {
|
||||
return null;
|
||||
} else {
|
||||
return Offset(left, top);
|
||||
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1759,15 +1806,10 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||
// Though "open in tabs" is true and the new window restore peer position, it's ok.
|
||||
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
|
||||
// If the restore position is called by main window, and the peer id is not null
|
||||
// then we may need to get the position by reading the peer config.
|
||||
// Because the session may not be read at this time.
|
||||
if (desktopType == DesktopType.main) {
|
||||
pos = bind.mainGetPeerFlutterOptionSync(
|
||||
id: peerId, k: windowFramePrefix + type.name);
|
||||
} else {
|
||||
pos = await bind.sessionGetFlutterOptionByPeerId(
|
||||
id: peerId, k: windowFramePrefix + type.name);
|
||||
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||
id: peerId, k: windowFramePrefix + type.name);
|
||||
if (peerPos.isNotEmpty) {
|
||||
pos = peerPos;
|
||||
}
|
||||
isRemotePeerPos = pos != null;
|
||||
}
|
||||
@@ -1798,22 +1840,47 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
}
|
||||
|
||||
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
||||
final offset = await _adjustRestoreMainWindowOffset(
|
||||
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
|
||||
lpos.offsetWidth,
|
||||
lpos.offsetHeight,
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
debugPrint(
|
||||
"restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
|
||||
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
||||
|
||||
switch (type) {
|
||||
case WindowType.Main:
|
||||
// https://github.com/rustdesk/rustdesk/issues/8038
|
||||
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
|
||||
// So we need to adjust the offset by the scale factor.
|
||||
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
|
||||
if (isWindows) {
|
||||
double? curDevicePixelRatio;
|
||||
Offset curPos = await windowManager.getPosition();
|
||||
for (final screen in await window_size.getScreenList()) {
|
||||
if (isPointInRect(curPos, screen.visibleFrame)) {
|
||||
curDevicePixelRatio = screen.scaleFactor;
|
||||
}
|
||||
}
|
||||
if (curDevicePixelRatio != null &&
|
||||
curDevicePixelRatio != 0 &&
|
||||
offsetDevicePixelRatio != null) {
|
||||
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
|
||||
final scale =
|
||||
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
|
||||
offsetDevicePixelRatio.offset =
|
||||
offsetDevicePixelRatio.offset.scale(scale, scale);
|
||||
debugPrint(
|
||||
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
|
||||
}
|
||||
}
|
||||
}
|
||||
restorePos() async {
|
||||
if (offset == null) {
|
||||
if (offsetDevicePixelRatio == null) {
|
||||
await windowManager.center();
|
||||
} else {
|
||||
await windowManager.setPosition(offset);
|
||||
await windowManager.setPosition(offsetDevicePixelRatio.offset);
|
||||
}
|
||||
}
|
||||
if (lpos.isMaximized == true) {
|
||||
@@ -1831,19 +1898,27 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
default:
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
restoreFrame() async {
|
||||
if (offset == null) {
|
||||
if (offsetDevicePixelRatio == null) {
|
||||
await wc.center();
|
||||
} else {
|
||||
final frame =
|
||||
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
||||
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
|
||||
offsetDevicePixelRatio.offset.dy, size.width, size.height);
|
||||
await wc.setFrame(frame);
|
||||
}
|
||||
}
|
||||
if (lpos.isFullscreen == true) {
|
||||
await restoreFrame();
|
||||
if (!isMacOS) {
|
||||
await restoreFrame();
|
||||
}
|
||||
// An duration is needed to avoid the window being restored after fullscreen.
|
||||
Future.delayed(Duration(milliseconds: 300), () async {
|
||||
stateGlobal.setFullscreen(true);
|
||||
if (kWindowId == windowId) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
} else {
|
||||
// If is not current window, we need to send a fullscreen message to `windowId`
|
||||
DesktopMultiWindow.invokeMethod(
|
||||
windowId, kWindowEventSetFullscreen, 'true');
|
||||
}
|
||||
});
|
||||
} else if (lpos.isMaximized == true) {
|
||||
await restoreFrame();
|
||||
@@ -2309,7 +2384,13 @@ Future<void> onActiveWindowChanged() async {
|
||||
await windowManager.setPreventClose(false);
|
||||
await windowManager.close();
|
||||
if (isMacOS) {
|
||||
RdPlatformChannel.instance.terminate();
|
||||
// If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event.
|
||||
// But the app will not close.
|
||||
//
|
||||
// No idea why we need to delay here, `terminate()` itself is also an async function.
|
||||
Future.delayed(Duration.zero, () {
|
||||
RdPlatformChannel.instance.terminate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2743,11 +2824,6 @@ sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
|
||||
}
|
||||
}
|
||||
|
||||
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
|
||||
pi.isSupportMultiDisplay &&
|
||||
useTextureRender &&
|
||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
|
||||
|
||||
Future<List<Rect>> getScreenListWayland() async {
|
||||
final screenRectList = <Rect>[];
|
||||
if (isMainDesktopWindow) {
|
||||
@@ -2846,6 +2922,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||
}
|
||||
|
||||
setNewConnectWindowFrame(int windowId, String peerId, Rect? screenRect) async {
|
||||
if (screenRect == null) {
|
||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||
windowId: windowId, peerId: peerId);
|
||||
} else {
|
||||
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
}
|
||||
}
|
||||
|
||||
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
||||
if (screenRect == null) {
|
||||
return;
|
||||
@@ -2978,16 +3063,16 @@ Future<bool> setServerConfig(
|
||||
}
|
||||
// id
|
||||
if (config.idServer.isNotEmpty && errMsgs != null) {
|
||||
errMsgs[0].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.idServer));
|
||||
errMsgs[0].value = translate(await bind.mainTestIfValidServer(
|
||||
server: config.idServer, testWithProxy: true));
|
||||
if (errMsgs[0].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// relay
|
||||
if (config.relayServer.isNotEmpty && errMsgs != null) {
|
||||
errMsgs[1].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.relayServer));
|
||||
errMsgs[1].value = translate(await bind.mainTestIfValidServer(
|
||||
server: config.relayServer, testWithProxy: true));
|
||||
if (errMsgs[1].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
@@ -3106,6 +3191,27 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
|
||||
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
||||
}
|
||||
|
||||
Widget loadPowered(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://rustdesk.com'));
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(
|
||||
translate("powered_by_me"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
|
||||
)),
|
||||
),
|
||||
).marginOnly(top: 6);
|
||||
}
|
||||
|
||||
// max 300 x 60
|
||||
Widget loadLogo() {
|
||||
return FutureBuilder<ByteData>(
|
||||
@@ -3155,3 +3261,114 @@ bool isInHomePage() {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
return controller.state.value.selected == 0;
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarning() {
|
||||
return FutureBuilder<bool>(
|
||||
future: bind.isPresetPassword(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||
} else if (snapshot.hasData && snapshot.data == true) {
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize:
|
||||
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink(); // Show nothing if the Future completed with false or null
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
|
||||
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
||||
boxShadow() => isMainDesktopWindow
|
||||
? <BoxShadow>[
|
||||
if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
offset: Offset(
|
||||
0.0,
|
||||
stateGlobal.isFocused.isTrue
|
||||
? kFrameBoxShadowOffsetFocused
|
||||
: kFrameBoxShadowOffsetUnfocused),
|
||||
blurRadius: kFrameBoxShadowBlurRadius,
|
||||
),
|
||||
]
|
||||
: null;
|
||||
return Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isMainDesktopWindow
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.background,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: stateGlobal.windowBorderWidth.value,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
|
||||
? 0
|
||||
: kFrameBorderRadius,
|
||||
),
|
||||
boxShadow: boxShadow(),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
|
||||
? 0
|
||||
: kFrameClipRRectBorderRadius,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
|
||||
|
||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
||||
// See _linuxWindowResizable for more details.
|
||||
// So we use `setResizable()` instead of `windowManager.setResizable()`.
|
||||
//
|
||||
// We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
|
||||
setResizable(bool resizable) {
|
||||
if (isLinux) {
|
||||
_linuxWindowResizable = resizable;
|
||||
stateGlobal.refreshResizeEdgeSize();
|
||||
} else {
|
||||
windowManager.setResizable(resizable);
|
||||
}
|
||||
}
|
||||
|
||||
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||
|
||||
final isCustomClient = bind.isCustomClient();
|
||||
get defaultOptionLang => isCustomClient ? 'default' : '';
|
||||
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||
get defaultOptionNo => isCustomClient ? 'N' : '';
|
||||
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
||||
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
||||
|
||||
@@ -168,6 +168,29 @@ class ShowRemoteCursorState {
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
class ShowRemoteCursorLockState {
|
||||
static String tag(String id) => 'show_remote_cursor_lock_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
class KeyboardEnabledState {
|
||||
static String tag(String id) => 'keyboard_enabled_$id';
|
||||
|
||||
@@ -315,9 +338,10 @@ initSharedStates(String id) {
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
ShowRemoteCursorLockState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
FingerprintState.init(id);
|
||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
||||
PeerBoolOption.init(id, kOptionZoomCursor, () => false);
|
||||
UnreadChatCountState.init(id);
|
||||
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
||||
}
|
||||
@@ -327,10 +351,11 @@ removeSharedStates(String id) {
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
ShowRemoteCursorLockState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
FingerprintState.delete(id);
|
||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
||||
PeerBoolOption.delete(id, kOptionZoomCursor);
|
||||
UnreadChatCountState.delete(id);
|
||||
if (isMobile) ConnectionTypeState.delete(id);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
@@ -189,41 +190,60 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (!names.contains(gFFI.abModel.currentName.value)) {
|
||||
return Offstage();
|
||||
}
|
||||
// order: personal, divider, character order
|
||||
// https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
|
||||
final personalAddressBookName = gFFI.abModel.personalAddressBookName();
|
||||
bool contains = names.remove(personalAddressBookName);
|
||||
names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
|
||||
if (contains) {
|
||||
names.insert(0, personalAddressBookName);
|
||||
}
|
||||
final items = names
|
||||
.map((e) => DropdownMenuItem(
|
||||
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();
|
||||
var menuItemStyleData = MenuItemStyleData(height: 36);
|
||||
if (contains && items.length > 1) {
|
||||
items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
|
||||
List<double> customHeights = List.filled(items.length, 36);
|
||||
customHeights[1] = 4;
|
||||
menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
|
||||
}
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
|
||||
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
|
||||
return DropdownButton2<String>(
|
||||
value: gFFI.abModel.currentName.value,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
gFFI.abModel.setCurrentName(value);
|
||||
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
|
||||
}
|
||||
},
|
||||
onChanged: isOptFixed
|
||||
? null
|
||||
: (value) {
|
||||
if (value != null) {
|
||||
gFFI.abModel.setCurrentName(value);
|
||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||
}
|
||||
},
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
buttonStyleData: ButtonStyleData(height: 48),
|
||||
menuItemStyleData: MenuItemStyleData(height: 36),
|
||||
items: names
|
||||
.map((e) => DropdownMenuItem(
|
||||
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(),
|
||||
menuItemStyleData: menuItemStyleData,
|
||||
items: items,
|
||||
isExpanded: true,
|
||||
dropdownSearchData: DropdownSearchData(
|
||||
searchController: textEditingController,
|
||||
@@ -333,6 +353,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> syncMenuItem() {
|
||||
final isOptFixed = isOptionFixed(syncAbOption);
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Sync with recent sessions'),
|
||||
@@ -343,11 +364,13 @@ class _AddressBookState extends State<AddressBook> {
|
||||
gFFI.abModel.setShouldAsync(v);
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
enabled: (!isOptFixed).obs,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> sortMenuItem() {
|
||||
final isOptFixed = isOptionFixed(sortAbTagsOption);
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Sort tags'),
|
||||
@@ -355,15 +378,18 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return shouldSortTags();
|
||||
},
|
||||
setter: (bool v) async {
|
||||
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
|
||||
bind.mainSetLocalOption(
|
||||
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
|
||||
gFFI.abModel.sortTags.value = v;
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
enabled: (!isOptFixed).obs,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> filterMenuItem() {
|
||||
final isOptFixed = isOptionFixed(filterAbTagOption);
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Filter by intersection'),
|
||||
@@ -371,10 +397,12 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return filterAbTagByIntersection();
|
||||
},
|
||||
setter: (bool v) async {
|
||||
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
|
||||
bind.mainSetLocalOption(
|
||||
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
|
||||
gFFI.abModel.filterByIntersection.value = v;
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
enabled: (!isOptFixed).obs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -385,10 +413,11 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||
sortMenuItem(),
|
||||
syncMenuItem(),
|
||||
if (canWrite) syncMenuItem(),
|
||||
filterMenuItem(),
|
||||
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(),
|
||||
if (!gFFI.abModel.legacyMode.value)
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
MenuEntryDivider<String>(),
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
getEntry(translate("ab_web_console_tip"), () async {
|
||||
final url = await bind.mainGetApiServer();
|
||||
if (await canLaunchUrlString(url)) {
|
||||
|
||||
56
flutter/lib/common/widgets/audio_input.dart
Normal file
56
flutter/lib/common/widgets/audio_input.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
typedef AudioINputSetDevice = void Function(String device);
|
||||
typedef AudioInputBuilder = Widget Function(
|
||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||
|
||||
class AudioInput extends StatelessWidget {
|
||||
final AudioInputBuilder builder;
|
||||
|
||||
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
||||
|
||||
static String getDefault() {
|
||||
if (isWindows) return translate('System Sound');
|
||||
return '';
|
||||
}
|
||||
|
||||
static Future<String> getValue() async {
|
||||
String device = await bind.mainGetOption(key: 'audio-input');
|
||||
if (device.isNotEmpty) {
|
||||
return device;
|
||||
} else {
|
||||
return getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setDevice(String device) async {
|
||||
if (device == getDefault()) device = '';
|
||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||
}
|
||||
|
||||
static Future<Map<String, Object>> getDevicesInfo() async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (isWindows) {
|
||||
devices.insert(0, translate('System Sound'));
|
||||
}
|
||||
String current = await getValue();
|
||||
return {'devices': devices, 'current': current};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return futureBuilder(
|
||||
future: getDevicesInfo(),
|
||||
hasData: (data) {
|
||||
String currentDevice = data['current'];
|
||||
List<String> devices = data['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
return const Offstage();
|
||||
}
|
||||
return builder(devices, currentDevice, setDevice);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -177,11 +177,14 @@ void changeIdDialog() {
|
||||
}
|
||||
|
||||
void changeWhiteList({Function()? callback}) async {
|
||||
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(',');
|
||||
var newWhiteListField = newWhiteList.join('\n');
|
||||
final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
|
||||
var newWhiteListField = curWhiteList == defaultOptionWhitelist
|
||||
? ''
|
||||
: curWhiteList.split(',').join('\n');
|
||||
var controller = TextEditingController(text: newWhiteListField);
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("IP Whitelisting")),
|
||||
@@ -201,6 +204,7 @@ void changeWhiteList({Function()? callback}) async {
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
enabled: !isOptFixed,
|
||||
autofocus: true),
|
||||
),
|
||||
],
|
||||
@@ -214,12 +218,13 @@ void changeWhiteList({Function()? callback}) async {
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("Clear", onPressed: () async {
|
||||
await bind.mainSetOption(key: 'whitelist', value: '');
|
||||
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||
callback?.call();
|
||||
close();
|
||||
}, isOutline: true),
|
||||
dialogButton(
|
||||
if (!isOptFixed) dialogButton(
|
||||
"OK",
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
@@ -248,7 +253,11 @@ void changeWhiteList({Function()? callback}) async {
|
||||
}
|
||||
newWhiteList = ips.join(',');
|
||||
}
|
||||
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
|
||||
if (newWhiteList.trim().isEmpty) {
|
||||
newWhiteList = defaultOptionWhitelist;
|
||||
}
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: newWhiteList);
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
@@ -298,7 +307,7 @@ Future<String> changeDirectAccessPort(
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: 'direct-access-port', value: controller.text);
|
||||
key: kOptionDirectAccessPort, value: controller.text);
|
||||
close();
|
||||
}),
|
||||
],
|
||||
@@ -345,7 +354,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: 'auto-disconnect-timeout', value: controller.text);
|
||||
key: kOptionAutoDisconnectTimeout, value: controller.text);
|
||||
close();
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -45,7 +45,7 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
position: SimpleWrapper(position),
|
||||
width: width,
|
||||
height: height,
|
||||
chatModel: chatModel,
|
||||
@@ -166,15 +166,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// floating buttons of back/home/recent actions for android
|
||||
class DraggableMobileActions extends StatelessWidget {
|
||||
DraggableMobileActions(
|
||||
{this.position = Offset.zero,
|
||||
this.onBackPressed,
|
||||
{this.onBackPressed,
|
||||
this.onRecentPressed,
|
||||
this.onHomePressed,
|
||||
this.onHidePressed,
|
||||
required this.position,
|
||||
required this.width,
|
||||
required this.height});
|
||||
required this.height,
|
||||
required this.scale});
|
||||
|
||||
final Offset position;
|
||||
final double scale;
|
||||
final SimpleWrapper<Offset> position;
|
||||
final double width;
|
||||
final double height;
|
||||
final VoidCallback? onBackPressed;
|
||||
@@ -186,8 +188,8 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable(
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
width: scale * width,
|
||||
height: scale * height,
|
||||
builder: (_, onPanUpdate) {
|
||||
return GestureDetector(
|
||||
onPanUpdate: onPanUpdate,
|
||||
@@ -197,7 +199,8 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.accent.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.all(Radius.circular(15))),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15 * scale))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
@@ -205,17 +208,20 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
onPressed: onBackPressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.arrow_back)),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
iconSize: 24 * scale),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
onPressed: onHomePressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.home)),
|
||||
icon: const Icon(Icons.home),
|
||||
iconSize: 24 * scale),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
onPressed: onRecentPressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.more_horiz)),
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
iconSize: 24 * scale),
|
||||
const VerticalDivider(
|
||||
width: 0,
|
||||
thickness: 2,
|
||||
@@ -226,7 +232,8 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
onPressed: onHidePressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.keyboard_arrow_down)),
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
iconSize: 24 * scale),
|
||||
],
|
||||
),
|
||||
)));
|
||||
@@ -235,11 +242,11 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
}
|
||||
|
||||
class Draggable extends StatefulWidget {
|
||||
const Draggable(
|
||||
Draggable(
|
||||
{Key? key,
|
||||
this.checkKeyboard = false,
|
||||
this.checkScreenSize = false,
|
||||
this.position = Offset.zero,
|
||||
required this.position,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.chatModel,
|
||||
@@ -248,7 +255,7 @@ class Draggable extends StatefulWidget {
|
||||
|
||||
final bool checkKeyboard;
|
||||
final bool checkScreenSize;
|
||||
final Offset position;
|
||||
final SimpleWrapper<Offset> position;
|
||||
final double width;
|
||||
final double height;
|
||||
final ChatModel? chatModel;
|
||||
@@ -259,7 +266,6 @@ class Draggable extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DraggableState extends State<Draggable> {
|
||||
late Offset _position;
|
||||
late ChatModel? _chatModel;
|
||||
bool _keyboardVisible = false;
|
||||
double _saveHeight = 0;
|
||||
@@ -268,35 +274,36 @@ class _DraggableState extends State<Draggable> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_position = widget.position;
|
||||
_chatModel = widget.chatModel;
|
||||
}
|
||||
|
||||
get position => widget.position.value;
|
||||
|
||||
void onPanUpdate(DragUpdateDetails d) {
|
||||
final offset = d.delta;
|
||||
final size = MediaQuery.of(context).size;
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
|
||||
if (_position.dx + offset.dx + widget.width > size.width) {
|
||||
if (position.dx + offset.dx + widget.width > size.width) {
|
||||
x = size.width - widget.width;
|
||||
} else if (_position.dx + offset.dx < 0) {
|
||||
} else if (position.dx + offset.dx < 0) {
|
||||
x = 0;
|
||||
} else {
|
||||
x = _position.dx + offset.dx;
|
||||
x = position.dx + offset.dx;
|
||||
}
|
||||
|
||||
if (_position.dy + offset.dy + widget.height > size.height) {
|
||||
if (position.dy + offset.dy + widget.height > size.height) {
|
||||
y = size.height - widget.height;
|
||||
} else if (_position.dy + offset.dy < 0) {
|
||||
} else if (position.dy + offset.dy < 0) {
|
||||
y = 0;
|
||||
} else {
|
||||
y = _position.dy + offset.dy;
|
||||
y = position.dy + offset.dy;
|
||||
}
|
||||
setState(() {
|
||||
_position = Offset(x, y);
|
||||
widget.position.value = Offset(x, y);
|
||||
});
|
||||
_chatModel?.setChatWindowPosition(_position);
|
||||
_chatModel?.setChatWindowPosition(position);
|
||||
}
|
||||
|
||||
checkScreenSize() {}
|
||||
@@ -307,13 +314,13 @@ class _DraggableState extends State<Draggable> {
|
||||
|
||||
// save
|
||||
if (!_keyboardVisible && currentVisible) {
|
||||
_saveHeight = _position.dy;
|
||||
_saveHeight = position.dy;
|
||||
}
|
||||
|
||||
// reset
|
||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, _saveHeight);
|
||||
widget.position.value = Offset(position.dx, _saveHeight);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,10 +328,10 @@ class _DraggableState extends State<Draggable> {
|
||||
if (_keyboardVisible && currentVisible) {
|
||||
final sumHeight = bottomHeight + widget.height;
|
||||
final contextHeight = MediaQuery.of(context).size.height;
|
||||
if (sumHeight + _position.dy > contextHeight) {
|
||||
if (sumHeight + position.dy > contextHeight) {
|
||||
final y = contextHeight - sumHeight;
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, y);
|
||||
widget.position.value = Offset(position.dx, y);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -343,8 +350,8 @@ class _DraggableState extends State<Draggable> {
|
||||
}
|
||||
return Stack(children: [
|
||||
Positioned(
|
||||
top: _position.dy,
|
||||
left: _position.dx,
|
||||
top: position.dy,
|
||||
left: position.dx,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: widget.builder(context, onPanUpdate))
|
||||
|
||||
@@ -74,9 +74,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
];
|
||||
RelativeRect? mobileTabContextMenuPos;
|
||||
|
||||
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
||||
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||
if (uiType != '') {
|
||||
peerCardUiType.value = int.parse(uiType) == 0
|
||||
? PeerUiType.grid
|
||||
@@ -85,7 +87,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
: PeerUiType.list;
|
||||
}
|
||||
hideAbTagsPanel.value =
|
||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -173,11 +175,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
child: Icon(model.tabIcon(t), color: color)
|
||||
.paddingSymmetric(horizontal: 4),
|
||||
).paddingSymmetric(horizontal: 4),
|
||||
onTap: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: PeerTabModel.kPeerTabIndex, v: t.toString());
|
||||
},
|
||||
onTap: isOptionFixed(kOptionPeerTabIndex)
|
||||
? null
|
||||
: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: kOptionPeerTabIndex, v: t.toString());
|
||||
},
|
||||
onHover: (value) => hover.value = value,
|
||||
),
|
||||
)));
|
||||
@@ -265,17 +269,22 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
if (!model.isEnabled[i]) continue;
|
||||
items.add(PopupMenuItem(
|
||||
height: kMinInteractiveDimension * 0.8,
|
||||
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||
onTap: isOptVisiableFixed
|
||||
? null
|
||||
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||
enabled: !isOptVisiableFixed,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: model.isVisibleEnabled[i],
|
||||
onChanged: (_) {
|
||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}),
|
||||
onChanged: isOptVisiableFixed
|
||||
? null
|
||||
: (_) {
|
||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}),
|
||||
Expanded(child: Text(model.tabTooltip(i))),
|
||||
],
|
||||
),
|
||||
@@ -332,8 +341,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
currentValue: model.isVisibleEnabled[tabIndex],
|
||||
setter: (show) async {
|
||||
model.setTabVisible(tabIndex, show);
|
||||
cancelFunc();
|
||||
}));
|
||||
// Do not hide the current menu (checkbox)
|
||||
// cancelFunc();
|
||||
},
|
||||
enabled: (!isOptVisiableFixed).obs));
|
||||
}
|
||||
return mod_menu.PopupMenu(
|
||||
items: menu
|
||||
@@ -529,7 +540,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget _toggleTags() {
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Toggle tags'),
|
||||
toolTip: translate('Toggle Tags'),
|
||||
hoverableWhenfalse: hideAbTagsPanel,
|
||||
child: Icon(
|
||||
Icons.tag_rounded,
|
||||
@@ -537,7 +548,8 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
),
|
||||
onTap: () async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
|
||||
key: kOptionHideAbTagsPanel,
|
||||
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
|
||||
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
||||
});
|
||||
}
|
||||
@@ -799,16 +811,22 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
style: style),
|
||||
e,
|
||||
peerCardUiType.value,
|
||||
dense: true, (PeerUiType? v) async {
|
||||
if (v != null) {
|
||||
peerCardUiType.value = v;
|
||||
setState(() {});
|
||||
await bind.setLocalFlutterOption(
|
||||
k: "peer-card-ui-type",
|
||||
v: peerCardUiType.value.index.toString(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
dense: true,
|
||||
isOptionFixed(kOptionPeerCardUiType)
|
||||
? null
|
||||
: (PeerUiType? v) async {
|
||||
if (v != null) {
|
||||
peerCardUiType.value = v;
|
||||
setState(() {});
|
||||
await bind.setLocalFlutterOption(
|
||||
k: kOptionPeerCardUiType,
|
||||
v: peerCardUiType.value.index.toString(),
|
||||
);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
))));
|
||||
}
|
||||
@@ -852,7 +870,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: "peer-sorting",
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
@@ -882,7 +900,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
if (v != null) {
|
||||
peerSort.value = v;
|
||||
await bind.setLocalFlutterOption(
|
||||
k: "peer-sorting",
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import 'dart:collection';
|
||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/peer_model.dart';
|
||||
@@ -45,7 +45,7 @@ class LoadEvent {
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
/// for peer sort, global obs value
|
||||
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
|
||||
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||
|
||||
// list for listener
|
||||
final obslist = [peerSearchText, peerSort].obs;
|
||||
@@ -86,17 +86,6 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
|
||||
late final mobileWidth = () {
|
||||
const minWidth = 320.0;
|
||||
final windowWidth = MediaQuery.of(context).size.width;
|
||||
var width = windowWidth - 2 * space;
|
||||
if (windowWidth > minWidth + 2 * space) {
|
||||
final n = (windowWidth / (minWidth + 2 * space)).floor();
|
||||
width = windowWidth / n - 2 * space;
|
||||
}
|
||||
return width;
|
||||
}();
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
_PeersViewState() {
|
||||
@@ -189,61 +178,60 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
child: widget.peerCardBuilder(peer),
|
||||
);
|
||||
final windowWidth = MediaQuery.of(context).size.width;
|
||||
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
|
||||
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
|
||||
//
|
||||
// No need to listen the currentTab change event.
|
||||
// Because the currentTab change event will trigger the peers change event,
|
||||
// and the peers change event will trigger _buildPeersView().
|
||||
final currentTab =
|
||||
Provider.of<PeerTabModel>(context, listen: false).currentTab;
|
||||
final hideAbTagsPanel =
|
||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
return (isDesktop || isWebDesktop)
|
||||
? Obx(
|
||||
() => SizedBox(
|
||||
width: peerCardUiType.value != PeerUiType.list
|
||||
? 220
|
||||
: currentTab == PeerTabIndex.group.index ||
|
||||
(currentTab == PeerTabIndex.ab.index &&
|
||||
!hideAbTagsPanel)
|
||||
? windowWidth - 390
|
||||
: windowWidth - 227,
|
||||
height: peerCardUiType.value == PeerUiType.grid
|
||||
? 140
|
||||
: peerCardUiType.value != PeerUiType.list
|
||||
? 42
|
||||
: 45,
|
||||
child: visibilityChild,
|
||||
),
|
||||
)
|
||||
: SizedBox(width: mobileWidth, child: visibilityChild);
|
||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? Container(height: 45, child: visibilityChild)
|
||||
: peerCardUiType.value == PeerUiType.grid
|
||||
? SizedBox(
|
||||
width: 220, height: 140, child: visibilityChild)
|
||||
: SizedBox(
|
||||
width: 220, height: 42, child: visibilityChild))
|
||||
: Container(child: visibilityChild);
|
||||
}
|
||||
|
||||
final Widget child;
|
||||
if (isMobile) {
|
||||
child = DynamicGridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
||||
child = ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
);
|
||||
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
if (updateEvent == UpdateEvent.load) {
|
||||
@@ -314,7 +302,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
if (!PeerSortType.values.contains(sortedBy)) {
|
||||
sortedBy = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: "peer-sorting",
|
||||
k: kOptionPeerSorting,
|
||||
v: sortedBy,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,9 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
if (isDesktop) {
|
||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
@@ -241,13 +244,16 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
customImageQualityWidget(
|
||||
{required double initQuality,
|
||||
required double initFps,
|
||||
required Function(double) setQuality,
|
||||
required Function(double) setFps,
|
||||
required Function(double)? setQuality,
|
||||
required Function(double)? setFps,
|
||||
required bool showFps,
|
||||
required bool showMoreQuality}) {
|
||||
if (initQuality < kMinQuality ||
|
||||
@@ -27,16 +26,12 @@ customImageQualityWidget(
|
||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setQuality(v);
|
||||
},
|
||||
onChanged: setQuality,
|
||||
initialValue: qualityValue.value,
|
||||
);
|
||||
final debouncerFps = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setFps(v);
|
||||
},
|
||||
onChanged: setFps,
|
||||
initialValue: fpsValue.value,
|
||||
);
|
||||
|
||||
@@ -62,10 +57,12 @@ customImageQualityWidget(
|
||||
divisions: moreQualityChecked.value
|
||||
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
||||
: ((kMaxQuality - kMinQuality) / 5).round(),
|
||||
onChanged: (double value) async {
|
||||
qualityValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
onChanged: setQuality == null
|
||||
? null
|
||||
: (double value) async {
|
||||
qualityValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -124,10 +121,12 @@ customImageQualityWidget(
|
||||
min: kMinFps,
|
||||
max: kMaxFps,
|
||||
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
onChanged: setFps == null
|
||||
? null
|
||||
: (double value) async {
|
||||
fpsValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -152,21 +151,29 @@ customImageQualitySetting() {
|
||||
final qualityKey = 'custom_image_quality';
|
||||
final fpsKey = 'custom-fps';
|
||||
|
||||
var initQuality =
|
||||
final initQuality =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||
kDefaultQuality);
|
||||
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||
kDefaultFps);
|
||||
final isQuanlityFixed = isOptionFixed(qualityKey);
|
||||
final initFps =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||
kDefaultFps);
|
||||
final isFpsFixed = isOptionFixed(fpsKey);
|
||||
|
||||
return customImageQualityWidget(
|
||||
initQuality: initQuality,
|
||||
initFps: initFps,
|
||||
setQuality: (v) {
|
||||
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
|
||||
},
|
||||
setFps: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
setQuality: isQuanlityFixed
|
||||
? null
|
||||
: (v) {
|
||||
bind.mainSetUserDefaultOption(
|
||||
key: qualityKey, value: v.toString());
|
||||
},
|
||||
setFps: isFpsFixed
|
||||
? null
|
||||
: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
showFps: true,
|
||||
showMoreQuality: true);
|
||||
}
|
||||
@@ -208,27 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
|
||||
List<(String, String)> otherDefaultSettings() {
|
||||
List<(String, String)> v = [
|
||||
('View Mode', 'view_only'),
|
||||
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
|
||||
('Show remote cursor', 'show_remote_cursor'),
|
||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
|
||||
('Show quality monitor', 'show_quality_monitor'),
|
||||
('Mute', 'disable_audio'),
|
||||
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
|
||||
('Disable clipboard', 'disable_clipboard'),
|
||||
('Lock after session end', 'lock_after_session_end'),
|
||||
('Privacy mode', 'privacy_mode'),
|
||||
if (isMobile) ('Touch mode', 'touch-mode'),
|
||||
('True color (4:4:4)', 'i444'),
|
||||
('View Mode', kOptionViewOnly),
|
||||
if ((isDesktop || isWebDesktop))
|
||||
('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if ((isDesktop || isWebDesktop))
|
||||
('Collapse toolbar', kOptionCollapseToolbar),
|
||||
('Show remote cursor', kOptionShowRemoteCursor),
|
||||
('Follow remote cursor', kOptionFollowRemoteCursor),
|
||||
('Follow remote window focus', kOptionFollowRemoteWindow),
|
||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
|
||||
('Show quality monitor', kOptionShowQualityMonitor),
|
||||
('Mute', kOptionDisableAudio),
|
||||
if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
|
||||
('Disable clipboard', kOptionDisableClipboard),
|
||||
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||
('Privacy mode', kOptionPrivacyMode),
|
||||
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||
('True color (4:4:4)', kOptionI444),
|
||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
||||
if (isDesktop && useTextureRender)
|
||||
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||
if (isDesktop && bind.mainGetUseTextureRender())
|
||||
(
|
||||
'Show displays as individual windows',
|
||||
kKeyShowDisplaysAsIndividualWindows
|
||||
),
|
||||
if (isDesktop && useTextureRender)
|
||||
if (isDesktop && bind.mainGetUseTextureRender())
|
||||
(
|
||||
'Use all my displays for the remote session',
|
||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
@@ -116,7 +115,10 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// paste
|
||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
if (isMobile &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
perms['keyboard'] != false &&
|
||||
perms['clipboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Paste')),
|
||||
onPressed: () async {
|
||||
@@ -327,7 +329,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
final alternativeCodecs =
|
||||
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
||||
final groupValue = await bind.sessionGetOption(
|
||||
sessionId: sessionId, arg: 'codec-preference') ??
|
||||
sessionId: sessionId, arg: kOptionCodecPreference) ??
|
||||
'';
|
||||
final List<bool> codecs = [];
|
||||
try {
|
||||
@@ -349,20 +351,25 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionPeerOption(
|
||||
sessionId: sessionId, name: 'codec-preference', value: value);
|
||||
sessionId: sessionId, name: kOptionCodecPreference, value: value);
|
||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||
}
|
||||
|
||||
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||
return TRadioMenu<String>(
|
||||
child: Text(translate(label)),
|
||||
child: Text(label),
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? onChanged : null);
|
||||
}
|
||||
|
||||
var autoLabel = translate('Auto');
|
||||
if (groupValue == 'auto' &&
|
||||
ffi.qualityMonitorModel.data.codecFormat != null) {
|
||||
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||
}
|
||||
return [
|
||||
radio('Auto', 'auto', true),
|
||||
radio(autoLabel, 'auto', true),
|
||||
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||
radio('VP9', 'vp9', true),
|
||||
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
||||
@@ -371,12 +378,11 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
Future<List<TToggleMenu>> toolbarCursor(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
List<TToggleMenu> v = [];
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
// show remote cursor
|
||||
@@ -384,12 +390,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland) {
|
||||
final state = ShowRemoteCursorState.find(id);
|
||||
final lockState = ShowRemoteCursorLockState.find(id);
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'show-remote-cursor';
|
||||
if (pi.currentDisplay == kAllDisplayValue ||
|
||||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
lockState.value = false;
|
||||
}
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Show remote cursor')),
|
||||
value: state.value,
|
||||
onChanged: enabled
|
||||
onChanged: enabled && !lockState.value
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
@@ -399,6 +410,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
}
|
||||
: null));
|
||||
}
|
||||
// follow remote cursor
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland &&
|
||||
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
pi.displays.length > 1 &&
|
||||
pi.currentDisplay != kAllDisplayValue &&
|
||||
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
final option = 'follow-remote-cursor';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
final showCursorOption = 'show-remote-cursor';
|
||||
final showCursorState = ShowRemoteCursorState.find(id);
|
||||
final showCursorLockState = ShowRemoteCursorLockState.find(id);
|
||||
final showCursorEnabled = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
showCursorLockState.value = value;
|
||||
if (value && !showCursorEnabled) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: showCursorOption);
|
||||
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
}
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote cursor')),
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
showCursorLockState.value = value;
|
||||
if (!showCursorEnabled) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: showCursorOption);
|
||||
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
}
|
||||
}));
|
||||
}
|
||||
// follow remote window focus
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland &&
|
||||
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
pi.displays.length > 1 &&
|
||||
pi.currentDisplay != kAllDisplayValue &&
|
||||
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
final option = 'follow-remote-window';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote window focus')),
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
}));
|
||||
}
|
||||
// zoom cursor
|
||||
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
|
||||
if (!isMobile &&
|
||||
@@ -417,6 +489,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
},
|
||||
));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
List<TToggleMenu> v = [];
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
// show quality monitor
|
||||
final option = 'show-quality-monitor';
|
||||
v.add(TToggleMenu(
|
||||
@@ -441,20 +524,27 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Mute'))));
|
||||
}
|
||||
// file copy and paste
|
||||
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
|
||||
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
|
||||
isWindows &&
|
||||
pi.platform == kPeerPlatformWindows;
|
||||
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
|
||||
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
|
||||
if (ffiModel.keyboard &&
|
||||
perms['file'] != false &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
|
||||
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'enable-file-transfer';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
final value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Enable file copy and paste'))));
|
||||
@@ -477,7 +567,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Disable clipboard'))));
|
||||
}
|
||||
// lock after session end
|
||||
if (ffiModel.keyboard) {
|
||||
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'lock-after-session-end';
|
||||
final value =
|
||||
@@ -493,7 +583,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Lock after session end'))));
|
||||
}
|
||||
|
||||
if (useTextureRender &&
|
||||
if (bind.mainGetUseTextureRender() &&
|
||||
pi.isSupportMultiDisplay &&
|
||||
PrivacyModeState.find(id).isEmpty &&
|
||||
pi.displaysCount.value > 1 &&
|
||||
@@ -512,7 +602,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
}
|
||||
|
||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
|
||||
if (bind.mainGetUseTextureRender() &&
|
||||
pi.isSupportMultiDisplay &&
|
||||
isMultiScreens) {
|
||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
|
||||
@@ -19,7 +19,11 @@ const kKeyTranslateMode = 'translate';
|
||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||
const String kPlatformAdditionsHeadless = "headless";
|
||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
|
||||
const String kPlatformAdditionsIddImpl = "idd_impl";
|
||||
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
||||
"rustdesk_virtual_displays";
|
||||
const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||
"amyuni_virtual_displays";
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||
"supported_privacy_mode_impl";
|
||||
@@ -58,16 +62,79 @@ const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
||||
const String kWindowEventSetFullscreen = "set_fullscreen";
|
||||
|
||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionViewStyle = "view_style";
|
||||
const String kOptionScrollStyle = "scroll_style";
|
||||
const String kOptionImageQuality = "image_quality";
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionTextureRender = "use-texture-render";
|
||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||
const String kOptionForceAlwaysRelay = "force-always-relay";
|
||||
const String kOptionViewOnly = "view-only";
|
||||
const String kOptionViewOnly = "view_only";
|
||||
const String kOptionEnableLanDiscovery = "enable-lan-discovery";
|
||||
const String kOptionWhitelist = "whitelist";
|
||||
const String kOptionEnableAbr = "enable-abr";
|
||||
const String kOptionEnableRecordSession = "enable-record-session";
|
||||
const String kOptionDirectServer = "direct-server";
|
||||
const String kOptionDirectAccessPort = "direct-access-port";
|
||||
const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
||||
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||
const String kOptionAccessMode = "access-mode";
|
||||
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||
// "Settings -> Security -> Permissions"
|
||||
const String kOptionEnableClipboard = "enable-clipboard";
|
||||
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||
const String kOptionEnableAudio = "enable-audio";
|
||||
const String kOptionEnableTunnel = "enable-tunnel";
|
||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||
const String kOptionEnableBlockInput = "enable-block-input";
|
||||
const String kOptionAllowRemoteConfigModification =
|
||||
"allow-remote-config-modification";
|
||||
const String kOptionVerificationMethod = "verification-method";
|
||||
const String kOptionApproveMode = "approve-mode";
|
||||
const String kOptionCollapseToolbar = "collapse_toolbar";
|
||||
const String kOptionShowRemoteCursor = "show_remote_cursor";
|
||||
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
|
||||
const String kOptionFollowRemoteWindow = "follow_remote_window";
|
||||
const String kOptionZoomCursor = "zoom-cursor";
|
||||
const String kOptionShowQualityMonitor = "show_quality_monitor";
|
||||
const String kOptionDisableAudio = "disable_audio";
|
||||
const String kOptionEnableFileCopyPaste = "enable-file-copy-paste";
|
||||
// "Settings -> Display -> Other default options"
|
||||
const String kOptionDisableClipboard = "disable_clipboard";
|
||||
const String kOptionLockAfterSessionEnd = "lock_after_session_end";
|
||||
const String kOptionPrivacyMode = "privacy_mode";
|
||||
const String kOptionTouchMode = "touch-mode";
|
||||
const String kOptionI444 = "i444";
|
||||
const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
|
||||
const String kOptionCodecPreference = "codec-preference";
|
||||
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
|
||||
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
|
||||
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
|
||||
const String kOptionRemoteMenubarState = "remoteMenubarState";
|
||||
const String kOptionPeerSorting = "peer-sorting";
|
||||
const String kOptionPeerTabIndex = "peer-tab-index";
|
||||
const String kOptionPeerTabOrder = "peer-tab-order";
|
||||
const String kOptionPeerTabVisible = "peer-tab-visible";
|
||||
const String kOptionPeerCardUiType = "peer-card-ui-type";
|
||||
const String kOptionCurrentAbName = "current-ab-name";
|
||||
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
||||
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||
|
||||
const String kOptionToggleViewOnly = "view-only";
|
||||
|
||||
const String kUrlActionClose = "close";
|
||||
|
||||
@@ -87,10 +154,13 @@ const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
||||
|
||||
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
|
||||
|
||||
// the executable name of the portable version
|
||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||
|
||||
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
||||
const Color kColorCanvas = Colors.black;
|
||||
|
||||
const int kMobileDefaultDisplayWidth = 720;
|
||||
const int kMobileDefaultDisplayHeight = 1280;
|
||||
@@ -121,12 +191,11 @@ double kNewWindowOffset = isWindows
|
||||
? 30.0
|
||||
: 50.0;
|
||||
|
||||
EdgeInsets get kDragToResizeAreaPadding =>
|
||||
!kUseCompatibleUiMode && isLinux
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||
const int $nbsp = 0x00A0;
|
||||
|
||||
@@ -150,9 +219,15 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
var kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||
final kWindowBorderWidth = 1.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
const kFrameBorderRadius = 12.0;
|
||||
const kFrameClipRRectBorderRadius = 12.0;
|
||||
const kFrameBoxShadowBlurRadius = 32.0;
|
||||
const kFrameBoxShadowOffsetFocused = 4.0;
|
||||
const kFrameBoxShadowOffsetUnfocused = 2.0;
|
||||
|
||||
const kInvalidValueStr = 'InvalidValueStr';
|
||||
|
||||
@@ -198,12 +273,6 @@ const kRemoteImageQualityLow = 'low';
|
||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||
const kRemoteImageQualityCustom = 'custom';
|
||||
|
||||
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
|
||||
const kRemoteAudioGuestToHost = 'guest-to-host';
|
||||
|
||||
/// [kRemoteAudioDualWay] dual-way audio mode(default).
|
||||
const kRemoteAudioDualWay = 'dual-way';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
// ================================ mobile ================================
|
||||
@@ -222,6 +291,7 @@ const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
||||
const kRequestIgnoreBatteryOptimizations =
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
|
||||
|
||||
/// Android channel invoke type key
|
||||
class AndroidChannel {
|
||||
|
||||
@@ -262,7 +262,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
void onWindowLeaveFullScreen() {
|
||||
// Restore edge border to default edge size.
|
||||
stateGlobal.resizeEdgeSize.value =
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -69,44 +69,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarning() {
|
||||
return FutureBuilder<bool>(
|
||||
future: bind.isPresetPassword(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||
} else if (snapshot.hasData && snapshot.data == true) {
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink(); // Show nothing if the Future completed with false or null
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLeftPane(BuildContext context) {
|
||||
final isIncomingOnly = bind.isIncomingOnly();
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
@@ -115,22 +77,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
if (bind.isCustomClient())
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://rustdesk.com'));
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(
|
||||
translate("powered_by_me"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 9, decoration: TextDecoration.underline),
|
||||
)),
|
||||
),
|
||||
).marginOnly(top: 6),
|
||||
child: loadPowered(context),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
@@ -206,7 +153,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(0),
|
||||
onTap: () => {
|
||||
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||
{
|
||||
DesktopSettingPage.switch2page(
|
||||
DesktopSettingPage.tabKeys[0])
|
||||
}
|
||||
},
|
||||
onHover: (value) => _editHover.value = value,
|
||||
),
|
||||
),
|
||||
@@ -298,19 +251,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
RxBool hover = false.obs;
|
||||
return InkWell(
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
)),
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onHover: (value) => hover.value = value,
|
||||
@@ -371,33 +325,36 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
AnimatedRotationWidget(
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)))),
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
))),
|
||||
),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
if (!bind.isDisableSettings())
|
||||
InkWell(
|
||||
child: Obx(
|
||||
() => Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)).marginOnly(right: 8, top: 4),
|
||||
child: Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(0),
|
||||
onTap: () => DesktopSettingPage.switch2page(
|
||||
SettingsTabKey.safety),
|
||||
onHover: (value) => editHover.value = value,
|
||||
),
|
||||
],
|
||||
@@ -701,10 +658,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (updateUrl.isNotEmpty) setState(() {});
|
||||
});
|
||||
if (!bind.isCustomClient()) {
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (updateUrl.isNotEmpty) setState(() {});
|
||||
});
|
||||
}
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||
await gFFI.serverModel.fetchID();
|
||||
final error = await bind.mainGetError();
|
||||
@@ -824,6 +783,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
await rustDeskWinManager.openMonitorSession(
|
||||
windowId, peerId, display, displayCount, screenRect);
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final windowId = int.tryParse(call.arguments);
|
||||
if (windowId != null) {
|
||||
return jsonEncode(
|
||||
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
|
||||
}
|
||||
}
|
||||
});
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,8 @@ class DesktopTabPage extends StatefulWidget {
|
||||
@override
|
||||
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||
|
||||
static void onAddSetting({int initialPage = 0}) {
|
||||
static void onAddSetting(
|
||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||
try {
|
||||
DesktopTabController tabController = Get.find();
|
||||
tabController.add(TabInfo(
|
||||
@@ -27,7 +28,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
unselectedIcon: Icons.build_outlined,
|
||||
page: DesktopSettingPage(
|
||||
key: const ValueKey(kTabLabelSettingPage),
|
||||
initialPage: initialPage,
|
||||
initialTabkey: initialPage,
|
||||
)));
|
||||
} catch (e) {
|
||||
debugPrintStack(label: '$e');
|
||||
@@ -56,10 +57,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
tabController.onSelected = (key) {
|
||||
if (key == kTabLabelHomePage) {
|
||||
windowManager.setSize(getIncomingOnlyHomeSize());
|
||||
windowManager.setResizable(false);
|
||||
setResizable(false);
|
||||
} else {
|
||||
windowManager.setSize(getIncomingOnlySettingsSize());
|
||||
windowManager.setResizable(true);
|
||||
setResizable(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,18 +91,21 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton().paddingOnly(left: 10),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
);
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
@@ -128,9 +131,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
tabController.clear();
|
||||
return true;
|
||||
} else {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
final bool res;
|
||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
|
||||
@@ -97,21 +97,30 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton(),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
),
|
||||
);
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(
|
||||
context,
|
||||
Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: child),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@@ -15,7 +16,6 @@ import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/toolbar.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/desktop_render_texture.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
import '../../utils/image.dart';
|
||||
@@ -93,7 +93,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
void _initStates(String id) {
|
||||
initSharedStates(id);
|
||||
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
||||
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||
@@ -135,7 +135,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'zoom-cursor');
|
||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||
DesktopMultiWindow.addListener(this);
|
||||
// if (!_isCustomCursorInited) {
|
||||
// customCursorController.registerNeedUpdateCursorCallback(
|
||||
@@ -165,6 +165,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// and let OS to handle events instead.
|
||||
_rawKeyFocusNode.unfocus();
|
||||
}
|
||||
stateGlobal.isFocused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -174,6 +175,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
stateGlobal.isFocused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -206,6 +208,22 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() {
|
||||
super.onWindowEnterFullScreen();
|
||||
if (isMacOS) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
super.onWindowLeaveFullScreen();
|
||||
if (isMacOS) {
|
||||
stateGlobal.setFullscreen(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
||||
@@ -218,6 +236,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.imageModel.disposeImage();
|
||||
_ffi.cursorModel.disposeImages();
|
||||
_ffi.recordingModel.onClose();
|
||||
_rawKeyFocusNode.dispose();
|
||||
await _ffi.close(closeSession: closeSession);
|
||||
@@ -258,7 +278,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.black,
|
||||
color: kColorCanvas,
|
||||
child: RawKeyFocusScope(
|
||||
focusNode: _rawKeyFocusNode,
|
||||
onFocusChange: (bool imageFocused) {
|
||||
@@ -287,8 +307,30 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||
? emptyOverlay()
|
||||
: () {
|
||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
||||
return Offstage();
|
||||
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||
return Offstage();
|
||||
} else {
|
||||
if (_ffi.connType == ConnType.defaultConn &&
|
||||
_ffi.ffiModel.permissions['keyboard'] != false) {
|
||||
Timer(
|
||||
Duration(milliseconds: 10),
|
||||
() => _ffi.dialogManager
|
||||
.mobileActionsOverlayVisible.value = true);
|
||||
}
|
||||
return Obx(() => Offstage(
|
||||
offstage: _ffi.dialogManager
|
||||
.mobileActionsOverlayVisible.isFalse,
|
||||
child: Overlay(initialEntries: [
|
||||
makeMobileActionsOverlayEntry(
|
||||
() => _ffi
|
||||
.dialogManager
|
||||
.mobileActionsOverlayVisible
|
||||
.value = false,
|
||||
ffi: _ffi,
|
||||
)
|
||||
]),
|
||||
));
|
||||
}
|
||||
}(),
|
||||
// Use Overlay to enable rebuild every time on menu button click.
|
||||
_ffi.ffiModel.pi.isSet.isTrue
|
||||
@@ -435,9 +477,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}, onExit: (evt) {
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
final c = Provider.of<CanvasModel>(context, listen: false);
|
||||
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||
return Obx(
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
@@ -572,12 +613,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
onHover: (evt) {},
|
||||
child: child);
|
||||
});
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
final paintWidget = useTextureRender
|
||||
final paintWidget = m.useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||
@@ -598,7 +638,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
));
|
||||
} else {
|
||||
if (c.size.width > 0 && c.size.height > 0) {
|
||||
final paintWidget = useTextureRender
|
||||
final paintWidget = m.useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c,
|
||||
s,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
@@ -107,107 +108,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final switchUuid = args['switch_uuid'];
|
||||
final sessionId = args['session_id'];
|
||||
final tabWindowId = args['tab_window_id'];
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
windowOnTop(windowId());
|
||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
if (tabController.length == 0) {
|
||||
// Show the hidden window.
|
||||
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
// Reset the state
|
||||
stateGlobal.closeOnFullscreen = null;
|
||||
}
|
||||
ConnectionTypeState.init(id);
|
||||
_toolbarState.setShow(
|
||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: args['forceRelay'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final display = args['display'];
|
||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventGetRemoteList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => e.key)
|
||||
.toList()
|
||||
.join(',');
|
||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||
.toList()
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == id)
|
||||
.page as RemotePage;
|
||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get cached session data: $e');
|
||||
}
|
||||
if (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
});
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
if (!_isScreenRectSet) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
@@ -230,103 +131,103 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton().paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
fingerprint = 'N/A';
|
||||
}
|
||||
if (fingerprint.length > 5 * 8) {
|
||||
var first = fingerprint.substring(0, 39);
|
||||
var second = fingerprint.substring(40);
|
||||
msgFingerprint += '$first\n$second';
|
||||
} else {
|
||||
msgFingerprint += fingerprint;
|
||||
}
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
fingerprint = 'N/A';
|
||||
}
|
||||
if (fingerprint.length > 5 * 8) {
|
||||
var first = fingerprint.substring(0, 39);
|
||||
var second = fingerprint.substring(40);
|
||||
msgFingerprint += '$first\n$second';
|
||||
} else {
|
||||
msgFingerprint += fingerprint;
|
||||
}
|
||||
|
||||
final tab = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||
.marginOnly(left: 4),
|
||||
],
|
||||
);
|
||||
final tab = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||
.marginOnly(left: 4),
|
||||
],
|
||||
);
|
||||
|
||||
return Listener(
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
return;
|
||||
}
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == key)
|
||||
.page as RemotePage;
|
||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
|
||||
e.buttons == 2) {
|
||||
showRightMenu(
|
||||
(CancelFunc cancelFunc) {
|
||||
return _tabMenuBuilder(key, cancelFunc);
|
||||
},
|
||||
target: e.position,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
return Listener(
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
return;
|
||||
}
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == key)
|
||||
.page as RemotePage;
|
||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) {
|
||||
showRightMenu(
|
||||
(CancelFunc cancelFunc) {
|
||||
return _tabMenuBuilder(key, cancelFunc);
|
||||
},
|
||||
target: e.position,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(() => SubWindowDragToResizeArea(
|
||||
@@ -449,7 +350,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
void onRemoveId(String id) async {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
// Keep calling until the window status is hidden.
|
||||
//
|
||||
// Workaround for Windows:
|
||||
@@ -483,9 +383,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
tabController.clear();
|
||||
return true;
|
||||
} else {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
final bool res;
|
||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
@@ -499,4 +399,132 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
_update_remote_count() =>
|
||||
RemoteCountState.find().value = tabController.length;
|
||||
|
||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final switchUuid = args['switch_uuid'];
|
||||
final sessionId = args['session_id'];
|
||||
final tabWindowId = args['tab_window_id'];
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (stateGlobal.fullscreen.isTrue) {
|
||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
}
|
||||
await setNewConnectWindowFrame(windowId(), id!, screenRect);
|
||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||
await windowOnTop(windowId());
|
||||
});
|
||||
});
|
||||
ConnectionTypeState.init(id);
|
||||
_toolbarState.setShow(
|
||||
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: args['forceRelay'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final display = args['display'];
|
||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventGetRemoteList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => e.key)
|
||||
.toList()
|
||||
.join(',');
|
||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||
.toList()
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == id)
|
||||
.page as RemotePage;
|
||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get cached session data: $e');
|
||||
}
|
||||
if (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final remotePage =
|
||||
tabController.state.value.selectedTabInfo.page as RemotePage;
|
||||
final ffi = remotePage.ffi;
|
||||
final displayRect = ffi.ffiModel.displaysRect();
|
||||
if (displayRect != null) {
|
||||
final wc = WindowController.fromWindowId(windowId());
|
||||
Rect? frame;
|
||||
try {
|
||||
frame = await wc.getFrame();
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"Failed to get frame of window $windowId, it may be hidden");
|
||||
}
|
||||
if (frame != null) {
|
||||
ffi.cursorModel.moveLocal(0, 0);
|
||||
final coords = RemoteWindowCoords(
|
||||
frame,
|
||||
CanvasCoords.fromCanvasModel(ffi.canvasModel),
|
||||
CursorCoords.fromCursorModel(ffi.cursorModel),
|
||||
displayRect);
|
||||
returnValue = jsonEncode(coords.toJson());
|
||||
}
|
||||
}
|
||||
} else if (call.method == kWindowEventSetFullscreen) {
|
||||
stateGlobal.setFullscreen(call.arguments == 'true');
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
@@ -76,14 +77,20 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
||||
],
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
builder: (context, serverModel, child) {
|
||||
final body = Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: ConnectionManager(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return isLinux
|
||||
? buildVirtualWindowFrame(context, body)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: body,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -157,7 +164,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
controller: serverModel.tabController,
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
maxLabelWidth: 100,
|
||||
tail: buildScrollJumper(),
|
||||
tail: null, //buildScrollJumper(),
|
||||
selectedTabBackgroundColor:
|
||||
Theme.of(context).hintColor.withOpacity(0),
|
||||
tabBuilder: (key, icon, label, themeConf) {
|
||||
@@ -282,9 +289,9 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
windowManager.close();
|
||||
return true;
|
||||
} else {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
final bool res;
|
||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
@@ -701,17 +708,86 @@ class _CmControlPanel extends StatelessWidget {
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !client.inVoiceCall,
|
||||
child: buildButton(
|
||||
context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(
|
||||
Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: buildButton(context,
|
||||
color: MyTheme.accent,
|
||||
onClick: null, onTapDown: (details) async {
|
||||
final devicesInfo = await AudioInput.getDevicesInfo();
|
||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
msgBox(
|
||||
gFFI.sessionId,
|
||||
'custom-nocancel-info',
|
||||
'Prompt',
|
||||
'no_audio_input_device_tip',
|
||||
'',
|
||||
gFFI.dialogManager,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentDevice = devicesInfo['current'] as String;
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
final position = RelativeRect.fromLTRB(x, y, x, y);
|
||||
showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: devices
|
||||
.map((d) => PopupMenuItem<String>(
|
||||
value: d,
|
||||
height: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () => AudioInput.setDevice(d),
|
||||
child: IgnorePointer(
|
||||
child: RadioMenuButton(
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) AudioInput.setDevice(v);
|
||||
},
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth:
|
||||
kConnectionManagerWindowSizeClosedChat
|
||||
.width -
|
||||
80),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.call_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Audio input",
|
||||
textColor: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: buildButton(
|
||||
context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(
|
||||
Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
@@ -872,12 +948,14 @@ class _CmControlPanel extends StatelessWidget {
|
||||
|
||||
Widget buildButton(BuildContext context,
|
||||
{required Color? color,
|
||||
required Function() onClick,
|
||||
Icon? icon,
|
||||
GestureTapCallback? onClick,
|
||||
Widget? icon,
|
||||
BoxBorder? border,
|
||||
required String text,
|
||||
required Color? textColor,
|
||||
String? tooltip}) {
|
||||
String? tooltip,
|
||||
GestureTapDownCallback? onTapDown}) {
|
||||
assert(!(onClick == null && onTapDown == null));
|
||||
Widget textWidget;
|
||||
if (icon != null) {
|
||||
textWidget = Text(
|
||||
@@ -901,7 +979,16 @@ class _CmControlPanel extends StatelessWidget {
|
||||
color: color, borderRadius: borderRadius, border: border),
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: () => checkClickTime(client.id, onClick),
|
||||
onTap: () {
|
||||
if (onClick == null) return;
|
||||
checkClickTime(client.id, onClick);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
if (onTapDown == null) return;
|
||||
checkClickTime(client.id, () {
|
||||
onTapDown.call(details);
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
@@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: FileManagerTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: PortForwardTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -341,8 +341,9 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
||||
@protected
|
||||
void handleTap() {
|
||||
widget.onTap?.call();
|
||||
|
||||
Navigator.pop<T>(context, widget.value);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop<T>(context, widget.value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -445,9 +445,18 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
|
||||
bool get isEnabled => enabled?.value ?? true;
|
||||
|
||||
RxBool get curOption;
|
||||
Future<void> setOption(bool? option);
|
||||
|
||||
tryPop(BuildContext context) {
|
||||
if (dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
super.dismissCallback?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
@@ -481,44 +490,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
if (switchType == SwitchType.sswitch) {
|
||||
return Switch(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
if (super.dismissCallback != null) {
|
||||
super.dismissCallback!();
|
||||
}
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
onChanged: isEnabled
|
||||
? (v) {
|
||||
tryPop(context);
|
||||
setOption(v);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
return Checkbox(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
if (super.dismissCallback != null) {
|
||||
super.dismissCallback!();
|
||||
}
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
onChanged: isEnabled
|
||||
? (v) {
|
||||
tryPop(context);
|
||||
setOption(v);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
})),
|
||||
))
|
||||
])),
|
||||
onPressed: () {
|
||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
if (super.dismissCallback != null) {
|
||||
super.dismissCallback!();
|
||||
}
|
||||
}
|
||||
setOption(!curOption.value);
|
||||
},
|
||||
onPressed: isEnabled
|
||||
? () {
|
||||
tryPop(context);
|
||||
setOption(!curOption.value);
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
)
|
||||
];
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
@@ -26,12 +26,11 @@ import './popup_menu.dart';
|
||||
import './kb_layout_type_chooser.dart';
|
||||
|
||||
class ToolbarState {
|
||||
final kStoreKey = 'remoteMenubarState';
|
||||
late RxBool show;
|
||||
late RxBool _pin;
|
||||
|
||||
ToolbarState() {
|
||||
final s = bind.getLocalFlutterOption(k: kStoreKey);
|
||||
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||
if (s.isEmpty) {
|
||||
_initSet(false, false);
|
||||
return;
|
||||
@@ -52,8 +51,8 @@ class ToolbarState {
|
||||
|
||||
_initSet(bool s, bool p) {
|
||||
// Show remubar when connection is established.
|
||||
show =
|
||||
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
||||
show = RxBool(
|
||||
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
||||
_pin = RxBool(p);
|
||||
}
|
||||
|
||||
@@ -85,7 +84,7 @@ class ToolbarState {
|
||||
|
||||
_savePin() async {
|
||||
bind.setLocalFlutterOption(
|
||||
k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
|
||||
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||
}
|
||||
|
||||
save() async {
|
||||
@@ -589,7 +588,7 @@ class _MobileActionMenu extends StatelessWidget {
|
||||
assetName: 'assets/actions_mobile.svg',
|
||||
tooltip: 'Mobile Actions',
|
||||
onPressed: () =>
|
||||
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
|
||||
ffi.dialogManager.mobileActionsOverlayVisible.toggle(),
|
||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||
? _ToolbarTheme.blueColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
@@ -615,14 +614,14 @@ class _MonitorMenu extends StatelessWidget {
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
||||
|
||||
bool get supportIndividualWindows =>
|
||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => showMonitorsToolbar
|
||||
? buildMultiMonitorMenu()
|
||||
: Obx(() => buildMonitorMenu());
|
||||
? buildMultiMonitorMenu(context)
|
||||
: Obx(() => buildMonitorMenu(context));
|
||||
|
||||
Widget buildMonitorMenu() {
|
||||
Widget buildMonitorMenu(BuildContext context) {
|
||||
final width = SimpleWrapper<double>(0);
|
||||
final monitorsIcon =
|
||||
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
||||
@@ -636,20 +635,23 @@ class _MonitorMenu extends StatelessWidget {
|
||||
menuStyle: MenuStyle(
|
||||
padding:
|
||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||
}
|
||||
|
||||
Widget buildMultiMonitorMenu() {
|
||||
return Row(children: buildMonitorList(true));
|
||||
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||
return Row(children: buildMonitorList(context, true));
|
||||
}
|
||||
|
||||
Widget buildMonitorSubmenuWidget() {
|
||||
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: buildMonitorList(false)),
|
||||
supportIndividualWindows ? Divider() : Offstage(),
|
||||
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
||||
Row(children: buildMonitorList(context, false)),
|
||||
supportIndividualWindows && m.useTextureRender ? Divider() : Offstage(),
|
||||
supportIndividualWindows && m.useTextureRender
|
||||
? chooseDisplayBehavior()
|
||||
: Offstage(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -680,7 +682,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> buildMonitorList(bool isMulti) {
|
||||
List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
|
||||
final List<Widget> monitorList = [];
|
||||
final pi = ffi.ffiModel.pi;
|
||||
|
||||
@@ -735,7 +737,10 @@ class _MonitorMenu extends StatelessWidget {
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
monitorList.add(buildMonitorButton(i));
|
||||
}
|
||||
if (supportIndividualWindows && pi.displays.length > 1) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
if (supportIndividualWindows &&
|
||||
m.useTextureRender &&
|
||||
pi.displays.length > 1) {
|
||||
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
||||
}
|
||||
return monitorList;
|
||||
@@ -818,7 +823,12 @@ class _MonitorMenu extends StatelessWidget {
|
||||
}
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
if (display.value != i) {
|
||||
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
|
||||
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||
bind.mainGetUseTextureRender() &&
|
||||
bind.sessionGetDisplaysAsIndividualWindows(
|
||||
sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
if (isChooseDisplayToOpenInNewWindow) {
|
||||
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
||||
} else {
|
||||
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
||||
@@ -1045,7 +1055,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_screenAdjustor.updateScreen();
|
||||
|
||||
menuChildrenGetter() {
|
||||
final menuChildren = <Widget>[
|
||||
_screenAdjustor.adjustWindow(context),
|
||||
@@ -1058,11 +1067,17 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
// We may add this feature if it is needed and we have an EV certificate.
|
||||
// _VirtualDisplayMenu(
|
||||
// id: widget.id,
|
||||
// ffi: widget.ffi,
|
||||
// ),
|
||||
if (pi.isRustDeskIdd)
|
||||
_RustDeskVirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
if (pi.isAmyuniIdd)
|
||||
_AmyuniVirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
cursorToggles(),
|
||||
Divider(),
|
||||
toggles(),
|
||||
];
|
||||
@@ -1207,6 +1222,25 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
});
|
||||
}
|
||||
|
||||
cursorToggles() {
|
||||
return futureBuilder(
|
||||
future: toolbarCursor(context, id, ffi),
|
||||
hasData: (data) {
|
||||
final v = data as List<TToggleMenu>;
|
||||
if (v.isEmpty) return Offstage();
|
||||
return Column(children: [
|
||||
Divider(),
|
||||
...v
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi))
|
||||
.toList(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
toggles() {
|
||||
return futureBuilder(
|
||||
future: toolbarDisplayToggle(context, id, ffi),
|
||||
@@ -1540,21 +1574,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenu extends StatefulWidget {
|
||||
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
_VirtualDisplayMenu({
|
||||
_RustDeskVirtualDisplayMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
|
||||
State<_RustDeskVirtualDisplayMenu> createState() =>
|
||||
_RustDeskVirtualDisplayMenuState();
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
class _RustDeskVirtualDisplayMenuState
|
||||
extends State<_RustDeskVirtualDisplayMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -1569,7 +1605,7 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
|
||||
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||
|
||||
final children = <Widget>[];
|
||||
@@ -1611,6 +1647,82 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
_AmyuniVirtualDisplayMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_AmyuniVirtualDisplayMenu> createState() =>
|
||||
_AmiyuniVirtualDisplayMenuState();
|
||||
}
|
||||
|
||||
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||
return Offstage();
|
||||
}
|
||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||
|
||||
final children = <Widget>[
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () => bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId, index: 0, on: false),
|
||||
child: Icon(Icons.remove),
|
||||
),
|
||||
Text(count.toString()),
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||
? null
|
||||
: () => bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId, index: 0, on: true),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Divider(),
|
||||
Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
},
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)),
|
||||
];
|
||||
|
||||
return _SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: children,
|
||||
child: Text(translate("Virtual display")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardMenu extends StatelessWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
@@ -1773,7 +1885,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: kOptionViewOnly);
|
||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
}
|
||||
: null,
|
||||
@@ -1852,32 +1964,70 @@ class _VoiceCallMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
menuChildrenGetter() {
|
||||
final audioInput =
|
||||
AudioInput(builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
children: devices
|
||||
.map((d) => RdoMenuButton<String>(
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: 250),
|
||||
),
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) setDevice(v);
|
||||
},
|
||||
ffi: ffi,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
return [
|
||||
audioInput,
|
||||
Divider(),
|
||||
MenuButton(
|
||||
child: Text(translate('End call')),
|
||||
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
ffi: ffi,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return Obx(
|
||||
() {
|
||||
final String tooltip;
|
||||
final String icon;
|
||||
switch (ffi.chatModel.voiceCallStatus.value) {
|
||||
case VoiceCallStatus.waitingForResponse:
|
||||
tooltip = "Waiting";
|
||||
icon = "assets/call_wait.svg";
|
||||
break;
|
||||
return buildCallWaiting(context);
|
||||
case VoiceCallStatus.connected:
|
||||
tooltip = "Disconnect";
|
||||
icon = "assets/call_end.svg";
|
||||
break;
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Voice call',
|
||||
svg: 'assets/voice_call.svg',
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: menuChildrenGetter,
|
||||
ffi: ffi,
|
||||
);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
return _IconMenuButton(
|
||||
assetName: icon,
|
||||
tooltip: tooltip,
|
||||
onPressed: () =>
|
||||
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCallWaiting(BuildContext context) {
|
||||
return _IconMenuButton(
|
||||
assetName: "assets/call_wait.svg",
|
||||
tooltip: "Waiting",
|
||||
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordMenu extends StatelessWidget {
|
||||
@@ -2014,7 +2164,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
final Color hoverColor;
|
||||
final List<Widget> Function() menuChildrenGetter;
|
||||
final MenuStyle? menuStyle;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
final double? width;
|
||||
|
||||
_IconSubmenuButton({
|
||||
@@ -2025,7 +2175,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.menuChildrenGetter,
|
||||
required this.ffi,
|
||||
this.ffi,
|
||||
this.menuStyle,
|
||||
this.width,
|
||||
}) : super(key: key);
|
||||
@@ -2107,13 +2257,13 @@ class MenuButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final Widget? trailingIcon;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
MenuButton(
|
||||
{Key? key,
|
||||
this.onPressed,
|
||||
this.trailingIcon,
|
||||
required this.child,
|
||||
required this.ffi})
|
||||
this.ffi})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2122,7 +2272,9 @@ class MenuButton extends StatelessWidget {
|
||||
key: key,
|
||||
onPressed: onPressed != null
|
||||
? () {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onPressed?.call();
|
||||
}
|
||||
: null,
|
||||
@@ -2135,13 +2287,13 @@ class CkbMenuButton extends StatelessWidget {
|
||||
final bool? value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
const CkbMenuButton(
|
||||
{Key? key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
required this.child,
|
||||
required this.ffi})
|
||||
this.ffi})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2152,7 +2304,9 @@ class CkbMenuButton extends StatelessWidget {
|
||||
child: child,
|
||||
onChanged: onChanged != null
|
||||
? (bool? value) {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
}
|
||||
: null,
|
||||
@@ -2165,13 +2319,13 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
final T? groupValue;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
const RdoMenuButton({
|
||||
Key? key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.child,
|
||||
required this.ffi,
|
||||
this.ffi,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -2183,7 +2337,9 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
child: child,
|
||||
onChanged: onChanged != null
|
||||
? (T? value) {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
}
|
||||
: null,
|
||||
@@ -2227,18 +2383,18 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
super.initState();
|
||||
|
||||
final confLeft = double.tryParse(
|
||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-left'));
|
||||
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
|
||||
if (confLeft == null) {
|
||||
bind.mainSetLocalOption(
|
||||
key: 'remote-menubar-drag-left', value: left.toString());
|
||||
key: kOptionRemoteMenubarDragLeft, value: left.toString());
|
||||
} else {
|
||||
left = confLeft;
|
||||
}
|
||||
final confRight = double.tryParse(
|
||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-right'));
|
||||
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
|
||||
if (confRight == null) {
|
||||
bind.mainSetLocalOption(
|
||||
key: 'remote-menubar-drag-right', value: right.toString());
|
||||
key: kOptionRemoteMenubarDragRight, value: right.toString());
|
||||
} else {
|
||||
right = confRight;
|
||||
}
|
||||
@@ -2309,7 +2465,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
Obx(() => Offstage(
|
||||
if (!isMacOS) Obx(() => Offstage(
|
||||
offstage: isFullscreen.isFalse,
|
||||
child: TextButton(
|
||||
onPressed: () => widget.setMinimize(),
|
||||
@@ -2370,10 +2526,11 @@ class InputModeMenu {
|
||||
|
||||
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
|
||||
|
||||
Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
|
||||
Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
|
||||
return Listener(
|
||||
onPointerHover: (PointerHoverEvent e) =>
|
||||
ffi.inputModel.lastMousePos = e.position,
|
||||
onPointerHover: (PointerHoverEvent e) => {
|
||||
if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
|
||||
},
|
||||
child: MouseRegion(
|
||||
child: child,
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||
import 'package:scroll_pos/scroll_pos.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
import '../../utils/multi_window_manager.dart';
|
||||
|
||||
@@ -256,6 +257,8 @@ class DesktopTab extends StatelessWidget {
|
||||
late final DesktopTabType tabType;
|
||||
late final bool isMainWindow;
|
||||
|
||||
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||
|
||||
DesktopTab({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
@@ -320,18 +323,18 @@ class DesktopTab extends StatelessWidget {
|
||||
return buildRemoteBlock(
|
||||
child: child,
|
||||
use: () async {
|
||||
var access_mode = await bind.mainGetOption(key: 'access-mode');
|
||||
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||
var option = option2bool(
|
||||
'allow-remote-config-modification',
|
||||
kOptionAllowRemoteConfigModification,
|
||||
await bind.mainGetOption(
|
||||
key: 'allow-remote-config-modification'));
|
||||
key: kOptionAllowRemoteConfigModification));
|
||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _tabWidgets = [];
|
||||
Widget _buildPageView() {
|
||||
return _buildBlock(
|
||||
final child = _buildBlock(
|
||||
child: Obx(() => PageView(
|
||||
controller: state.value.pageController,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
@@ -355,6 +358,11 @@ class DesktopTab extends StatelessWidget {
|
||||
return newList;
|
||||
}
|
||||
}())));
|
||||
if (tabType == DesktopTabType.remoteScreen) {
|
||||
return Container(color: kColorCanvas, child: child);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether to show ListView
|
||||
@@ -388,6 +396,16 @@ class DesktopTab extends StatelessWidget {
|
||||
}
|
||||
: null,
|
||||
onPanStart: (_) => startDragging(isMainWindow),
|
||||
onPanCancel: () {
|
||||
if (isMacOS) {
|
||||
setMovable(isMainWindow, false);
|
||||
}
|
||||
},
|
||||
onPanEnd: (_) {
|
||||
if (isMacOS) {
|
||||
setMovable(isMainWindow, false);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Offstage(
|
||||
@@ -430,6 +448,7 @@ class DesktopTab extends StatelessWidget {
|
||||
},
|
||||
child: _ListView(
|
||||
controller: controller,
|
||||
invisibleTabKeys: invisibleTabKeys,
|
||||
tabBuilder: tabBuilder,
|
||||
tabMenuBuilder: tabMenuBuilder,
|
||||
labelGetter: labelGetter,
|
||||
@@ -448,12 +467,14 @@ class DesktopTab extends StatelessWidget {
|
||||
tabType: tabType,
|
||||
state: state,
|
||||
tabController: controller,
|
||||
invisibleTabKeys: invisibleTabKeys,
|
||||
tail: tail,
|
||||
showMinimize: showMinimize,
|
||||
showMaximize: showMaximize,
|
||||
showClose: showClose,
|
||||
onClose: onWindowCloseButton,
|
||||
)
|
||||
labelGetter: labelGetter,
|
||||
).paddingOnly(left: 10)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -471,17 +492,22 @@ class WindowActionPanel extends StatefulWidget {
|
||||
final Widget? tail;
|
||||
final Future<bool> Function()? onClose;
|
||||
|
||||
final RxList<String> invisibleTabKeys;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
const WindowActionPanel(
|
||||
{Key? key,
|
||||
required this.isMainWindow,
|
||||
required this.tabType,
|
||||
required this.state,
|
||||
required this.tabController,
|
||||
required this.invisibleTabKeys,
|
||||
this.tail,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true,
|
||||
this.onClose})
|
||||
this.onClose,
|
||||
this.labelGetter})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -537,6 +563,16 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
stateGlobal.isFocused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
stateGlobal.isFocused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
stateGlobal.setMinimized(true);
|
||||
@@ -620,14 +656,12 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
}
|
||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||
if (isMacOS && await windowManager.isFullScreen()) {
|
||||
stateGlobal.closeOnFullscreen ??= true;
|
||||
await windowManager.setFullScreen(false);
|
||||
await macOSWindowClose(
|
||||
() async => await windowManager.isFullScreen(),
|
||||
mainWindowClose,
|
||||
);
|
||||
} else {
|
||||
stateGlobal.closeOnFullscreen ??= false;
|
||||
await mainWindowClose();
|
||||
}
|
||||
} else {
|
||||
@@ -639,7 +673,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
|
||||
if (await widget.onClose?.call() ?? true) {
|
||||
if (await controller.isFullScreen()) {
|
||||
stateGlobal.closeOnFullscreen ??= true;
|
||||
await controller.setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
await macOSWindowClose(
|
||||
@@ -647,7 +680,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
() async => await notMainWindowClose(controller),
|
||||
);
|
||||
} else {
|
||||
stateGlobal.closeOnFullscreen ??= false;
|
||||
await notMainWindowClose(controller);
|
||||
}
|
||||
}
|
||||
@@ -658,11 +690,34 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
bool showTabDowndown() {
|
||||
return widget.tabController.state.value.tabs.length > 1 &&
|
||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
|
||||
widget.tabController.tabType == DesktopTabType.portForward ||
|
||||
widget.tabController.tabType == DesktopTabType.cm);
|
||||
}
|
||||
|
||||
List<String> existingInvisibleTab() {
|
||||
return widget.invisibleTabKeys
|
||||
.where((key) =>
|
||||
widget.tabController.state.value.tabs.any((tab) => tab.key == key))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Obx(() => Offstage(
|
||||
offstage:
|
||||
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
||||
child: _TabDropDownButton(
|
||||
controller: widget.tabController,
|
||||
labelGetter: widget.labelGetter,
|
||||
tabkeys: existingInvisibleTab()),
|
||||
)),
|
||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
||||
Offstage(
|
||||
offstage: kUseCompatibleUiMode,
|
||||
@@ -741,6 +796,14 @@ void startDragging(bool isMainWindow) {
|
||||
}
|
||||
}
|
||||
|
||||
void setMovable(bool isMainWindow, bool movable) {
|
||||
if (isMainWindow) {
|
||||
windowManager.setMovable(movable);
|
||||
} else {
|
||||
WindowController.fromWindowId(kWindowId!).setMovable(movable);
|
||||
}
|
||||
}
|
||||
|
||||
/// return true -> window will be maximize
|
||||
/// return false -> window will be unmaximize
|
||||
Future<bool> toggleMaximize(bool isMainWindow) async {
|
||||
@@ -768,9 +831,9 @@ Future<bool> closeConfirmDialog() async {
|
||||
var confirm = true;
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
submit() {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
String value = bool2option(opt, confirm);
|
||||
bind.mainSetLocalOption(key: opt, value: value);
|
||||
String value = bool2option(kOptionEnableConfirmClosingTabs, confirm);
|
||||
bind.mainSetLocalOption(
|
||||
key: kOptionEnableConfirmClosingTabs, value: value);
|
||||
close(true);
|
||||
}
|
||||
|
||||
@@ -814,6 +877,7 @@ Future<bool> closeConfirmDialog() async {
|
||||
|
||||
class _ListView extends StatelessWidget {
|
||||
final DesktopTabController controller;
|
||||
final RxList<String> invisibleTabKeys;
|
||||
|
||||
final TabBuilder? tabBuilder;
|
||||
final TabMenuBuilder? tabMenuBuilder;
|
||||
@@ -825,8 +889,9 @@ class _ListView extends StatelessWidget {
|
||||
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
|
||||
const _ListView({
|
||||
_ListView({
|
||||
required this.controller,
|
||||
required this.invisibleTabKeys,
|
||||
this.tabBuilder,
|
||||
this.tabMenuBuilder,
|
||||
this.labelGetter,
|
||||
@@ -846,6 +911,19 @@ class _ListView extends StatelessWidget {
|
||||
controller.tabType == DesktopTabType.install;
|
||||
}
|
||||
|
||||
onVisibilityChanged(VisibilityInfo info) {
|
||||
final key = (info.key as ValueKey).value;
|
||||
if (info.visibleFraction < 0.75) {
|
||||
if (!invisibleTabKeys.contains(key)) {
|
||||
invisibleTabKeys.add(key);
|
||||
}
|
||||
invisibleTabKeys.removeWhere((key) =>
|
||||
controller.state.value.tabs.where((e) => e.key == key).isEmpty);
|
||||
} else {
|
||||
invisibleTabKeys.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => ListView(
|
||||
@@ -858,35 +936,45 @@ class _ListView extends StatelessWidget {
|
||||
: state.value.tabs.asMap().entries.map((e) {
|
||||
final index = e.key;
|
||||
final tab = e.value;
|
||||
return _Tab(
|
||||
final label = labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label);
|
||||
final child = VisibilityDetector(
|
||||
key: ValueKey(tab.key),
|
||||
index: index,
|
||||
tabInfoKey: tab.key,
|
||||
label: labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label),
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
selected: state.value.selected,
|
||||
onClose: () {
|
||||
if (tab.onTabCloseButton != null) {
|
||||
tab.onTabCloseButton!();
|
||||
} else {
|
||||
controller.remove(index);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
controller.jumpTo(index);
|
||||
tab.onTap?.call();
|
||||
},
|
||||
tabBuilder: tabBuilder,
|
||||
tabMenuBuilder: tabMenuBuilder,
|
||||
maxLabelWidth: maxLabelWidth,
|
||||
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
||||
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
||||
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
||||
selectedBorderColor: selectedBorderColor,
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
child: _Tab(
|
||||
key: ValueKey(tab.key),
|
||||
index: index,
|
||||
tabInfoKey: tab.key,
|
||||
label: label,
|
||||
tabType: controller.tabType,
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
selected: state.value.selected,
|
||||
onClose: () {
|
||||
if (tab.onTabCloseButton != null) {
|
||||
tab.onTabCloseButton!();
|
||||
} else {
|
||||
controller.remove(index);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
controller.jumpTo(index);
|
||||
tab.onTap?.call();
|
||||
},
|
||||
tabBuilder: tabBuilder,
|
||||
tabMenuBuilder: tabMenuBuilder,
|
||||
maxLabelWidth: maxLabelWidth,
|
||||
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
||||
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
||||
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
||||
selectedBorderColor: selectedBorderColor,
|
||||
),
|
||||
);
|
||||
return GestureDetector(
|
||||
onPanStart: (e) {},
|
||||
child: child,
|
||||
);
|
||||
}).toList()));
|
||||
}
|
||||
@@ -896,6 +984,7 @@ class _Tab extends StatefulWidget {
|
||||
final int index;
|
||||
final String tabInfoKey;
|
||||
final Rx<String> label;
|
||||
final DesktopTabType tabType;
|
||||
final IconData? selectedIcon;
|
||||
final IconData? unselectedIcon;
|
||||
final bool closable;
|
||||
@@ -914,6 +1003,7 @@ class _Tab extends StatefulWidget {
|
||||
required this.index,
|
||||
required this.tabInfoKey,
|
||||
required this.label,
|
||||
required this.tabType,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
@@ -953,7 +1043,9 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
||||
child: Tooltip(
|
||||
message: translate(widget.label.value),
|
||||
message: widget.tabType == DesktopTabType.main
|
||||
? ''
|
||||
: translate(widget.label.value),
|
||||
child: Text(
|
||||
translate(widget.label.value),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -1110,6 +1202,7 @@ class ActionIcon extends StatefulWidget {
|
||||
final String? message;
|
||||
final IconData icon;
|
||||
final GestureTapCallback? onTap;
|
||||
final GestureTapDownCallback? onTapDown;
|
||||
final bool isClose;
|
||||
final double iconSize;
|
||||
final double boxSize;
|
||||
@@ -1119,6 +1212,7 @@ class ActionIcon extends StatefulWidget {
|
||||
this.message,
|
||||
required this.icon,
|
||||
this.onTap,
|
||||
this.onTapDown,
|
||||
this.isClose = false,
|
||||
this.iconSize = _kActionIconSize,
|
||||
this.boxSize = _kTabBarHeight - 1})
|
||||
@@ -1148,6 +1242,7 @@ class _ActionIconState extends State<ActionIcon> {
|
||||
: MyTheme.tabbar(context).hoverColor,
|
||||
onHover: (value) => hover.value = value,
|
||||
onTap: widget.onTap,
|
||||
onTapDown: widget.onTapDown,
|
||||
child: SizedBox(
|
||||
height: widget.boxSize,
|
||||
width: widget.boxSize,
|
||||
@@ -1188,6 +1283,103 @@ class AddButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TabDropDownButton extends StatefulWidget {
|
||||
final DesktopTabController controller;
|
||||
final List<String> tabkeys;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
const _TabDropDownButton(
|
||||
{required this.controller, required this.tabkeys, this.labelGetter});
|
||||
|
||||
@override
|
||||
State<_TabDropDownButton> createState() => _TabDropDownButtonState();
|
||||
}
|
||||
|
||||
class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
||||
var position = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<String> sortedKeys = widget.controller.state.value.tabs
|
||||
.where((e) => widget.tabkeys.contains(e.key))
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
return ActionIcon(
|
||||
onTapDown: (details) {
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
position = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
icon: Icons.arrow_drop_down,
|
||||
onTap: () {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: sortedKeys.map((e) {
|
||||
var label = e;
|
||||
final tabInfo = widget.controller.state.value.tabs
|
||||
.firstWhereOrNull((element) => element.key == e);
|
||||
if (tabInfo != null) {
|
||||
label = tabInfo.label;
|
||||
}
|
||||
if (widget.labelGetter != null) {
|
||||
label = widget.labelGetter!(e).value;
|
||||
}
|
||||
var index = widget.controller.state.value.tabs
|
||||
.indexWhere((t) => t.key == e);
|
||||
label = '${index + 1}. $label';
|
||||
final menuHover = false.obs;
|
||||
final btnHover = false.obs;
|
||||
return PopupMenuItem<String>(
|
||||
value: e,
|
||||
height: 32,
|
||||
onTap: () {
|
||||
widget.controller.jumpToByKey(e);
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
onHover: (event) => setState(() => menuHover.value = true),
|
||||
onExit: (event) => setState(() => menuHover.value = false),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(child: Text(label)),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !(tabInfo?.onTabCloseButton != null &&
|
||||
menuHover.value),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
tabInfo?.onTabCloseButton?.call();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onHover: (event) =>
|
||||
setState(() => btnHover.value = true),
|
||||
onExit: (event) =>
|
||||
setState(() => btnHover.value = false),
|
||||
child: Icon(Icons.close,
|
||||
color:
|
||||
btnHover.value ? Colors.red : null))),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||
final Color? selectedTabIconColor;
|
||||
final Color? unSelectedTabIconColor;
|
||||
|
||||
@@ -95,6 +95,7 @@ Future<void> main(List<String> args) async {
|
||||
desktopType = DesktopType.main;
|
||||
await windowManager.ensureInitialized();
|
||||
windowManager.setPreventClose(true);
|
||||
windowManager.setMovable(false);
|
||||
runMainApp(true);
|
||||
}
|
||||
}
|
||||
@@ -144,7 +145,8 @@ void runMainApp(bool startService) async {
|
||||
}
|
||||
windowManager.setOpacity(1);
|
||||
windowManager.setTitle(getWindowName());
|
||||
windowManager.setResizable(!bind.isIncomingOnly());
|
||||
// Do not use `windowManager.setResizable()` here.
|
||||
setResizable(!bind.isIncomingOnly());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,6 +168,7 @@ void runMultiWindow(
|
||||
final title = getWindowName();
|
||||
// set prevent close to true, we handle close event manually
|
||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||
WindowController.fromWindowId(kWindowId!).setMovable(false);
|
||||
late Widget widget;
|
||||
switch (appType) {
|
||||
case kAppTypeDesktopRemote:
|
||||
@@ -238,7 +241,7 @@ void runConnectionManagerScreen() async {
|
||||
} else {
|
||||
await showCmWindow(isStartup: true);
|
||||
}
|
||||
windowManager.setResizable(false);
|
||||
setResizable(false);
|
||||
// Start the uni links handler and redirect links to Native, not for Flutter.
|
||||
listenUniLinks(handleByFlutter: false);
|
||||
}
|
||||
@@ -248,7 +251,7 @@ bool _isCmReadyToShow = false;
|
||||
showCmWindow({bool isStartup = false}) async {
|
||||
if (isStartup) {
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||
size: kConnectionManagerWindowSizeClosedChat);
|
||||
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||
bind.mainHideDocker();
|
||||
await Future.wait([
|
||||
@@ -337,12 +340,11 @@ void runInstallPage() async {
|
||||
windowManager.focus();
|
||||
windowManager.setOpacity(1);
|
||||
windowManager.setAlignment(Alignment.center); // ensure
|
||||
windowManager.setTitle(getWindowName());
|
||||
});
|
||||
}
|
||||
|
||||
WindowOptions getHiddenTitleBarWindowOptions(
|
||||
{Size? size, bool center = false}) {
|
||||
{Size? size, bool center = false, bool? alwaysOnTop}) {
|
||||
var defaultTitleBarStyle = TitleBarStyle.hidden;
|
||||
// we do not hide titlebar on win7 because of the frame overflow.
|
||||
if (kUseCompatibleUiMode) {
|
||||
@@ -354,6 +356,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: defaultTitleBarStyle,
|
||||
alwaysOnTop: alwaysOnTop,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,6 +443,9 @@ class _AppState extends State<App> {
|
||||
if (isDesktop && desktopType == DesktopType.main) {
|
||||
child = keyListenerBuilder(context, child);
|
||||
}
|
||||
if (isLinux) {
|
||||
child = buildVirtualWindowFrame(context, child);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -65,10 +65,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}();
|
||||
}
|
||||
if (isAndroid) {
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (_updateUrl.isNotEmpty) setState(() {});
|
||||
});
|
||||
if (!bind.isCustomClient()) {
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (_updateUrl.isNotEmpty) setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_idController.addListener(() {
|
||||
@@ -84,7 +86,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
_buildUpdateUI(),
|
||||
if (!bind.isCustomClient()) _buildUpdateUI(),
|
||||
_buildRemoteIDTextField(),
|
||||
])),
|
||||
SliverFillRemaining(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'connection_page.dart';
|
||||
|
||||
abstract class PageShape extends Widget {
|
||||
@@ -25,8 +26,9 @@ class HomePageState extends State<HomePage> {
|
||||
var _selectedIndex = 0;
|
||||
int get selectedIndex => _selectedIndex;
|
||||
final List<PageShape> _pages = [];
|
||||
int _chatPageTabIndex = -1;
|
||||
bool get isChatPageCurrentTab => isAndroid
|
||||
? _selectedIndex == 1
|
||||
? _selectedIndex == _chatPageTabIndex
|
||||
: false; // change this when ios have chat page
|
||||
|
||||
void refreshPages() {
|
||||
@@ -43,8 +45,9 @@ class HomePageState extends State<HomePage> {
|
||||
|
||||
void initPages() {
|
||||
_pages.clear();
|
||||
_pages.add(ConnectionPage());
|
||||
if (isAndroid) {
|
||||
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
||||
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||
_chatPageTabIndex = _pages.length;
|
||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||
}
|
||||
_pages.add(SettingsPage());
|
||||
@@ -141,7 +144,7 @@ class HomePageState extends State<HomePage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
return Text("RustDesk");
|
||||
return Text(bind.mainGetAppNameSync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +157,7 @@ class WebHomePage extends StatelessWidget {
|
||||
// backgroundColor: MyTheme.grayBg,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text("RustDesk${isWeb ? " (Beta) " : ""}"),
|
||||
title: Text(bind.mainGetAppNameSync()),
|
||||
actions: connectionPage.appBarActions,
|
||||
),
|
||||
body: connectionPage,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -68,7 +69,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
WakelockPlus.enable();
|
||||
if (!isWeb) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
_physicalFocusNode.requestFocus();
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
@@ -77,7 +80,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
initSharedStates(widget.id);
|
||||
gFFI.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||
|
||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
}
|
||||
|
||||
@@ -87,6 +90,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
super.dispose();
|
||||
gFFI.dialogManager.hideMobileActionsOverlay();
|
||||
gFFI.inputModel.listenToMouse(false);
|
||||
gFFI.imageModel.disposeImage();
|
||||
gFFI.cursorModel.disposeImages();
|
||||
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
_mobileFocusNode.dispose();
|
||||
_physicalFocusNode.dispose();
|
||||
@@ -95,9 +100,16 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
await WakelockPlus.disable();
|
||||
if (!isWeb) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
await keyboardSubscription.cancel();
|
||||
removeSharedStates(widget.id);
|
||||
if (isAndroid) {
|
||||
// Only one client is considered here for now.
|
||||
// TODO: take into account the case where there are multiple clients
|
||||
gFFI.invokeMethod("on_voice_call_closed");
|
||||
}
|
||||
}
|
||||
|
||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||
@@ -219,7 +231,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||
// show now, and sleep a while to requestFocus to
|
||||
// make sure edit ready, so that keyboard wont show/hide/show/hide happen
|
||||
// make sure edit ready, so that keyboard won't show/hide/show/hide happen
|
||||
setState(() => _showEdit = true);
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
@@ -300,7 +312,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
color: kColorCanvas,
|
||||
child: isWebDesktop
|
||||
? getBodyForDesktopWithListener(keyboard)
|
||||
: SafeArea(
|
||||
@@ -365,9 +377,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
<Widget>[
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.tv),
|
||||
@@ -411,12 +421,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.message),
|
||||
onPressed: () {
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(
|
||||
widget.id, ChatModel.clientModeID));
|
||||
gFFI.chatModel.toggleChatOverlay();
|
||||
},
|
||||
icon: isAndroid
|
||||
? SvgPicture.asset('assets/chat.svg',
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.white, BlendMode.srcIn))
|
||||
: Icon(Icons.message),
|
||||
onPressed: () => isAndroid
|
||||
? showChatOptions(widget.id)
|
||||
: onPressedTextChat(widget.id),
|
||||
)
|
||||
]) +
|
||||
[
|
||||
@@ -534,6 +546,88 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}();
|
||||
}
|
||||
|
||||
onPressedTextChat(String id) {
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
|
||||
gFFI.chatModel.toggleChatOverlay();
|
||||
}
|
||||
|
||||
showChatOptions(String id) async {
|
||||
onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
|
||||
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
|
||||
|
||||
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
|
||||
{TextStyle? labelStyle}) =>
|
||||
TTextMenu(
|
||||
child: Text(translate(label), style: labelStyle),
|
||||
trailingIcon: Transform.scale(
|
||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||
child: IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
|
||||
final isInVoice = [
|
||||
VoiceCallStatus.waitingForResponse,
|
||||
VoiceCallStatus.connected
|
||||
].contains(gFFI.chatModel.voiceCallStatus.value);
|
||||
final menus = [
|
||||
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
|
||||
() => onPressedTextChat(widget.id)),
|
||||
isInVoice
|
||||
? makeTextMenu(
|
||||
'End voice call',
|
||||
SvgPicture.asset(
|
||||
'assets/call_wait.svg',
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
|
||||
),
|
||||
onPressEndVoiceCall,
|
||||
labelStyle: TextStyle(color: Colors.redAccent))
|
||||
: makeTextMenu(
|
||||
'Voice call',
|
||||
SvgPicture.asset(
|
||||
'assets/call_wait.svg',
|
||||
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
|
||||
),
|
||||
onPressVoiceCall),
|
||||
];
|
||||
getChild(TTextMenu menu) {
|
||||
if (menu.trailingIcon != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
menu.child,
|
||||
menu.trailingIcon!,
|
||||
]);
|
||||
} else {
|
||||
return menu.child;
|
||||
}
|
||||
}
|
||||
|
||||
final menuItems = menus
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
||||
.toList();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final x = 120.0;
|
||||
final y = size.height;
|
||||
var index = await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: menuItems,
|
||||
elevation: 8,
|
||||
);
|
||||
if (index != null && index < menus.length) {
|
||||
menus[index].onPressed.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// aka changeTouchMode
|
||||
BottomAppBar getGestureHelp() {
|
||||
return BottomAppBar(
|
||||
@@ -546,7 +640,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.ffiModel.toggleTouchMode();
|
||||
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||
bind.sessionPeerOption(
|
||||
sessionId: sessionId, name: "touch-mode", value: v);
|
||||
sessionId: sessionId, name: kOptionTouchMode, value: v);
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -832,6 +926,7 @@ void showOptions(
|
||||
List<TRadioMenu<String>> imageQualityRadios =
|
||||
await toolbarImageQuality(context, id, gFFI);
|
||||
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
||||
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
||||
List<TToggleMenu> displayToggles =
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
|
||||
@@ -872,8 +967,23 @@ void showOptions(
|
||||
})),
|
||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
];
|
||||
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||
final cursorTogglesList = cursorToggles
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxCursorToggleValues[e.key].value,
|
||||
onChanged: (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||
},
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
|
||||
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
||||
final toggles = displayToggles
|
||||
final displayTogglesList = displayToggles
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
@@ -886,6 +996,11 @@ void showOptions(
|
||||
},
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
final toggles = [
|
||||
...cursorTogglesList,
|
||||
if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
...displayTogglesList,
|
||||
];
|
||||
|
||||
Widget privacyModeWidget = Offstage();
|
||||
if (privacyModeList.length > 1) {
|
||||
|
||||
@@ -39,6 +39,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
final approveMode = gFFI.serverModel.approveMode;
|
||||
final verificationMethod = gFFI.serverModel.verificationMethod;
|
||||
final showPasswordOption = approveMode != 'click';
|
||||
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: gFFI.serverModel.connectStatus > 0,
|
||||
@@ -50,16 +51,19 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
value: 'AcceptSessionsViaPassword',
|
||||
child: listTile(
|
||||
'Accept sessions via password', approveMode == 'password'),
|
||||
enabled: !isApproveModeFixed,
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'AcceptSessionsViaClick',
|
||||
child:
|
||||
listTile('Accept sessions via click', approveMode == 'click'),
|
||||
enabled: !isApproveModeFixed,
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: "AcceptSessionsViaBoth",
|
||||
child: listTile("Accept sessions via both",
|
||||
approveMode != 'password' && approveMode != 'click'),
|
||||
enabled: !isApproveModeFixed,
|
||||
),
|
||||
if (showPasswordOption) const PopupMenuDivider(),
|
||||
if (showPasswordOption &&
|
||||
@@ -107,7 +111,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
} else if (value == kUsePermanentPassword ||
|
||||
value == kUseTemporaryPassword ||
|
||||
value == kUseBothPasswords) {
|
||||
bind.mainSetOption(key: "verification-method", value: value);
|
||||
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||
gFFI.serverModel.updatePasswordModel();
|
||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||
value = value.substring("AcceptSessionsVia".length);
|
||||
@@ -116,7 +120,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
} else if (value == "Click") {
|
||||
gFFI.serverModel.setApproveMode('click');
|
||||
} else {
|
||||
gFFI.serverModel.setApproveMode('');
|
||||
gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -158,6 +162,7 @@ class _ServerPageState extends State<ServerPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
buildPresetPasswordWarning(),
|
||||
gFFI.serverModel.isStart
|
||||
? ServerInfo()
|
||||
: ServiceNotRunningNotification(),
|
||||
@@ -226,7 +231,7 @@ class ScamWarningDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
int _countdown = 12;
|
||||
int _countdown = bind.isCustomClient() ? 0 : 12;
|
||||
bool show_warning = false;
|
||||
late Timer _timer;
|
||||
late ServerModel _serverModel;
|
||||
@@ -636,40 +641,94 @@ class ConnectionManager extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).marginOnly(bottom: 5),
|
||||
client.authorized
|
||||
? Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Colors.red)),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
bind.cmCloseConnection(connId: client.id);
|
||||
gFFI.invokeMethod(
|
||||
"cancel_notification", client.id);
|
||||
},
|
||||
label: Text(translate("Disconnect"))))
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(
|
||||
client, false);
|
||||
}).marginOnly(right: 15),
|
||||
if (serverModel.approveMode != 'password')
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(
|
||||
client, true);
|
||||
}),
|
||||
]),
|
||||
? _buildDisconnectButton(client)
|
||||
: _buildNewConnectionHint(serverModel, client),
|
||||
if (client.incomingVoiceCall && !client.inVoiceCall)
|
||||
..._buildNewVoiceCallHint(context, serverModel, client),
|
||||
])))
|
||||
.toList());
|
||||
}
|
||||
|
||||
Widget _buildDisconnectButton(Client client) {
|
||||
final disconnectButton = ElevatedButton.icon(
|
||||
style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
bind.cmCloseConnection(connId: client.id);
|
||||
gFFI.invokeMethod("cancel_notification", client.id);
|
||||
},
|
||||
label: Text(translate("Disconnect")),
|
||||
);
|
||||
final buttons = [disconnectButton];
|
||||
if (client.inVoiceCall) {
|
||||
buttons.insert(
|
||||
0,
|
||||
ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||
icon: const Icon(Icons.phone),
|
||||
label: Text(translate("Stop")),
|
||||
onPressed: () {
|
||||
bind.cmCloseVoiceCall(id: client.id);
|
||||
gFFI.invokeMethod("cancel_notification", client.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (buttons.length == 1) {
|
||||
return Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: disconnectButton,
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
children: buttons,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildNewConnectionHint(ServerModel serverModel, Client client) {
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(client, false);
|
||||
}).marginOnly(right: 15),
|
||||
if (serverModel.approveMode != 'password')
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
serverModel.sendLoginResponse(client, true);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
List<Widget> _buildNewVoiceCallHint(
|
||||
BuildContext context, ServerModel serverModel, Client client) {
|
||||
return [
|
||||
Text(
|
||||
translate("android_new_voice_call_tip"),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).marginOnly(bottom: 5),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
serverModel.handleVoiceCall(client, false);
|
||||
}).marginOnly(right: 15),
|
||||
if (serverModel.approveMode != 'password')
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
serverModel.handleVoiceCall(client, true);
|
||||
}),
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class PaddingCard extends StatelessWidget {
|
||||
@@ -786,6 +845,15 @@ void androidChannelInit() {
|
||||
gFFI.serverModel.stopService();
|
||||
break;
|
||||
}
|
||||
case "msgbox":
|
||||
{
|
||||
var type = arguments["type"] as String;
|
||||
var title = arguments["title"] as String;
|
||||
var text = arguments["text"] as String;
|
||||
var link = (arguments["link"] ?? '') as String;
|
||||
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrintStack(label: "MethodCallHandler err:$e");
|
||||
|
||||
@@ -27,7 +27,7 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
||||
final icon = Icon(Icons.settings);
|
||||
|
||||
@override
|
||||
final appBarActions = [ScanButton()];
|
||||
final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsState();
|
||||
@@ -44,6 +44,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _onlyWhiteList = false;
|
||||
var _enableDirectIPAccess = false;
|
||||
var _enableRecordSession = false;
|
||||
var _enableHardwareCodec = false;
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _allowAutoDisconnect = false;
|
||||
var _localIP = "";
|
||||
@@ -86,7 +87,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
final enableAbrRes = option2bool(
|
||||
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
|
||||
kOptionEnableAbr, await bind.mainGetOption(key: kOptionEnableAbr));
|
||||
if (enableAbrRes != _enableAbr) {
|
||||
update = true;
|
||||
_enableAbr = enableAbrRes;
|
||||
@@ -99,30 +100,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_denyLANDiscovery = denyLanDiscovery;
|
||||
}
|
||||
|
||||
final onlyWhiteList =
|
||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
||||
final onlyWhiteList = (await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||
defaultOptionWhitelist;
|
||||
if (onlyWhiteList != _onlyWhiteList) {
|
||||
update = true;
|
||||
_onlyWhiteList = onlyWhiteList;
|
||||
}
|
||||
|
||||
final enableDirectIPAccess = option2bool(
|
||||
'direct-server', await bind.mainGetOption(key: 'direct-server'));
|
||||
final enableDirectIPAccess = option2bool(kOptionDirectServer,
|
||||
await bind.mainGetOption(key: kOptionDirectServer));
|
||||
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
||||
update = true;
|
||||
_enableDirectIPAccess = enableDirectIPAccess;
|
||||
}
|
||||
|
||||
final enableRecordSession = option2bool('enable-record-session',
|
||||
await bind.mainGetOption(key: 'enable-record-session'));
|
||||
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(
|
||||
'allow-auto-record-incoming',
|
||||
await bind.mainGetOption(key: 'allow-auto-record-incoming'));
|
||||
kOptionAllowAutoRecordIncoming,
|
||||
await bind.mainGetOption(key: kOptionAllowAutoRecordIncoming));
|
||||
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
||||
update = true;
|
||||
_autoRecordIncomingSession = autoRecordIncomingSession;
|
||||
@@ -135,7 +143,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
final directAccessPort =
|
||||
await bind.mainGetOption(key: 'direct-access-port');
|
||||
await bind.mainGetOption(key: kOptionDirectAccessPort);
|
||||
if (directAccessPort != _directAccessPort) {
|
||||
update = true;
|
||||
_directAccessPort = directAccessPort;
|
||||
@@ -153,15 +161,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_buildDate = buildDate;
|
||||
}
|
||||
|
||||
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
|
||||
await bind.mainGetOption(key: 'allow-auto-disconnect'));
|
||||
final allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||
await bind.mainGetOption(key: kOptionAllowAutoDisconnect));
|
||||
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
||||
update = true;
|
||||
_allowAutoDisconnect = allowAutoDisconnect;
|
||||
}
|
||||
|
||||
final autoDisconnectTimeout =
|
||||
await bind.mainGetOption(key: 'auto-disconnect-timeout');
|
||||
await bind.mainGetOption(key: kOptionAutoDisconnectTimeout);
|
||||
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
||||
update = true;
|
||||
_autoDisconnectTimeout = autoDisconnectTimeout;
|
||||
@@ -218,6 +226,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final customClientSection = CustomSettingsSection(
|
||||
child: Column(
|
||||
children: [
|
||||
if (bind.isCustomClient())
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: loadPowered(context),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: loadLogo(),
|
||||
)
|
||||
],
|
||||
));
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
@@ -234,16 +257,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN discovery')),
|
||||
initialValue: _denyLANDiscovery,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: "enable-lan-discovery",
|
||||
value: bool2option("enable-lan-discovery", !v));
|
||||
final newValue = !option2bool('enable-lan-discovery',
|
||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
||||
setState(() {
|
||||
_denyLANDiscovery = newValue;
|
||||
});
|
||||
},
|
||||
onToggle: isOptionFixed(kOptionEnableLanDiscovery)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionEnableLanDiscovery,
|
||||
value: bool2option(kOptionEnableLanDiscovery, !v));
|
||||
final newValue = !option2bool(kOptionEnableLanDiscovery,
|
||||
await bind.mainGetOption(key: kOptionEnableLanDiscovery));
|
||||
setState(() {
|
||||
_denyLANDiscovery = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Row(children: [
|
||||
@@ -258,7 +283,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
onToggle: (_) async {
|
||||
update() async {
|
||||
final onlyWhiteList =
|
||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
||||
(await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||
defaultOptionWhitelist;
|
||||
if (onlyWhiteList != _onlyWhiteList) {
|
||||
setState(() {
|
||||
_onlyWhiteList = onlyWhiteList;
|
||||
@@ -272,26 +298,34 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
SettingsTile.switchTile(
|
||||
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
||||
initialValue: _enableAbr,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(key: "enable-abr", value: v ? "" : "N");
|
||||
final newValue = await bind.mainGetOption(key: "enable-abr") != "N";
|
||||
setState(() {
|
||||
_enableAbr = newValue;
|
||||
});
|
||||
},
|
||||
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionEnableAbr, value: v ? defaultOptionYes : "N");
|
||||
final newValue =
|
||||
await bind.mainGetOption(key: kOptionEnableAbr) != "N";
|
||||
setState(() {
|
||||
_enableAbr = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable recording session')),
|
||||
initialValue: _enableRecordSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: "enable-record-session", value: v ? "" : "N");
|
||||
final newValue =
|
||||
await bind.mainGetOption(key: "enable-record-session") != "N";
|
||||
setState(() {
|
||||
_enableRecordSession = newValue;
|
||||
});
|
||||
},
|
||||
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionEnableRecordSession,
|
||||
value: v ? defaultOptionYes : "N");
|
||||
final newValue =
|
||||
await bind.mainGetOption(key: kOptionEnableRecordSession) !=
|
||||
"N";
|
||||
setState(() {
|
||||
_enableRecordSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Row(
|
||||
@@ -318,21 +352,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
Icons.edit,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () async {
|
||||
final port = await changeDirectAccessPort(
|
||||
_localIP, _directAccessPort);
|
||||
setState(() {
|
||||
_directAccessPort = port;
|
||||
});
|
||||
}))
|
||||
onPressed: isOptionFixed(kOptionDirectAccessPort)
|
||||
? null
|
||||
: () async {
|
||||
final port = await changeDirectAccessPort(
|
||||
_localIP, _directAccessPort);
|
||||
setState(() {
|
||||
_directAccessPort = port;
|
||||
});
|
||||
}))
|
||||
]),
|
||||
initialValue: _enableDirectIPAccess,
|
||||
onToggle: (_) async {
|
||||
_enableDirectIPAccess = !_enableDirectIPAccess;
|
||||
String value = bool2option('direct-server', _enableDirectIPAccess);
|
||||
await bind.mainSetOption(key: 'direct-server', value: value);
|
||||
setState(() {});
|
||||
},
|
||||
onToggle: isOptionFixed(kOptionDirectServer)
|
||||
? null
|
||||
: (_) async {
|
||||
_enableDirectIPAccess = !_enableDirectIPAccess;
|
||||
String value =
|
||||
bool2option(kOptionDirectServer, _enableDirectIPAccess);
|
||||
await bind.mainSetOption(
|
||||
key: kOptionDirectServer, value: value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Row(
|
||||
@@ -359,22 +399,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
Icons.edit,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () async {
|
||||
final timeout = await changeAutoDisconnectTimeout(
|
||||
_autoDisconnectTimeout);
|
||||
setState(() {
|
||||
_autoDisconnectTimeout = timeout;
|
||||
});
|
||||
}))
|
||||
onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
|
||||
? null
|
||||
: () async {
|
||||
final timeout = await changeAutoDisconnectTimeout(
|
||||
_autoDisconnectTimeout);
|
||||
setState(() {
|
||||
_autoDisconnectTimeout = timeout;
|
||||
});
|
||||
}))
|
||||
]),
|
||||
initialValue: _allowAutoDisconnect,
|
||||
onToggle: (_) async {
|
||||
_allowAutoDisconnect = !_allowAutoDisconnect;
|
||||
String value =
|
||||
bool2option('allow-auto-disconnect', _allowAutoDisconnect);
|
||||
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value);
|
||||
setState(() {});
|
||||
},
|
||||
onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
|
||||
? null
|
||||
: (_) async {
|
||||
_allowAutoDisconnect = !_allowAutoDisconnect;
|
||||
String value = bool2option(
|
||||
kOptionAllowAutoDisconnect, _allowAutoDisconnect);
|
||||
await bind.mainSetOption(
|
||||
key: kOptionAllowAutoDisconnect, value: value);
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
];
|
||||
if (_hasIgnoreBattery) {
|
||||
@@ -448,33 +493,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||
}));
|
||||
|
||||
return SettingsList(
|
||||
final disabledSettings = bind.isDisableSettings();
|
||||
final settings = SettingsList(
|
||||
sections: [
|
||||
SettingsSection(
|
||||
title: Text(translate('Account')),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||
leading: Icon(Icons.person),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
customClientSection,
|
||||
if (!bind.isDisableAccount())
|
||||
SettingsSection(
|
||||
title: Text(translate('Account')),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||
leading: Icon(Icons.person),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||
SettingsTile(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}),
|
||||
if (!disabledSettings)
|
||||
SettingsTile(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}),
|
||||
SettingsTile(
|
||||
title: Text(translate('Language')),
|
||||
leading: Icon(Icons.translate),
|
||||
@@ -495,6 +544,26 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
)
|
||||
]),
|
||||
if (isAndroid)
|
||||
SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable hardware codec')),
|
||||
initialValue: _enableHardwareCodec,
|
||||
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionEnableHwcodec,
|
||||
value: v ? defaultOptionYes : "N");
|
||||
final newValue =
|
||||
await bind.mainGetOption(key: kOptionEnableHwcodec) !=
|
||||
"N";
|
||||
setState(() {
|
||||
_enableHardwareCodec = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
]),
|
||||
if (isAndroid && !outgoingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate("Recording")),
|
||||
tiles: [
|
||||
@@ -506,30 +575,33 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
builder: (ctx, data) => Offstage(
|
||||
offstage: !data.hasData,
|
||||
child: Text("${translate("Directory")}: ${data.data}")),
|
||||
future: bind.mainDefaultVideoSaveDirectory()),
|
||||
future: bind.mainVideoSaveDirectory(root: false)),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: "allow-auto-record-incoming",
|
||||
value: bool2option("allow-auto-record-incoming", v));
|
||||
final newValue = option2bool(
|
||||
'allow-auto-record-incoming',
|
||||
await bind.mainGetOption(
|
||||
key: 'allow-auto-record-incoming'));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionAllowAutoRecordIncoming,
|
||||
value:
|
||||
bool2option(kOptionAllowAutoRecordIncoming, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordIncoming,
|
||||
await bind.mainGetOption(
|
||||
key: kOptionAllowAutoRecordIncoming));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isAndroid)
|
||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate("Share Screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
defaultDisplaySection(),
|
||||
if (isAndroid)
|
||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate("Enhancements")),
|
||||
tiles: enhancementsTiles,
|
||||
@@ -578,6 +650,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
),
|
||||
],
|
||||
);
|
||||
return settings;
|
||||
}
|
||||
|
||||
Future<bool> canStartOnBoot() async {
|
||||
@@ -617,29 +690,32 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||
try {
|
||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||
var lang = bind.mainGetLocalOption(key: "lang");
|
||||
var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
||||
dialogManager.show((setState, close, context) {
|
||||
setLang(v) async {
|
||||
if (lang != v) {
|
||||
setState(() {
|
||||
lang = v;
|
||||
});
|
||||
await bind.mainSetLocalOption(key: "lang", value: v);
|
||||
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
|
||||
HomePage.homeKey.currentState?.refreshPages();
|
||||
Future.delayed(Duration(milliseconds: 200), close);
|
||||
}
|
||||
}
|
||||
|
||||
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
||||
return CustomAlertDialog(
|
||||
content: Column(
|
||||
children: [
|
||||
getRadio(Text(translate('Default')), '', lang, setLang),
|
||||
getRadio(Text(translate('Default')), defaultOptionLang, lang,
|
||||
isOptFixed ? null : setLang),
|
||||
Divider(color: MyTheme.border),
|
||||
] +
|
||||
langs.map((e) {
|
||||
final key = e[0] as String;
|
||||
final name = e[1] as String;
|
||||
return getRadio(Text(translate(name)), key, lang, setLang);
|
||||
return getRadio(Text(translate(name)), key, lang,
|
||||
isOptFixed ? null : setLang);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@@ -663,13 +739,15 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
||||
}
|
||||
}
|
||||
|
||||
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
|
||||
return CustomAlertDialog(
|
||||
content: Column(children: [
|
||||
getRadio(
|
||||
Text(translate('Light')), ThemeMode.light, themeMode, setTheme),
|
||||
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme),
|
||||
getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
|
||||
isOptFixed ? null : setTheme),
|
||||
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
|
||||
isOptFixed ? null : setTheme),
|
||||
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
||||
setTheme)
|
||||
isOptFixed ? null : setTheme)
|
||||
]),
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
@@ -757,11 +835,14 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
||||
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
||||
],
|
||||
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
|
||||
asyncSetter: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: 'view_style', value: value);
|
||||
},
|
||||
getter: () =>
|
||||
bind.mainGetUserDefaultOption(key: kOptionViewStyle),
|
||||
asyncSetter: isOptionFixed(kOptionViewStyle)
|
||||
? null
|
||||
: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: kOptionViewStyle, value: value);
|
||||
},
|
||||
),
|
||||
_getPopupDialogRadioEntry(
|
||||
title: 'Default Image Quality',
|
||||
@@ -772,16 +853,19 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
||||
],
|
||||
getter: () {
|
||||
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
|
||||
final v =
|
||||
bind.mainGetUserDefaultOption(key: kOptionImageQuality);
|
||||
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
||||
return v;
|
||||
},
|
||||
asyncSetter: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: 'image_quality', value: value);
|
||||
showCustomImageQuality.value =
|
||||
value == kRemoteImageQualityCustom;
|
||||
},
|
||||
asyncSetter: isOptionFixed(kOptionImageQuality)
|
||||
? null
|
||||
: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: kOptionImageQuality, value: value);
|
||||
showCustomImageQuality.value =
|
||||
value == kRemoteImageQualityCustom;
|
||||
},
|
||||
tail: customImageQualitySetting(),
|
||||
showTail: showCustomImageQuality,
|
||||
notCloseValue: kRemoteImageQualityCustom,
|
||||
@@ -790,11 +874,13 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
title: 'Default Codec',
|
||||
list: codecList,
|
||||
getter: () =>
|
||||
bind.mainGetUserDefaultOption(key: 'codec-preference'),
|
||||
asyncSetter: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: 'codec-preference', value: value);
|
||||
},
|
||||
bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
|
||||
asyncSetter: isOptionFixed(kOptionCodecPreference)
|
||||
? null
|
||||
: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: kOptionCodecPreference, value: value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -809,13 +895,17 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
|
||||
SettingsTile otherRow(String label, String key) {
|
||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||
final isOptFixed = isOptionFixed(key);
|
||||
return SettingsTile.switchTile(
|
||||
initialValue: value,
|
||||
title: Text(translate(label)),
|
||||
onToggle: (b) async {
|
||||
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
|
||||
setState(() {});
|
||||
},
|
||||
onToggle: isOptFixed
|
||||
? null
|
||||
: (b) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: key, value: b ? 'Y' : defaultOptionNo);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -833,7 +923,7 @@ _getPopupDialogRadioEntry({
|
||||
required String title,
|
||||
required List<_RadioEntry> list,
|
||||
required _RadioEntryGetter getter,
|
||||
required _RadioEntrySetter asyncSetter,
|
||||
required _RadioEntrySetter? asyncSetter,
|
||||
Widget? tail,
|
||||
RxBool? showTail,
|
||||
String? notCloseValue,
|
||||
@@ -853,21 +943,23 @@ _getPopupDialogRadioEntry({
|
||||
|
||||
void showDialog() async {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await asyncSetter(value);
|
||||
init();
|
||||
if (value != notCloseValue) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
final onChanged = asyncSetter == null
|
||||
? null
|
||||
: (String? value) async {
|
||||
if (value == null) return;
|
||||
await asyncSetter(value);
|
||||
init();
|
||||
if (value != notCloseValue) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Obx(
|
||||
() => Column(children: [
|
||||
...list
|
||||
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
||||
groupValue.value, (String? value) => onChanged(value)))
|
||||
groupValue.value, onChanged))
|
||||
.toList(),
|
||||
Offstage(
|
||||
offstage:
|
||||
|
||||
@@ -283,12 +283,3 @@ void setPrivacyModeDialog(
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
Future<String?> validateAsync(String value) async {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final res = await bind.mainTestIfValidServer(server: value);
|
||||
return res.isEmpty ? null : res;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../utils/http_service.dart' as http;
|
||||
import '../common.dart';
|
||||
|
||||
final syncAbOption = 'sync-ab-with-recent-sessions';
|
||||
@@ -84,7 +85,7 @@ class AbModel {
|
||||
reset() async {
|
||||
print("reset ab model");
|
||||
addressbooks.clear();
|
||||
setCurrentName('');
|
||||
_currentName.value = '';
|
||||
await bind.mainClearAb();
|
||||
listInitialized = false;
|
||||
}
|
||||
@@ -509,7 +510,8 @@ class AbModel {
|
||||
}
|
||||
|
||||
void setShouldAsync(bool v) async {
|
||||
await bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
|
||||
await bind.mainSetLocalOption(
|
||||
key: syncAbOption, value: v ? 'Y' : defaultOptionNo);
|
||||
_syncAllFromRecent = true;
|
||||
_timerCounter = 0;
|
||||
}
|
||||
@@ -548,7 +550,7 @@ class AbModel {
|
||||
}
|
||||
|
||||
trySetCurrentToLast() {
|
||||
final name = bind.getLocalFlutterOption(k: 'current-ab-name');
|
||||
final name = bind.getLocalFlutterOption(k: kOptionCurrentAbName);
|
||||
if (addressbooks.containsKey(name)) {
|
||||
_currentName.value = name;
|
||||
}
|
||||
@@ -647,6 +649,10 @@ class AbModel {
|
||||
return addressbooks.keys.toList();
|
||||
}
|
||||
|
||||
String personalAddressBookName() {
|
||||
return _personalAddressBookName;
|
||||
}
|
||||
|
||||
Future<void> setCurrentName(String name) async {
|
||||
final oldName = _currentName.value;
|
||||
if (addressbooks.containsKey(name)) {
|
||||
|
||||
@@ -527,10 +527,16 @@ class ChatModel with ChangeNotifier {
|
||||
|
||||
void onVoiceCallStarted() {
|
||||
_voiceCallStatus.value = VoiceCallStatus.connected;
|
||||
if (isAndroid) {
|
||||
parent.target?.invokeMethod("on_voice_call_started");
|
||||
}
|
||||
}
|
||||
|
||||
void onVoiceCallClosed(String reason) {
|
||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
if (isAndroid) {
|
||||
parent.target?.invokeMethod("on_voice_call_closed");
|
||||
}
|
||||
}
|
||||
|
||||
void onVoiceCallIncoming() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -10,15 +11,10 @@ import './platform_model.dart';
|
||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||
|
||||
// Feature flutter_texture_render need to be enabled if feature gpucodec is enabled.
|
||||
final useTextureRender = !isWeb &&
|
||||
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
|
||||
|
||||
class _PixelbufferTexture {
|
||||
int _textureKey = -1;
|
||||
int _display = 0;
|
||||
SessionID? _sessionId;
|
||||
final support = bind.mainHasPixelbufferTextureRender();
|
||||
bool _destroying = false;
|
||||
int? _id;
|
||||
|
||||
@@ -27,26 +23,24 @@ class _PixelbufferTexture {
|
||||
int get display => _display;
|
||||
|
||||
create(int d, SessionID sessionId, FFI ffi) {
|
||||
if (support) {
|
||||
_display = d;
|
||||
_textureKey = bind.getNextTextureKey();
|
||||
_sessionId = sessionId;
|
||||
_display = d;
|
||||
_textureKey = bind.getNextTextureKey();
|
||||
_sessionId = sessionId;
|
||||
|
||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||
_id = id;
|
||||
if (id != -1) {
|
||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||
debugPrint(
|
||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id");
|
||||
}
|
||||
});
|
||||
}
|
||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||
_id = id;
|
||||
if (id != -1) {
|
||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||
debugPrint(
|
||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy(bool unregisterTexture, FFI ffi) async {
|
||||
if (!_destroying && support && _textureKey != -1 && _sessionId != null) {
|
||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||
_destroying = true;
|
||||
if (unregisterTexture) {
|
||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
||||
@@ -101,13 +95,15 @@ class _GpuTexture {
|
||||
}
|
||||
}
|
||||
|
||||
destroy(FFI ffi) async {
|
||||
destroy(bool unregisterTexture, FFI ffi) async {
|
||||
// must stop texture render, render unregistered texture cause crash
|
||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||
_destroying = true;
|
||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
if (unregisterTexture) {
|
||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
}
|
||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||
_textureId = -1;
|
||||
_destroying = false;
|
||||
@@ -152,40 +148,36 @@ class TextureModel {
|
||||
TextureModel(this.parent);
|
||||
|
||||
setTextureType({required int display, required bool gpuTexture}) {
|
||||
debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture");
|
||||
var texture = _control[display];
|
||||
if (texture == null) {
|
||||
texture = _Control();
|
||||
_control[display] = texture;
|
||||
debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture");
|
||||
ensureControl(display);
|
||||
_control[display]?.setTextureType(gpuTexture: gpuTexture);
|
||||
// For versions that do not support multiple displays, the display parameter is always 0, need set type of current display
|
||||
final ffi = parent.target;
|
||||
if (ffi == null) return;
|
||||
if (!ffi.ffiModel.pi.isSupportMultiDisplay) {
|
||||
final currentDisplay = CurrentDisplayState.find(ffi.id).value;
|
||||
if (currentDisplay != display) {
|
||||
debugPrint(
|
||||
"setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture");
|
||||
ensureControl(currentDisplay);
|
||||
_control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture);
|
||||
}
|
||||
}
|
||||
texture.setTextureType(gpuTexture: gpuTexture);
|
||||
}
|
||||
|
||||
setRgbaTextureId({required int display, required int id}) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
ctl.setRgbaTextureId(id);
|
||||
ensureControl(display);
|
||||
_control[display]?.setRgbaTextureId(id);
|
||||
}
|
||||
|
||||
setGpuTextureId({required int display, required int id}) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
ctl.setGpuTextureId(id);
|
||||
ensureControl(display);
|
||||
_control[display]?.setGpuTextureId(id);
|
||||
}
|
||||
|
||||
RxInt getTextureId(int display) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
return ctl.textureID;
|
||||
ensureControl(display);
|
||||
return _control[display]!.textureID;
|
||||
}
|
||||
|
||||
updateCurrentDisplay(int curDisplay) {
|
||||
@@ -211,7 +203,7 @@ class TextureModel {
|
||||
_pixelbufferRenderTextures.remove(idx);
|
||||
}
|
||||
if (_gpuRenderTextures.containsKey(idx)) {
|
||||
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
||||
_gpuRenderTextures.remove(idx);
|
||||
}
|
||||
}
|
||||
@@ -238,7 +230,15 @@ class TextureModel {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
}
|
||||
for (final texture in _gpuRenderTextures.values) {
|
||||
await texture.destroy(ffi);
|
||||
await texture.destroy(closeSession, ffi);
|
||||
}
|
||||
}
|
||||
|
||||
ensureControl(int display) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../utils/http_service.dart' as http;
|
||||
|
||||
class GroupModel {
|
||||
final RxBool groupLoading = false.obs;
|
||||
@@ -26,6 +26,7 @@ class GroupModel {
|
||||
GroupModel(this.parent);
|
||||
|
||||
Future<void> pull({force = true, quiet = false}) async {
|
||||
if (bind.isDisableGroupPanel()) return;
|
||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||
if (!force && initialized) return;
|
||||
if (!quiet) {
|
||||
|
||||
@@ -4,9 +4,12 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../models/model.dart';
|
||||
@@ -21,6 +24,128 @@ const _kMouseEventDown = 'mousedown';
|
||||
const _kMouseEventUp = 'mouseup';
|
||||
const _kMouseEventMove = 'mousemove';
|
||||
|
||||
class CanvasCoords {
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
double scale = 1.0;
|
||||
double scrollX = 0;
|
||||
double scrollY = 0;
|
||||
ScrollStyle scrollStyle = ScrollStyle.scrollauto;
|
||||
Size size = Size.zero;
|
||||
|
||||
CanvasCoords();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'x': x,
|
||||
'y': y,
|
||||
'scale': scale,
|
||||
'scrollX': scrollX,
|
||||
'scrollY': scrollY,
|
||||
'scrollStyle':
|
||||
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
|
||||
'size': {
|
||||
'w': size.width,
|
||||
'h': size.height,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static CanvasCoords fromJson(Map<String, dynamic> json) {
|
||||
final model = CanvasCoords();
|
||||
model.x = json['x'];
|
||||
model.y = json['y'];
|
||||
model.scale = json['scale'];
|
||||
model.scrollX = json['scrollX'];
|
||||
model.scrollY = json['scrollY'];
|
||||
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
|
||||
? ScrollStyle.scrollauto
|
||||
: ScrollStyle.scrollbar;
|
||||
model.size = Size(json['size']['w'], json['size']['h']);
|
||||
return model;
|
||||
}
|
||||
|
||||
static CanvasCoords fromCanvasModel(CanvasModel model) {
|
||||
final coords = CanvasCoords();
|
||||
coords.x = model.x;
|
||||
coords.y = model.y;
|
||||
coords.scale = model.scale;
|
||||
coords.scrollX = model.scrollX;
|
||||
coords.scrollY = model.scrollY;
|
||||
coords.scrollStyle = model.scrollStyle;
|
||||
coords.size = model.size;
|
||||
return coords;
|
||||
}
|
||||
}
|
||||
|
||||
class CursorCoords {
|
||||
Offset offset = Offset.zero;
|
||||
|
||||
CursorCoords();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'offset_x': offset.dx,
|
||||
'offset_y': offset.dy,
|
||||
};
|
||||
}
|
||||
|
||||
static CursorCoords fromJson(Map<String, dynamic> json) {
|
||||
final model = CursorCoords();
|
||||
model.offset = Offset(json['offset_x'], json['offset_y']);
|
||||
return model;
|
||||
}
|
||||
|
||||
static CursorCoords fromCursorModel(CursorModel model) {
|
||||
final coords = CursorCoords();
|
||||
coords.offset = model.offset;
|
||||
return coords;
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteWindowCoords {
|
||||
RemoteWindowCoords(
|
||||
this.windowRect, this.canvas, this.cursor, this.remoteRect);
|
||||
Rect windowRect;
|
||||
CanvasCoords canvas;
|
||||
CursorCoords cursor;
|
||||
Rect remoteRect;
|
||||
Offset relativeOffset = Offset.zero;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'canvas': canvas.toJson(),
|
||||
'cursor': cursor.toJson(),
|
||||
'windowRect': rectToJson(windowRect),
|
||||
'remoteRect': rectToJson(remoteRect),
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> rectToJson(Rect r) {
|
||||
return {
|
||||
'l': r.left,
|
||||
't': r.top,
|
||||
'w': r.width,
|
||||
'h': r.height,
|
||||
};
|
||||
}
|
||||
|
||||
static Rect rectFromJson(Map<String, dynamic> json) {
|
||||
return Rect.fromLTWH(
|
||||
json['l'],
|
||||
json['t'],
|
||||
json['w'],
|
||||
json['h'],
|
||||
);
|
||||
}
|
||||
|
||||
RemoteWindowCoords.fromJson(Map<String, dynamic> json)
|
||||
: windowRect = rectFromJson(json['windowRect']),
|
||||
canvas = CanvasCoords.fromJson(json['canvas']),
|
||||
cursor = CursorCoords.fromJson(json['cursor']),
|
||||
remoteRect = rectFromJson(json['remoteRect']);
|
||||
}
|
||||
|
||||
extension ToString on MouseButtons {
|
||||
String get value {
|
||||
switch (this) {
|
||||
@@ -188,32 +313,29 @@ class InputModel {
|
||||
int _lastButtons = 0;
|
||||
Offset lastMousePos = Offset.zero;
|
||||
|
||||
bool _queryOtherWindowCoords = false;
|
||||
Rect? _windowRect;
|
||||
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
||||
|
||||
late final SessionID sessionId;
|
||||
|
||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||
String get id => parent.target?.id ?? '';
|
||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
|
||||
InputModel(this.parent) {
|
||||
sessionId = parent.target!.sessionId;
|
||||
|
||||
// It is ok to call updateKeyboardMode() directly.
|
||||
// Because `bind` is initialized in `PlatformFFI.init()` which is called very early.
|
||||
// But we still wrap it in a Future.delayed() to make it more clear.
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
updateKeyboardMode();
|
||||
});
|
||||
}
|
||||
|
||||
// This function must be called after the peer info is received.
|
||||
// Because `sessionGetKeyboardMode` relies on the peer version.
|
||||
updateKeyboardMode() async {
|
||||
// * Currently mobile does not enable map mode
|
||||
if (isDesktop || isWebDesktop) {
|
||||
if (keyboardMode.isEmpty) {
|
||||
keyboardMode =
|
||||
await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
|
||||
kKeyLegacyMode;
|
||||
}
|
||||
keyboardMode = await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
|
||||
kKeyLegacyMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,6 +738,9 @@ class InputModel {
|
||||
void onPointDownImage(PointerDownEvent e) {
|
||||
debugPrint("onPointDownImage ${e.kind}");
|
||||
_stopFling = true;
|
||||
if (isDesktop) _queryOtherWindowCoords = true;
|
||||
_remoteWindowCoords = [];
|
||||
_windowRect = null;
|
||||
if (isViewOnly) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (isPhysicalMouse.value) {
|
||||
@@ -628,6 +753,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
void onPointUpImage(PointerUpEvent e) {
|
||||
if (isDesktop) _queryOtherWindowCoords = false;
|
||||
if (isViewOnly) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (isPhysicalMouse.value) {
|
||||
@@ -638,11 +764,37 @@ class InputModel {
|
||||
void onPointMoveImage(PointerMoveEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_queryOtherWindowCoords) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
||||
});
|
||||
_queryOtherWindowCoords = false;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
|
||||
List<RemoteWindowCoords> remoteWindowCoords) async {
|
||||
final coords =
|
||||
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
|
||||
final wc = WindowController.fromWindowId(kWindowId!);
|
||||
try {
|
||||
final frame = await wc.getFrame();
|
||||
for (final c in coords) {
|
||||
c.relativeOffset = Offset(
|
||||
c.windowRect.left - frame.left, c.windowRect.top - frame.top);
|
||||
remoteWindowCoords.add(c);
|
||||
}
|
||||
return frame;
|
||||
} catch (e) {
|
||||
// Unreachable code
|
||||
debugPrint("Failed to get frame of window $kWindowId, it may be hidden");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void onPointerSignalImage(PointerSignalEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (e is PointerScrollEvent) {
|
||||
@@ -843,43 +995,107 @@ class InputModel {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
}) {
|
||||
y -= CanvasModel.topToEdge;
|
||||
x -= CanvasModel.leftToEdge;
|
||||
final canvasModel = parent.target!.canvasModel;
|
||||
final ffiModel = parent.target!.ffiModel;
|
||||
CanvasCoords canvas =
|
||||
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
|
||||
Rect? rect = ffiModel.rect;
|
||||
|
||||
if (isMove) {
|
||||
canvasModel.moveDesktopMouse(x, y);
|
||||
if (_remoteWindowCoords.isNotEmpty &&
|
||||
_windowRect != null &&
|
||||
!_isInCurrentWindow(x, y)) {
|
||||
final coords =
|
||||
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
|
||||
if (coords != null) {
|
||||
isMove = false;
|
||||
canvas = coords.canvas;
|
||||
rect = coords.remoteRect;
|
||||
x -= coords.relativeOffset.dx / devicePixelRatio;
|
||||
y -= coords.relativeOffset.dy / devicePixelRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final nearThr = 3;
|
||||
var nearRight = (canvasModel.size.width - x) < nearThr;
|
||||
var nearBottom = (canvasModel.size.height - y) < nearThr;
|
||||
final rect = ffiModel.rect;
|
||||
y -= CanvasModel.topToEdge;
|
||||
x -= CanvasModel.leftToEdge;
|
||||
if (isMove) {
|
||||
parent.target!.canvasModel.moveDesktopMouse(x, y);
|
||||
}
|
||||
|
||||
return _handlePointerDevicePos(
|
||||
kind,
|
||||
x,
|
||||
y,
|
||||
isMove,
|
||||
canvas,
|
||||
rect,
|
||||
evtType,
|
||||
onExit: onExit,
|
||||
buttons: buttons,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isInCurrentWindow(double x, double y) {
|
||||
final w = _windowRect!.width / devicePixelRatio;
|
||||
final h = _windowRect!.width / devicePixelRatio;
|
||||
return x >= 0 && y >= 0 && x <= w && y <= h;
|
||||
}
|
||||
|
||||
static RemoteWindowCoords? findRemoteCoords(double x, double y,
|
||||
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
|
||||
x *= devicePixelRatio;
|
||||
y *= devicePixelRatio;
|
||||
for (final c in remoteWindowCoords) {
|
||||
if (x >= c.relativeOffset.dx &&
|
||||
y >= c.relativeOffset.dy &&
|
||||
x <= c.relativeOffset.dx + c.windowRect.width &&
|
||||
y <= c.relativeOffset.dy + c.windowRect.height) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Point? _handlePointerDevicePos(
|
||||
String kind,
|
||||
double x,
|
||||
double y,
|
||||
bool moveInCanvas,
|
||||
CanvasCoords canvas,
|
||||
Rect? rect,
|
||||
String evtType, {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
}) {
|
||||
if (rect == null) {
|
||||
return null;
|
||||
}
|
||||
final imageWidth = rect.width * canvasModel.scale;
|
||||
final imageHeight = rect.height * canvasModel.scale;
|
||||
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
|
||||
x += imageWidth * canvasModel.scrollX;
|
||||
y += imageHeight * canvasModel.scrollY;
|
||||
|
||||
final nearThr = 3;
|
||||
var nearRight = (canvas.size.width - x) < nearThr;
|
||||
var nearBottom = (canvas.size.height - y) < nearThr;
|
||||
final imageWidth = rect.width * canvas.scale;
|
||||
final imageHeight = rect.height * canvas.scale;
|
||||
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
|
||||
x += imageWidth * canvas.scrollX;
|
||||
y += imageHeight * canvas.scrollY;
|
||||
|
||||
// boxed size is a center widget
|
||||
if (canvasModel.size.width > imageWidth) {
|
||||
x -= ((canvasModel.size.width - imageWidth) / 2);
|
||||
if (canvas.size.width > imageWidth) {
|
||||
x -= ((canvas.size.width - imageWidth) / 2);
|
||||
}
|
||||
if (canvasModel.size.height > imageHeight) {
|
||||
y -= ((canvasModel.size.height - imageHeight) / 2);
|
||||
if (canvas.size.height > imageHeight) {
|
||||
y -= ((canvas.size.height - imageHeight) / 2);
|
||||
}
|
||||
} else {
|
||||
x -= canvasModel.x;
|
||||
y -= canvasModel.y;
|
||||
x -= canvas.x;
|
||||
y -= canvas.y;
|
||||
}
|
||||
|
||||
x /= canvasModel.scale;
|
||||
y /= canvasModel.scale;
|
||||
if (canvasModel.scale > 0 && canvasModel.scale < 1) {
|
||||
final step = 1.0 / canvasModel.scale - 1;
|
||||
x /= canvas.scale;
|
||||
y /= canvas.scale;
|
||||
if (canvas.scale > 0 && canvas.scale < 1) {
|
||||
final step = 1.0 / canvas.scale - 1;
|
||||
if (nearRight) {
|
||||
x += step;
|
||||
}
|
||||
@@ -902,8 +1118,7 @@ class InputModel {
|
||||
evtX = x.round();
|
||||
evtY = y.round();
|
||||
} catch (e) {
|
||||
debugPrintStack(
|
||||
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
|
||||
debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ class CachedPeerData {
|
||||
Map<String, dynamic> peerInfo = {};
|
||||
List<Map<String, dynamic>> cursorDataList = [];
|
||||
Map<String, dynamic> lastCursorId = {};
|
||||
Map<String, bool> permissions = {};
|
||||
|
||||
bool secure = false;
|
||||
bool direct = false;
|
||||
|
||||
@@ -62,6 +64,7 @@ class CachedPeerData {
|
||||
'peerInfo': peerInfo,
|
||||
'cursorDataList': cursorDataList,
|
||||
'lastCursorId': lastCursorId,
|
||||
'permissions': permissions,
|
||||
'secure': secure,
|
||||
'direct': direct,
|
||||
});
|
||||
@@ -77,6 +80,9 @@ class CachedPeerData {
|
||||
data.cursorDataList.add(cursorData);
|
||||
}
|
||||
data.lastCursorId = map['lastCursorId'];
|
||||
map['permissions'].forEach((key, value) {
|
||||
data.permissions[key] = value;
|
||||
});
|
||||
data.secure = map['secure'];
|
||||
data.direct = map['direct'];
|
||||
return data;
|
||||
@@ -106,6 +112,7 @@ class FfiModel with ChangeNotifier {
|
||||
RxBool waitForImageDialogShow = true.obs;
|
||||
Timer? waitForImageTimer;
|
||||
RxBool waitForFirstImage = true.obs;
|
||||
bool isRefreshing = false;
|
||||
|
||||
Rect? get rect => _rect;
|
||||
bool get isOriginalResolutionSet =>
|
||||
@@ -116,6 +123,10 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
|
||||
|
||||
Map<String, bool> get permissions => _permissions;
|
||||
setPermissions(Map<String, bool> permissions) {
|
||||
_permissions.clear();
|
||||
_permissions.addAll(permissions);
|
||||
}
|
||||
|
||||
bool? get secure => _secure;
|
||||
|
||||
@@ -138,6 +149,7 @@ class FfiModel with ChangeNotifier {
|
||||
FfiModel(this.parent) {
|
||||
clear();
|
||||
sessionId = parent.target!.sessionId;
|
||||
cachedPeerData.permissions = _permissions;
|
||||
}
|
||||
|
||||
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
|
||||
@@ -233,7 +245,7 @@ class FfiModel with ChangeNotifier {
|
||||
handleMsgBox({
|
||||
'type': 'success',
|
||||
'title': 'Successful',
|
||||
'text': 'Connected, waiting for image...',
|
||||
'text': kMsgboxTextWaitingForImage,
|
||||
'link': '',
|
||||
}, sessionId, peerId);
|
||||
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
||||
@@ -367,16 +379,29 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
} else if (name == 'sync_peer_option') {
|
||||
_handleSyncPeerOption(evt, peerId);
|
||||
} else if (name == 'follow_current_display') {
|
||||
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||
} else if (name == 'use_texture_render') {
|
||||
_handleUseTextureRender(evt, sessionId, peerId);
|
||||
} else {
|
||||
debugPrint('Unknown event name: $name');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_handleUseTextureRender(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
||||
waitForFirstImage.value = true;
|
||||
isRefreshing = true;
|
||||
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
||||
'success', 'Successful', kMsgboxTextWaitingForImage);
|
||||
}
|
||||
|
||||
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
|
||||
final k = evt['k'];
|
||||
final v = evt['v'];
|
||||
if (k == kOptionViewOnly) {
|
||||
if (k == kOptionToggleViewOnly) {
|
||||
setViewOnly(peer, v as bool);
|
||||
} else if (k == 'keyboard_mode') {
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
@@ -440,7 +465,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) {
|
||||
updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
|
||||
final newRect = displaysRect();
|
||||
if (newRect == null) {
|
||||
return;
|
||||
@@ -559,10 +584,14 @@ class FfiModel with ChangeNotifier {
|
||||
showElevationError(sessionId, type, title, text, dialogManager);
|
||||
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
||||
} else if (text == 'Connected, waiting for image...') {
|
||||
} else if (text == kMsgboxTextWaitingForImage) {
|
||||
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
||||
} else if (title == 'Privacy mode') {
|
||||
final hasRetry = evt['hasRetry'] == 'true';
|
||||
showPrivacyFailedDialog(
|
||||
sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
} else {
|
||||
var hasRetry = evt['hasRetry'] == 'true';
|
||||
final hasRetry = evt['hasRetry'] == 'true';
|
||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
}
|
||||
}
|
||||
@@ -650,13 +679,34 @@ class FfiModel with ChangeNotifier {
|
||||
);
|
||||
waitForImageDialogShow.value = true;
|
||||
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
||||
if (waitForFirstImage.isTrue) {
|
||||
if (waitForFirstImage.isTrue && !isRefreshing) {
|
||||
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
||||
}
|
||||
});
|
||||
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
|
||||
}
|
||||
|
||||
void showPrivacyFailedDialog(
|
||||
SessionID sessionId,
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
String link,
|
||||
bool hasRetry,
|
||||
OverlayDialogManager dialogManager) {
|
||||
if (text == 'no_need_privacy_mode_no_physical_displays_tip' ||
|
||||
text == 'Enter privacy mode') {
|
||||
// There are display changes on the remote side,
|
||||
// which will cause some messages to refresh the canvas and dismiss dialogs.
|
||||
// So we add a delay here to ensure the dialog is displayed.
|
||||
Future.delayed(Duration(milliseconds: 3000), () {
|
||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
});
|
||||
} else {
|
||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
}
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(SessionID sessionId) {
|
||||
if (_rect == null) return;
|
||||
if (_rect!.width <= 0 || _rect!.height <= 0) {
|
||||
@@ -687,9 +737,14 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
|
||||
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
||||
// Because this function is asynchronous, there's an "await" in this function.
|
||||
cachedPeerData.peerInfo = {...evt};
|
||||
// Do not cache resolutions, because a new display connection have different resolutions.
|
||||
cachedPeerData.peerInfo.remove('resolutions');
|
||||
|
||||
// Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
|
||||
bind.mainLoadRecentPeers();
|
||||
@@ -722,7 +777,7 @@ class FfiModel with ChangeNotifier {
|
||||
_touchMode = true;
|
||||
} else {
|
||||
_touchMode = await bind.sessionGetOption(
|
||||
sessionId: sessionId, arg: 'touch-mode') !=
|
||||
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||
'';
|
||||
}
|
||||
if (connType == ConnType.fileTransfer) {
|
||||
@@ -742,17 +797,20 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.isNotEmpty) {
|
||||
_reconnects = 1;
|
||||
waitForFirstImage.value = true;
|
||||
isRefreshing = false;
|
||||
}
|
||||
Map<String, dynamic> features = json.decode(evt['features']);
|
||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
if (!isCache) {
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
}
|
||||
parent.target?.elevationModel.onPeerInfo(_pi);
|
||||
}
|
||||
if (connType == ConnType.defaultConn) {
|
||||
setViewOnly(
|
||||
peerId,
|
||||
bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionViewOnly));
|
||||
sessionId: sessionId, arg: kOptionToggleViewOnly));
|
||||
}
|
||||
if (connType == ConnType.defaultConn) {
|
||||
final platformAdditions = evt['platform_additions'];
|
||||
@@ -975,6 +1033,8 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
parent.target!.canvasModel
|
||||
.tryUpdateScrollStyle(Duration(milliseconds: 300), null);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -986,15 +1046,21 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (updateData.isEmpty) {
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsRustDeskVirtualDisplays);
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
|
||||
} else {
|
||||
try {
|
||||
final updateJson = json.decode(updateData) as Map<String, dynamic>;
|
||||
for (final key in updateJson.keys) {
|
||||
_pi.platformAdditions[key] = updateJson[key];
|
||||
}
|
||||
if (!updateJson.containsKey(kPlatformAdditionsVirtualDisplays)) {
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
|
||||
if (!updateJson
|
||||
.containsKey(kPlatformAdditionsRustDeskVirtualDisplays)) {
|
||||
_pi.platformAdditions
|
||||
.remove(kPlatformAdditionsRustDeskVirtualDisplays);
|
||||
}
|
||||
if (!updateJson.containsKey(kPlatformAdditionsAmyuniVirtualDisplays)) {
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode platformAdditions $e');
|
||||
@@ -1005,9 +1071,30 @@ class FfiModel with ChangeNotifier {
|
||||
json.encode(_pi.platformAdditions);
|
||||
}
|
||||
|
||||
handleFollowCurrentDisplay(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
|
||||
if (evt['display_idx'] != null) {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
return;
|
||||
}
|
||||
_pi.currentDisplay = int.parse(evt['display_idx']);
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
bind.sessionSwitchDisplay(
|
||||
isDesktop: isDesktop,
|
||||
sessionId: sessionId,
|
||||
value: Int32List.fromList([_pi.currentDisplay]),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Directly switch to the new display without waiting for the response.
|
||||
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
||||
{bool updateCursorPos = true}) {
|
||||
{bool updateCursorPos = false}) {
|
||||
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
|
||||
parent.target?.recordingModel.onClose();
|
||||
// no need to wait for the response
|
||||
@@ -1082,6 +1169,8 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
late final SessionID sessionId;
|
||||
|
||||
bool _useTextureRender = false;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
final List<Function(String)> callbacksOnFirstImage = [];
|
||||
@@ -1090,6 +1179,8 @@ class ImageModel with ChangeNotifier {
|
||||
sessionId = parent.target!.sessionId;
|
||||
}
|
||||
|
||||
get useTextureRender => _useTextureRender;
|
||||
|
||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||
|
||||
onRgba(int display, Uint8List rgba) {
|
||||
@@ -1129,11 +1220,8 @@ class ImageModel with ChangeNotifier {
|
||||
if (parent.target != null) {
|
||||
await initializeCursorAndCanvas(parent.target!);
|
||||
}
|
||||
if (parent.target?.ffiModel.isPeerAndroid ?? false) {
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: 'adaptive');
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
}
|
||||
}
|
||||
_image?.dispose();
|
||||
_image = image;
|
||||
if (image != null) notifyListeners();
|
||||
}
|
||||
@@ -1157,6 +1245,24 @@ class ImageModel with ChangeNotifier {
|
||||
final yscale = size.height / _image!.height;
|
||||
return min(xscale, yscale) / 1.5;
|
||||
}
|
||||
|
||||
updateUserTextureRender() {
|
||||
final preValue = _useTextureRender;
|
||||
_useTextureRender = isDesktop && bind.mainGetUseTextureRender();
|
||||
if (preValue != _useTextureRender) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
setUseTextureRender(bool value) {
|
||||
_useTextureRender = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void disposeImage() {
|
||||
_image?.dispose();
|
||||
_image = null;
|
||||
}
|
||||
}
|
||||
|
||||
enum ScrollStyle {
|
||||
@@ -1343,10 +1449,20 @@ class CanvasModel with ChangeNotifier {
|
||||
if (refreshMousePos) {
|
||||
parent.target?.inputModel.refreshMousePos();
|
||||
}
|
||||
if (style == kRemoteViewStyleOriginal &&
|
||||
_scrollStyle == ScrollStyle.scrollbar) {
|
||||
updateScrollPercent();
|
||||
tryUpdateScrollStyle(Duration.zero, style);
|
||||
}
|
||||
|
||||
tryUpdateScrollStyle(Duration duration, String? style) async {
|
||||
if (_scrollStyle != ScrollStyle.scrollbar) return;
|
||||
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||
if (style != kRemoteViewStyleOriginal) {
|
||||
return;
|
||||
}
|
||||
|
||||
_resetScroll();
|
||||
Future.delayed(duration, () async {
|
||||
updateScrollPercent();
|
||||
});
|
||||
}
|
||||
|
||||
updateScrollStyle() async {
|
||||
@@ -1583,13 +1699,15 @@ const _forbiddenCursorPng =
|
||||
const _defaultCursorPng =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
||||
|
||||
const kPreForbiddenCursorId = -2;
|
||||
final preForbiddenCursor = PredefinedCursor(
|
||||
png: _forbiddenCursorPng,
|
||||
id: -2,
|
||||
id: kPreForbiddenCursorId,
|
||||
);
|
||||
const kPreDefaultCursorId = -1;
|
||||
final preDefaultCursor = PredefinedCursor(
|
||||
png: _defaultCursorPng,
|
||||
id: -1,
|
||||
id: kPreDefaultCursorId,
|
||||
hotxGetter: (double w) => w / 2,
|
||||
hotyGetter: (double h) => h / 2,
|
||||
);
|
||||
@@ -1614,13 +1732,22 @@ class PredefinedCursor {
|
||||
init() {
|
||||
_image2 = img2.decodePng(base64Decode(png));
|
||||
if (_image2 != null) {
|
||||
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||
if (isWindows && id == kPreForbiddenCursorId) {
|
||||
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
||||
}
|
||||
|
||||
() async {
|
||||
final defaultImg = _image2!;
|
||||
// This function is called only one time, no need to care about the performance.
|
||||
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
||||
_image?.dispose();
|
||||
_image = await img.decodeImageFromPixels(
|
||||
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
||||
|
||||
if (_image == null) {
|
||||
print("decodeImageFromPixels failed, pre-defined cursor $id");
|
||||
return;
|
||||
}
|
||||
double scale = 1.0;
|
||||
if (isWindows) {
|
||||
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
||||
@@ -1660,6 +1787,8 @@ class CursorModel with ChangeNotifier {
|
||||
double _displayOriginX = 0;
|
||||
double _displayOriginY = 0;
|
||||
DateTime? _firstUpdateMouseTime;
|
||||
Rect? _windowRect;
|
||||
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
||||
bool gotMouseControl = true;
|
||||
DateTime _lastPeerMouse = DateTime.now()
|
||||
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
|
||||
@@ -1672,6 +1801,8 @@ class CursorModel with ChangeNotifier {
|
||||
double get x => _x - _displayOriginX;
|
||||
double get y => _y - _displayOriginY;
|
||||
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
|
||||
Offset get offset => Offset(_x, _y);
|
||||
|
||||
double get hotx => _hotx;
|
||||
@@ -1741,15 +1872,13 @@ class CursorModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
updatePan(double dx, double dy, bool touchMode) {
|
||||
updatePan(Offset delta, Offset localPosition, bool touchMode) {
|
||||
if (touchMode) {
|
||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
||||
_x += dx / scale;
|
||||
_y += dy / scale;
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
notifyListeners();
|
||||
_handleTouchMode(delta, localPosition);
|
||||
return;
|
||||
}
|
||||
double dx = delta.dx;
|
||||
double dy = delta.dy;
|
||||
if (parent.target?.imageModel.image == null) return;
|
||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
||||
dx /= scale;
|
||||
@@ -1816,6 +1945,46 @@ class CursorModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _isInCurrentWindow(double x, double y) {
|
||||
final w = _windowRect!.width / devicePixelRatio;
|
||||
final h = _windowRect!.width / devicePixelRatio;
|
||||
return x >= 0 && y >= 0 && x <= w && y <= h;
|
||||
}
|
||||
|
||||
_handleTouchMode(Offset delta, Offset localPosition) {
|
||||
bool isMoved = false;
|
||||
if (_remoteWindowCoords.isNotEmpty &&
|
||||
_windowRect != null &&
|
||||
!_isInCurrentWindow(localPosition.dx, localPosition.dy)) {
|
||||
final coords = InputModel.findRemoteCoords(localPosition.dx,
|
||||
localPosition.dy, _remoteWindowCoords, devicePixelRatio);
|
||||
if (coords != null) {
|
||||
double x2 =
|
||||
(localPosition.dx - coords.relativeOffset.dx / devicePixelRatio) /
|
||||
coords.canvas.scale;
|
||||
double y2 =
|
||||
(localPosition.dy - coords.relativeOffset.dy / devicePixelRatio) /
|
||||
coords.canvas.scale;
|
||||
x2 += coords.cursor.offset.dx;
|
||||
y2 += coords.cursor.offset.dy;
|
||||
parent.target?.inputModel.moveMouse(x2, y2);
|
||||
isMoved = true;
|
||||
}
|
||||
}
|
||||
if (!isMoved) {
|
||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
||||
_x += delta.dx / scale;
|
||||
_y += delta.dy / scale;
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
disposeImages() {
|
||||
_images.forEach((_, v) => v.item1.dispose());
|
||||
_images.clear();
|
||||
}
|
||||
|
||||
updateCursorData(Map<String, dynamic> evt) async {
|
||||
final id = int.parse(evt['id']);
|
||||
final hotx = double.parse(evt['hotx']);
|
||||
@@ -1826,7 +1995,11 @@ class CursorModel with ChangeNotifier {
|
||||
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||
final image = await img.decodeImageFromPixels(
|
||||
rgba, width, height, ui.PixelFormat.rgba8888);
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
||||
_images[id]?.item1.dispose();
|
||||
_images[id] = Tuple3(image, hotx, hoty);
|
||||
}
|
||||
|
||||
@@ -1941,7 +2114,7 @@ class CursorModel with ChangeNotifier {
|
||||
_x = -10000;
|
||||
_x = -10000;
|
||||
_image = null;
|
||||
_images.clear();
|
||||
disposeImages();
|
||||
|
||||
_clearCache();
|
||||
_cache = null;
|
||||
@@ -1955,6 +2128,18 @@ class CursorModel with ChangeNotifier {
|
||||
deleteCustomCursor(k);
|
||||
}
|
||||
}
|
||||
|
||||
trySetRemoteWindowCoords() {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
_windowRect =
|
||||
await InputModel.fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
||||
});
|
||||
}
|
||||
|
||||
clearRemoteWindowCoords() {
|
||||
_windowRect = null;
|
||||
_remoteWindowCoords.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class QualityMonitorData {
|
||||
@@ -2172,6 +2357,7 @@ class FFI {
|
||||
/// Mobile reuse FFI
|
||||
void mobileReset() {
|
||||
ffiModel.waitForFirstImage.value = true;
|
||||
ffiModel.isRefreshing = false;
|
||||
ffiModel.waitForImageDialogShow.value = true;
|
||||
ffiModel.waitForImageTimer?.cancel();
|
||||
ffiModel.waitForImageTimer = null;
|
||||
@@ -2238,7 +2424,7 @@ class FFI {
|
||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||
ffiModel.pi.currentDisplay = display;
|
||||
}
|
||||
if (connType == ConnType.defaultConn && useTextureRender) {
|
||||
if (isDesktop && connType == ConnType.defaultConn) {
|
||||
textureModel.updateCurrentDisplay(display ?? 0);
|
||||
}
|
||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||
@@ -2260,9 +2446,8 @@ class FFI {
|
||||
}
|
||||
}
|
||||
|
||||
final hasPixelBufferTextureRender = bind.mainHasPixelbufferTextureRender();
|
||||
imageModel.updateUserTextureRender();
|
||||
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
||||
|
||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||
// Preserved for the rgba data.
|
||||
stream.listen((message) {
|
||||
@@ -2284,8 +2469,11 @@ class FFI {
|
||||
debugPrint('Unreachable, the cached data cannot be decoded.');
|
||||
return;
|
||||
}
|
||||
ffiModel.setPermissions(data.permissions);
|
||||
await ffiModel.handleCachedPeerData(data, id);
|
||||
await sessionRefreshVideo(sessionId, ffiModel.pi);
|
||||
await bind.sessionRequestNewDisplayInitMsgs(
|
||||
sessionId: sessionId, display: ffiModel.pi.currentDisplay);
|
||||
});
|
||||
isToNewWindowNotified.value = true;
|
||||
}
|
||||
@@ -2308,7 +2496,7 @@ class FFI {
|
||||
}
|
||||
} else if (message is EventToUI_Rgba) {
|
||||
final display = message.field0;
|
||||
if (hasPixelBufferTextureRender) {
|
||||
if (imageModel.useTextureRender) {
|
||||
debugPrint("EventToUI_Rgba display:$display");
|
||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||
onEvent2UIRgba();
|
||||
@@ -2490,14 +2678,21 @@ class PeerInfo with ChangeNotifier {
|
||||
bool get isInstalled =>
|
||||
platform != kPeerPlatformWindows ||
|
||||
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
||||
List<int> get virtualDisplays => List<int>.from(
|
||||
platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []);
|
||||
List<int> get RustDeskVirtualDisplays => List<int>.from(
|
||||
platformAdditions[kPlatformAdditionsRustDeskVirtualDisplays] ?? []);
|
||||
int get amyuniVirtualDisplayCount =>
|
||||
platformAdditions[kPlatformAdditionsAmyuniVirtualDisplays] ?? 0;
|
||||
|
||||
bool get isSupportMultiDisplay =>
|
||||
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
||||
|
||||
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
||||
|
||||
bool get isRustDeskIdd =>
|
||||
platformAdditions[kPlatformAdditionsIddImpl] == 'rustdesk_idd';
|
||||
bool get isAmyuniIdd =>
|
||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||
|
||||
Display? tryGetDisplay() {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
|
||||
@@ -198,7 +198,10 @@ class PlatformFFI {
|
||||
await _ffiBind.mainDeviceId(id: id);
|
||||
await _ffiBind.mainDeviceName(name: name);
|
||||
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
||||
await _ffiBind.mainInit(appDir: _dir);
|
||||
await _ffiBind.mainInit(
|
||||
appDir: _dir,
|
||||
customClientConfig: '',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrintStack(label: 'initialize failed: $e');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -22,9 +23,6 @@ class PeerTabModel with ChangeNotifier {
|
||||
int get currentTab => _currentTab;
|
||||
int _currentTab = 0; // index in tabNames
|
||||
static const int maxTabCount = 5;
|
||||
static const String kPeerTabIndex = 'peer-tab-index';
|
||||
static const String kPeerTabOrder = 'peer-tab-order';
|
||||
static const String kPeerTabVisible = 'peer-tab-visible';
|
||||
static const List<String> tabNames = [
|
||||
'Recent sessions',
|
||||
'Favorites',
|
||||
@@ -44,7 +42,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
true,
|
||||
!isWeb,
|
||||
!(bind.isDisableAb() || bind.isDisableAccount()),
|
||||
!bind.isDisableAccount(),
|
||||
!(bind.isDisableGroupPanel() || bind.isDisableAccount()),
|
||||
]);
|
||||
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
||||
List<bool> get isVisibleEnabled => () {
|
||||
@@ -72,7 +70,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
PeerTabModel(this.parent) {
|
||||
// visible
|
||||
try {
|
||||
final option = bind.getLocalFlutterOption(k: kPeerTabVisible);
|
||||
final option = bind.getLocalFlutterOption(k: kOptionPeerTabVisible);
|
||||
if (option.isNotEmpty) {
|
||||
List<dynamic> decodeList = jsonDecode(option);
|
||||
if (decodeList.length == _isVisible.length) {
|
||||
@@ -88,7 +86,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
}
|
||||
// order
|
||||
try {
|
||||
final option = bind.getLocalFlutterOption(k: kPeerTabOrder);
|
||||
final option = bind.getLocalFlutterOption(k: kOptionPeerTabOrder);
|
||||
if (option.isNotEmpty) {
|
||||
List<dynamic> decodeList = jsonDecode(option);
|
||||
if (decodeList.length == maxTabCount) {
|
||||
@@ -112,7 +110,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
}
|
||||
// init currentTab
|
||||
_currentTab =
|
||||
int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0;
|
||||
int.tryParse(bind.getLocalFlutterOption(k: kOptionPeerTabIndex)) ?? 0;
|
||||
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
||||
_currentTab = 0;
|
||||
}
|
||||
@@ -222,7 +220,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
}
|
||||
try {
|
||||
bind.setLocalFlutterOption(
|
||||
k: kPeerTabVisible, v: jsonEncode(_isVisible));
|
||||
k: kOptionPeerTabVisible, v: jsonEncode(_isVisible));
|
||||
} catch (_) {}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -258,7 +256,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
for (int i = 0; i < list.length; i++) {
|
||||
orders[i] = list[i];
|
||||
}
|
||||
bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders));
|
||||
bind.setLocalFlutterOption(k: kOptionPeerTabOrder, v: jsonEncode(orders));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class ServerModel with ChangeNotifier {
|
||||
String get approveMode => _approveMode;
|
||||
|
||||
setVerificationMethod(String method) async {
|
||||
await bind.mainSetOption(key: "verification-method", value: method);
|
||||
await bind.mainSetOption(key: kOptionVerificationMethod, value: method);
|
||||
/*
|
||||
if (method != kUsePermanentPassword) {
|
||||
await bind.mainSetOption(
|
||||
@@ -99,7 +99,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
setApproveMode(String mode) async {
|
||||
await bind.mainSetOption(key: 'approve-mode', value: mode);
|
||||
await bind.mainSetOption(key: kOptionApproveMode, value: mode);
|
||||
/*
|
||||
if (mode != 'password') {
|
||||
await bind.mainSetOption(
|
||||
@@ -125,8 +125,8 @@ class ServerModel with ChangeNotifier {
|
||||
/*
|
||||
// initital _hideCm at startup
|
||||
final verificationMethod =
|
||||
bind.mainGetOptionSync(key: "verification-method");
|
||||
final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
|
||||
bind.mainGetOptionSync(key: kOptionVerificationMethod);
|
||||
final approveMode = bind.mainGetOptionSync(key: kOptionApproveMode);
|
||||
_hideCm = option2bool(
|
||||
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
||||
if (!(approveMode == 'password' &&
|
||||
@@ -187,18 +187,19 @@ class ServerModel with ChangeNotifier {
|
||||
if (androidVersion < 30 ||
|
||||
!await AndroidPermissionManager.check(kRecordAudio)) {
|
||||
_audioOk = false;
|
||||
bind.mainSetOption(key: "enable-audio", value: "N");
|
||||
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||
} else {
|
||||
final audioOption = await bind.mainGetOption(key: 'enable-audio');
|
||||
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||
_audioOk = audioOption.isEmpty;
|
||||
}
|
||||
|
||||
// file
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
_fileOk = false;
|
||||
bind.mainSetOption(key: "enable-file-transfer", value: "N");
|
||||
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
|
||||
} else {
|
||||
final fileOption = await bind.mainGetOption(key: 'enable-file-transfer');
|
||||
final fileOption =
|
||||
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||
_fileOk = fileOption.isEmpty;
|
||||
}
|
||||
|
||||
@@ -209,10 +210,10 @@ class ServerModel with ChangeNotifier {
|
||||
var update = false;
|
||||
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
||||
final verificationMethod =
|
||||
await bind.mainGetOption(key: "verification-method");
|
||||
await bind.mainGetOption(key: kOptionVerificationMethod);
|
||||
final temporaryPasswordLength =
|
||||
await bind.mainGetOption(key: "temporary-password-length");
|
||||
final approveMode = await bind.mainGetOption(key: 'approve-mode');
|
||||
final approveMode = await bind.mainGetOption(key: kOptionApproveMode);
|
||||
/*
|
||||
var hideCm = option2bool(
|
||||
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
||||
@@ -283,7 +284,8 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
_audioOk = !_audioOk;
|
||||
bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N');
|
||||
bind.mainSetOption(
|
||||
key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -302,7 +304,9 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
_fileOk = !_fileOk;
|
||||
bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N');
|
||||
bind.mainSetOption(
|
||||
key: kOptionEnableFileTransfer,
|
||||
value: _fileOk ? defaultOptionYes : 'N');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -312,7 +316,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
if (_inputOk) {
|
||||
parent.target?.invokeMethod("stop_input");
|
||||
bind.mainSetOption(key: "enable-keyboard", value: 'N');
|
||||
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||
} else {
|
||||
if (parent.target != null) {
|
||||
/// the result of toggle-on depends on user actions in the settings page.
|
||||
@@ -322,6 +326,20 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkRequestNotificationPermission() async {
|
||||
debugPrint("androidVersion $androidVersion");
|
||||
if (androidVersion < 33) {
|
||||
return true;
|
||||
}
|
||||
if (await AndroidPermissionManager.check(kAndroid13Notification)) {
|
||||
debugPrint("notification permission already granted");
|
||||
return true;
|
||||
}
|
||||
var res = await AndroidPermissionManager.request(kAndroid13Notification);
|
||||
debugPrint("notification permission request result: $res");
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Toggle the screen sharing service.
|
||||
toggleService() async {
|
||||
if (_isStart) {
|
||||
@@ -348,6 +366,10 @@ class ServerModel with ChangeNotifier {
|
||||
stopService();
|
||||
}
|
||||
} else {
|
||||
await checkRequestNotificationPermission();
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||
}
|
||||
final res = await parent.target?.dialogManager
|
||||
.show<bool>((setState, close, context) {
|
||||
submit() => close(true);
|
||||
@@ -430,7 +452,9 @@ class ServerModel with ChangeNotifier {
|
||||
break;
|
||||
case "input":
|
||||
if (_inputOk != value) {
|
||||
bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N');
|
||||
bind.mainSetOption(
|
||||
key: kOptionEnableKeyboard,
|
||||
value: value ? defaultOptionYes : 'N');
|
||||
}
|
||||
_inputOk = value;
|
||||
break;
|
||||
@@ -535,37 +559,60 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void showLoginDialog(Client client) {
|
||||
showClientDialog(
|
||||
client,
|
||||
client.isFileTransfer ? "File Connection" : "Screen Connection",
|
||||
'Do you accept?',
|
||||
'android_new_connection_tip',
|
||||
() => sendLoginResponse(client, false),
|
||||
() => sendLoginResponse(client, true),
|
||||
);
|
||||
}
|
||||
|
||||
handleVoiceCall(Client client, bool accept) {
|
||||
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
|
||||
}
|
||||
|
||||
showVoiceCallDialog(Client client) {
|
||||
showClientDialog(
|
||||
client,
|
||||
'Voice call',
|
||||
'Do you accept?',
|
||||
'android_new_voice_call_tip',
|
||||
() => handleVoiceCall(client, false),
|
||||
() => handleVoiceCall(client, true),
|
||||
);
|
||||
}
|
||||
|
||||
showClientDialog(Client client, String title, String contentTitle,
|
||||
String content, VoidCallback onCancel, VoidCallback onSubmit) {
|
||||
parent.target?.dialogManager.show((setState, close, context) {
|
||||
cancel() {
|
||||
sendLoginResponse(client, false);
|
||||
onCancel();
|
||||
close();
|
||||
}
|
||||
|
||||
submit() {
|
||||
sendLoginResponse(client, true);
|
||||
onSubmit();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title:
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(translate(
|
||||
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
icon: const Icon(Icons.close))
|
||||
Text(translate(title)),
|
||||
IconButton(onPressed: close, icon: const Icon(Icons.close))
|
||||
]),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("Do you accept?")),
|
||||
Text(translate(contentTitle)),
|
||||
ClientInfo(client),
|
||||
Text(
|
||||
translate("android_new_connection_tip"),
|
||||
translate(content),
|
||||
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
@@ -661,10 +708,14 @@ class ServerModel with ChangeNotifier {
|
||||
_clients[index].inVoiceCall = client.inVoiceCall;
|
||||
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
||||
if (client.incomingVoiceCall) {
|
||||
// Has incoming phone call, let's set the window on top.
|
||||
Future.delayed(Duration.zero, () {
|
||||
windowOnTop(null);
|
||||
});
|
||||
if (isAndroid) {
|
||||
showVoiceCallDialog(client);
|
||||
} else {
|
||||
// Has incoming phone call, let's set the window on top.
|
||||
Future.delayed(Duration.zero, () {
|
||||
windowOnTop(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -14,12 +14,11 @@ class StateGlobal {
|
||||
bool _isMinimized = false;
|
||||
final RxBool isMaximized = false.obs;
|
||||
final RxBool _showTabBar = true.obs;
|
||||
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
|
||||
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
|
||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||
final RxBool showRemoteToolBar = false.obs;
|
||||
final svcStatus = SvcStatus.notReady.obs;
|
||||
// Only used for macOS
|
||||
bool? closeOnFullscreen;
|
||||
final RxBool isFocused = false.obs;
|
||||
|
||||
String _inputSource = '';
|
||||
|
||||
@@ -55,8 +54,7 @@ class StateGlobal {
|
||||
if (!_fullscreen.isTrue) {
|
||||
if (isMaximized.value != v) {
|
||||
isMaximized.value = v;
|
||||
_resizeEdgeSize.value =
|
||||
isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
|
||||
refreshResizeEdgeSize();
|
||||
}
|
||||
if (!isMacOS) {
|
||||
_windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
|
||||
@@ -70,11 +68,7 @@ class StateGlobal {
|
||||
if (_fullscreen.value != v) {
|
||||
_fullscreen.value = v;
|
||||
_showTabBar.value = !_fullscreen.value;
|
||||
_resizeEdgeSize.value = fullscreen.isTrue
|
||||
? kFullScreenEdgeSize
|
||||
: isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: kWindowEdgeSize;
|
||||
refreshResizeEdgeSize();
|
||||
print(
|
||||
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||
@@ -95,6 +89,12 @@ class StateGlobal {
|
||||
}
|
||||
}
|
||||
|
||||
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
|
||||
? kFullScreenEdgeSize
|
||||
: isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: windowEdgeSize;
|
||||
|
||||
String getInputSource({bool force = false}) {
|
||||
if (force || _inputSource.isEmpty) {
|
||||
_inputSource = bind.mainGetInputSource();
|
||||
@@ -112,4 +112,5 @@ class StateGlobal {
|
||||
static final StateGlobal instance = StateGlobal._();
|
||||
}
|
||||
|
||||
// This final variable is initialized when the first time it is accessed.
|
||||
final stateGlobal = StateGlobal.instance;
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../common.dart';
|
||||
import '../utils/http_service.dart' as http;
|
||||
import 'model.dart';
|
||||
import 'platform_model.dart';
|
||||
|
||||
@@ -136,7 +136,6 @@ class UserModel {
|
||||
Future<LoginResponse> login(LoginRequest loginRequest) async {
|
||||
final url = await bind.mainGetApiServer();
|
||||
final resp = await http.post(Uri.parse('$url/api/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(loginRequest.toJson()));
|
||||
|
||||
final Map<String, dynamic> body;
|
||||
|
||||
115
flutter/lib/utils/http_service.dart
Normal file
115
flutter/lib/utils/http_service.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/platform_model.dart';
|
||||
export 'package:http/http.dart' show Response;
|
||||
|
||||
enum HttpMethod { get, post, put, delete }
|
||||
|
||||
class HttpService {
|
||||
Future<http.Response> sendRequest(
|
||||
Uri url,
|
||||
HttpMethod method, {
|
||||
Map<String, String>? headers,
|
||||
dynamic body,
|
||||
}) async {
|
||||
headers ??= {'Content-Type': 'application/json'};
|
||||
|
||||
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
|
||||
final isProxy = await bind.mainGetProxyStatus();
|
||||
|
||||
if (!isProxy) {
|
||||
return await _pollFultterHttp(url, method, headers: headers, body: body);
|
||||
}
|
||||
|
||||
String headersJson = jsonEncode(headers);
|
||||
String methodName = method.toString().split('.').last;
|
||||
await bind.mainHttpRequest(
|
||||
url: url.toString(),
|
||||
method: methodName.toLowerCase(),
|
||||
body: body,
|
||||
header: headersJson);
|
||||
|
||||
var resJson = await _pollForResponse(url.toString());
|
||||
return _parseHttpResponse(resJson);
|
||||
}
|
||||
|
||||
Future<http.Response> _pollFultterHttp(
|
||||
Uri url,
|
||||
HttpMethod method, {
|
||||
Map<String, String>? headers,
|
||||
dynamic body,
|
||||
}) async {
|
||||
var response = http.Response('', 400);
|
||||
|
||||
switch (method) {
|
||||
case HttpMethod.get:
|
||||
response = await http.get(url, headers: headers);
|
||||
break;
|
||||
case HttpMethod.post:
|
||||
response = await http.post(url, headers: headers, body: body);
|
||||
break;
|
||||
case HttpMethod.put:
|
||||
response = await http.put(url, headers: headers, body: body);
|
||||
break;
|
||||
case HttpMethod.delete:
|
||||
response = await http.delete(url, headers: headers, body: body);
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unsupported HTTP method');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<String> _pollForResponse(String url) async {
|
||||
String? responseJson = " ";
|
||||
while (responseJson == " ") {
|
||||
responseJson = await bind.mainGetHttpStatus(url: url);
|
||||
if (responseJson == null) {
|
||||
throw Exception('The HTTP request failed');
|
||||
}
|
||||
if (responseJson == " ") {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
return responseJson!;
|
||||
}
|
||||
|
||||
http.Response _parseHttpResponse(String responseJson) {
|
||||
try {
|
||||
var parsedJson = jsonDecode(responseJson);
|
||||
String body = parsedJson['body'];
|
||||
Map<String, String> headers = {};
|
||||
for (var key in parsedJson['headers'].keys) {
|
||||
headers[key] = parsedJson['headers'][key];
|
||||
}
|
||||
int statusCode = parsedJson['status_code'];
|
||||
return http.Response(body, statusCode, headers: headers);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to parse response: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
|
||||
return await HttpService().sendRequest(url, HttpMethod.get, headers: headers);
|
||||
}
|
||||
|
||||
Future<http.Response> post(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
return await HttpService()
|
||||
.sendRequest(url, HttpMethod.post, body: body, headers: headers);
|
||||
}
|
||||
|
||||
Future<http.Response> put(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
return await HttpService()
|
||||
.sendRequest(url, HttpMethod.put, body: body, headers: headers);
|
||||
}
|
||||
|
||||
Future<http.Response> delete(Uri url,
|
||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||
return await HttpService()
|
||||
.sendRequest(url, HttpMethod.delete, body: body, headers: headers);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
|
||||
Future<ui.Image> decodeImageFromPixels(
|
||||
Future<ui.Image?> decodeImageFromPixels(
|
||||
Uint8List pixels,
|
||||
int width,
|
||||
int height,
|
||||
@@ -18,36 +18,74 @@ Future<ui.Image> decodeImageFromPixels(
|
||||
}) async {
|
||||
if (targetWidth != null) {
|
||||
assert(allowUpscaling || targetWidth <= width);
|
||||
if (!(allowUpscaling || targetWidth <= width)) {
|
||||
print("not allow upscaling but targetWidth > width");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (targetHeight != null) {
|
||||
assert(allowUpscaling || targetHeight <= height);
|
||||
}
|
||||
|
||||
final ui.ImmutableBuffer buffer =
|
||||
await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||
onPixelsCopied?.call();
|
||||
final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
|
||||
buffer,
|
||||
width: width,
|
||||
height: height,
|
||||
rowBytes: rowBytes,
|
||||
pixelFormat: format,
|
||||
);
|
||||
if (!allowUpscaling) {
|
||||
if (targetWidth != null && targetWidth > descriptor.width) {
|
||||
targetWidth = descriptor.width;
|
||||
}
|
||||
if (targetHeight != null && targetHeight > descriptor.height) {
|
||||
targetHeight = descriptor.height;
|
||||
if (!(allowUpscaling || targetHeight <= height)) {
|
||||
print("not allow upscaling but targetHeight > height");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final ui.Codec codec = await descriptor.instantiateCodec(
|
||||
targetWidth: targetWidth,
|
||||
targetHeight: targetHeight,
|
||||
);
|
||||
final ui.ImmutableBuffer buffer;
|
||||
try {
|
||||
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||
onPixelsCopied?.call();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ui.ImageDescriptor descriptor;
|
||||
try {
|
||||
descriptor = ui.ImageDescriptor.raw(
|
||||
buffer,
|
||||
width: width,
|
||||
height: height,
|
||||
rowBytes: rowBytes,
|
||||
pixelFormat: format,
|
||||
);
|
||||
if (!allowUpscaling) {
|
||||
if (targetWidth != null && targetWidth > descriptor.width) {
|
||||
targetWidth = descriptor.width;
|
||||
}
|
||||
if (targetHeight != null && targetHeight > descriptor.height) {
|
||||
targetHeight = descriptor.height;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("ImageDescriptor.raw failed: $e");
|
||||
buffer.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
final ui.Codec codec;
|
||||
try {
|
||||
codec = await descriptor.instantiateCodec(
|
||||
targetWidth: targetWidth,
|
||||
targetHeight: targetHeight,
|
||||
);
|
||||
} catch (e) {
|
||||
print("instantiateCodec failed: $e");
|
||||
buffer.dispose();
|
||||
descriptor.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
final ui.FrameInfo frameInfo;
|
||||
try {
|
||||
frameInfo = await codec.getNextFrame();
|
||||
} catch (e) {
|
||||
print("getNextFrame failed: $e");
|
||||
codec.dispose();
|
||||
buffer.dispose();
|
||||
descriptor.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
||||
codec.dispose();
|
||||
buffer.dispose();
|
||||
descriptor.dispose();
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
|
||||
/// must keep the order
|
||||
// ignore: constant_identifier_names
|
||||
@@ -172,7 +174,9 @@ class RustDeskMultiWindowManager {
|
||||
windowId: windowId, peerId: remoteId);
|
||||
}
|
||||
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
||||
WindowController.fromWindowId(windowId).show();
|
||||
if (methodName != kWindowEventNewRemoteDesktop) {
|
||||
WindowController.fromWindowId(windowId).show();
|
||||
}
|
||||
registerActiveWindow(windowId);
|
||||
return MultiWindowCallResult(windowId, null);
|
||||
}
|
||||
@@ -332,10 +336,10 @@ class RustDeskMultiWindowManager {
|
||||
}
|
||||
|
||||
Future<void> closeAllSubWindows() async {
|
||||
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
||||
await Future.wait(WindowType.values.map((e) => _closeWindows(e)));
|
||||
}
|
||||
|
||||
Future<void> closeWindows(WindowType type) async {
|
||||
Future<void> _closeWindows(WindowType type) async {
|
||||
if (type == WindowType.Main) {
|
||||
// skip main window, use window manager instead
|
||||
return;
|
||||
@@ -343,7 +347,7 @@ class RustDeskMultiWindowManager {
|
||||
|
||||
List<int> windows = [];
|
||||
try {
|
||||
windows = await DesktopMultiWindow.getAllSubWindowIds();
|
||||
windows = _findWindowsByType(type);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
||||
return;
|
||||
@@ -353,14 +357,9 @@ class RustDeskMultiWindowManager {
|
||||
return;
|
||||
}
|
||||
for (final wId in windows) {
|
||||
debugPrint("closing multi window: ${type.toString()}");
|
||||
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
try {
|
||||
// final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||
// if (!ids.contains(wId)) {
|
||||
// // no such window already
|
||||
// return;
|
||||
// }
|
||||
await WindowController.fromWindowId(wId).setPreventClose(false);
|
||||
await WindowController.fromWindowId(wId).close();
|
||||
_activeWindows.remove(wId);
|
||||
@@ -369,7 +368,6 @@ class RustDeskMultiWindowManager {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await _notifyActiveWindow();
|
||||
clearWindowType(type);
|
||||
}
|
||||
|
||||
@@ -402,14 +400,6 @@ class RustDeskMultiWindowManager {
|
||||
await _notifyActiveWindow();
|
||||
}
|
||||
|
||||
Future<void> destroyWindow(int windowId) async {
|
||||
await WindowController.fromWindowId(windowId).setPreventClose(false);
|
||||
await WindowController.fromWindowId(windowId).close();
|
||||
_remoteDesktopWindows.remove(windowId);
|
||||
_fileTransferWindows.remove(windowId);
|
||||
_portForwardWindows.remove(windowId);
|
||||
}
|
||||
|
||||
/// Remove active window which has [`windowId`]
|
||||
///
|
||||
/// [Availability]
|
||||
@@ -431,6 +421,39 @@ class RustDeskMultiWindowManager {
|
||||
void unregisterActiveWindowListener(AsyncCallback callback) {
|
||||
_windowActiveCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
// This function is called from the main window.
|
||||
// It will query the active remote windows to get their coords.
|
||||
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
|
||||
List<String> coords = [];
|
||||
for (final windowId in _remoteDesktopWindows) {
|
||||
if (windowId != wId) {
|
||||
if (_activeWindows.contains(windowId)) {
|
||||
final res = await DesktopMultiWindow.invokeMethod(
|
||||
windowId, kWindowEventRemoteWindowCoords, '');
|
||||
if (res != null) {
|
||||
coords.add(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
|
||||
// This function is called from one remote window.
|
||||
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
|
||||
// So we need to call the main window to get the other remote windows' coords.
|
||||
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
|
||||
List<RemoteWindowCoords> coords = [];
|
||||
// Call the main window to get the coords of other remote windows.
|
||||
String res = await DesktopMultiWindow.invokeMethod(
|
||||
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
|
||||
List<dynamic> list = jsonDecode(res);
|
||||
for (var item in list) {
|
||||
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
}
|
||||
|
||||
final rustDeskWinManager = RustDeskMultiWindowManager.instance;
|
||||
|
||||
@@ -203,12 +203,6 @@ class RustdeskImpl {
|
||||
]));
|
||||
}
|
||||
|
||||
Future<String?> sessionGetFlutterOptionByPeerId(
|
||||
{required String id, required String k, dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('getByName', ['option:flutter:peer', k]));
|
||||
}
|
||||
|
||||
int getNextTextureKey({dynamic hint}) {
|
||||
return 0;
|
||||
}
|
||||
@@ -346,6 +340,10 @@ class RustdeskImpl {
|
||||
return mode == kKeyLegacyMode;
|
||||
}
|
||||
|
||||
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> sessionSetCustomImageQuality(
|
||||
{required UuidValue sessionId, required int value, dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
@@ -676,7 +674,8 @@ class RustdeskImpl {
|
||||
return Future(() => js.context.callMethod('setByName', ['options', json]));
|
||||
}
|
||||
|
||||
Future<String> mainTestIfValidServer({required String server, dynamic hint}) {
|
||||
Future<String> mainTestIfValidServer(
|
||||
{required String server, required bool testWithProxy, dynamic hint}) {
|
||||
// TODO: implement
|
||||
return Future.value('');
|
||||
}
|
||||
@@ -770,6 +769,24 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<bool> mainGetProxyStatus({dynamic hint}) {
|
||||
return Future(() => false);
|
||||
}
|
||||
|
||||
Future<void> mainHttpRequest({
|
||||
required String url,
|
||||
required String method,
|
||||
String? body,
|
||||
required String header,
|
||||
dynamic hint,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<String?> mainGetHttpStatus({required String url, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
String mainGetLocalOption({required String key, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['option:local', key]);
|
||||
}
|
||||
@@ -920,7 +937,7 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<String> mainDefaultVideoSaveDirectory({dynamic hint}) {
|
||||
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -1052,7 +1069,7 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool mainHasGpucodec({dynamic hint}) {
|
||||
bool mainHasVram({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -1397,10 +1414,6 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool mainHasPixelbufferTextureRender({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mainHasFileClipboard({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
@@ -1447,6 +1460,10 @@ class RustdeskImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDisableGroupPanel({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDisableAccount({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
@@ -1573,5 +1590,27 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> mainCheckHwcodec({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> sessionRequestNewDisplayInitMsgs(
|
||||
{required UuidValue sessionId, required int display, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<String> mainHandleWaylandScreencastRestoreToken(
|
||||
{required String key, required String value, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool mainIsOptionFixed({required String key, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool mainGetUseTextureRender({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,33 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#define RUSTDESK_LIB_PATH "librustdesk.so"
|
||||
// #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so"
|
||||
typedef bool (*RustDeskCoreMain)();
|
||||
bool gIsConnectionManager = false;
|
||||
|
||||
void print_help_install_pkg(const char* so);
|
||||
|
||||
bool flutter_rustdesk_core_main() {
|
||||
void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY);
|
||||
if (!librustdesk) {
|
||||
fprintf(stderr,"load librustdesk.so failed\n");
|
||||
return true;
|
||||
fprintf(stderr,"Failed to load \"librustdesk.so\"\n");
|
||||
char* error;
|
||||
if ((error = dlerror()) != nullptr) {
|
||||
fprintf(stderr, "%s\n", error);
|
||||
char* libmissed = strstr(error, ": cannot open shared object file: No such file or directory");
|
||||
if (libmissed != nullptr) {
|
||||
*libmissed = '\0';
|
||||
char* so = strdup(error);
|
||||
print_help_install_pkg(so);
|
||||
free(so);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main");
|
||||
char* error;
|
||||
if ((error = dlerror()) != nullptr) {
|
||||
fprintf(stderr, "error finding rustdesk_core_main: %s", error);
|
||||
return true;
|
||||
fprintf(stderr, "Program entry \"rustdesk_core_main\" is not found: %s\n", error);
|
||||
return false;
|
||||
}
|
||||
return core_main();
|
||||
}
|
||||
@@ -33,3 +45,80 @@ int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char* mgr;
|
||||
const char* search;
|
||||
} PkgMgrSearch;
|
||||
|
||||
const PkgMgrSearch g_mgrs[] = {
|
||||
{
|
||||
"apt",
|
||||
"apt-file search",
|
||||
},
|
||||
{
|
||||
"dnf",
|
||||
"dnf provides",
|
||||
},
|
||||
{
|
||||
"yum",
|
||||
"yum provides",
|
||||
},
|
||||
{
|
||||
"zypper",
|
||||
"zypper wp",
|
||||
},
|
||||
{
|
||||
"pacman",
|
||||
"pacman -Qo",
|
||||
},
|
||||
{
|
||||
NULL,
|
||||
NULL,
|
||||
},
|
||||
};
|
||||
|
||||
int is_command_exists(const char* command) {
|
||||
char* path = getenv("PATH");
|
||||
char* path_copy = strdup(path);
|
||||
char* dir = strtok(path_copy, ":");
|
||||
|
||||
while (dir != NULL) {
|
||||
char command_path[256];
|
||||
snprintf(command_path, sizeof(command_path), "%s/%s", dir, command);
|
||||
if (access(command_path, X_OK) == 0) {
|
||||
free(path_copy);
|
||||
return 1;
|
||||
}
|
||||
dir = strtok(NULL, ":");
|
||||
}
|
||||
|
||||
free(path_copy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We do not automatically search pkg
|
||||
// as the search process can be time consuming and update may be required.
|
||||
void print_help_install_pkg(const char* so)
|
||||
{
|
||||
if (strcmp(so, "libnsl.so.1") == 0) {
|
||||
const char* mgr[] = {"yum", "dnf", NULL};
|
||||
const char** m = mgr;
|
||||
while (*m != NULL) {
|
||||
if (is_command_exists(*m)) {
|
||||
fprintf(stderr, "Please run \"%s install libnsl\" to install the required package.\n", *m);
|
||||
return;
|
||||
}
|
||||
m++;
|
||||
}
|
||||
}
|
||||
|
||||
const PkgMgrSearch *mgr_search = g_mgrs;
|
||||
while (mgr_search->mgr != NULL) {
|
||||
if (is_command_exists(mgr_search->mgr) == 1) {
|
||||
fprintf(stderr, "Please run \"%s %s\" to search and install the pkg.\n", mgr_search->search, so);
|
||||
break;
|
||||
}
|
||||
mgr_search++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
// we have custom window frame
|
||||
gtk_window_set_decorated(window, FALSE);
|
||||
// try setting icon for rustdesk, which uses the system cache
|
||||
GtkIconTheme* theme = gtk_icon_theme_get_default();
|
||||
@@ -75,12 +74,7 @@ static void my_application_activate(GApplication* application) {
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
|
||||
auto border_frame = gtk_frame_new(nullptr);
|
||||
gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN);
|
||||
gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view));
|
||||
gtk_widget_show(GTK_WIDGET(border_frame));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter
|
||||
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter
|
||||
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter,hwcodec
|
||||
|
||||
@@ -335,7 +335,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
|
||||
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
|
||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@@ -849,18 +849,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -921,10 +921,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1526,10 +1526,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.4.2"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
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
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.2.4+39
|
||||
version: 1.2.5+39
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -660,7 +660,7 @@ export default class Connection {
|
||||
const defaultToggleTrue = [
|
||||
'show-remote-cursor',
|
||||
'privacy-mode',
|
||||
'enable-file-transfer',
|
||||
'enable-file-copy-paste',
|
||||
'allow_swap_key',
|
||||
];
|
||||
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
|
||||
@@ -906,7 +906,7 @@ export default class Connection {
|
||||
case "privacy-mode":
|
||||
option.privacy_mode = v2;
|
||||
break;
|
||||
case "enable-file-transfer":
|
||||
case "enable-file-copy-paste":
|
||||
option.enable_file_transfer = v2;
|
||||
break;
|
||||
case "block-input":
|
||||
@@ -933,7 +933,7 @@ export default class Connection {
|
||||
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-transfer")
|
||||
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")
|
||||
|
||||
@@ -116,15 +116,27 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) {
|
||||
is_cm_page = true;
|
||||
}
|
||||
bool is_install_page = false;
|
||||
auto installParam = std::string("--install");
|
||||
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, installParam.size(), installParam.c_str()) == 0) {
|
||||
is_install_page = true;
|
||||
}
|
||||
|
||||
command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end());
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(800, 600);
|
||||
if (!window.CreateAndShow(
|
||||
is_cm_page ? app_name + L" - Connection Manager" : app_name, origin,
|
||||
size, !is_cm_page)) {
|
||||
std::wstring window_title;
|
||||
if (is_cm_page) {
|
||||
window_title = app_name + L" - Connection Manager";
|
||||
} else if (is_install_page) {
|
||||
window_title = app_name + L" - Install";
|
||||
} else {
|
||||
window_title = app_name;
|
||||
}
|
||||
if (!window.CreateAndShow(window_title, origin, size, !is_cm_page)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
@@ -8,16 +8,15 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
flexi_logger = { version = "0.27", features = ["async"] }
|
||||
protobuf = { version = "3.3", features = ["with-bytes"] }
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
protobuf = { version = "3.4", features = ["with-bytes"] }
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["full"] }
|
||||
futures = "0.3"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
bytes = { version = "1.6", features = ["serde"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||
zstd = "0.13"
|
||||
quinn = {version = "0.9", optional = true }
|
||||
anyhow = "1.0"
|
||||
futures-util = "0.3"
|
||||
directories-next = "2.0"
|
||||
@@ -26,12 +25,12 @@ serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
lazy_static = "1.4"
|
||||
confy = { git = "https://github.com/open-trade/confy" }
|
||||
confy = { git = "https://github.com/rustdesk-org/confy" }
|
||||
dirs-next = "2.0"
|
||||
filetime = "0.2"
|
||||
sodiumoxide = "0.2"
|
||||
regex = "1.8"
|
||||
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
|
||||
tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" }
|
||||
chrono = "0.4"
|
||||
backtrace = "0.3"
|
||||
libc = "0.2"
|
||||
@@ -40,17 +39,23 @@ toml = "0.7"
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
|
||||
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
|
||||
thiserror = "1.0"
|
||||
httparse = "1.5"
|
||||
base64 = "0.22"
|
||||
url = "2.2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
machine-uid = { git = "https://github.com/21pages/machine-uid" }
|
||||
|
||||
[features]
|
||||
quic = []
|
||||
flatpak = []
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
|
||||
rustls-platform-verifier = "0.3.1"
|
||||
rustls-pki-types = "1.4"
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
tokio-native-tls ="0.3"
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen = { version = "3.3" }
|
||||
protobuf-codegen = { version = "3.4" }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi"] }
|
||||
|
||||
@@ -504,6 +504,11 @@ message Resolution {
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message DisplayResolution {
|
||||
int32 display = 1;
|
||||
Resolution resolution = 2;
|
||||
}
|
||||
|
||||
message SupportedResolutions { repeated Resolution resolutions = 1; }
|
||||
|
||||
message SwitchDisplay {
|
||||
@@ -596,7 +601,10 @@ message OptionMessage {
|
||||
BoolOption disable_keyboard = 12;
|
||||
// Position 13 is used for Resolution. Remove later.
|
||||
// Resolution custom_resolution = 13;
|
||||
BoolOption support_windows_specific_session = 14;
|
||||
// BoolOption support_windows_specific_session = 14;
|
||||
// starting from 15 please, do not use removed fields
|
||||
BoolOption follow_remote_cursor = 15;
|
||||
BoolOption follow_remote_window = 16;
|
||||
}
|
||||
|
||||
message TestDelay {
|
||||
@@ -716,6 +724,13 @@ message WindowsSessions {
|
||||
uint32 current_sid = 2;
|
||||
}
|
||||
|
||||
// Query messages from peer.
|
||||
message MessageQuery {
|
||||
// The SwitchDisplay message of the target display.
|
||||
// If the target display is not found, the message will be ignored.
|
||||
int32 switch_display = 1;
|
||||
}
|
||||
|
||||
message Misc {
|
||||
oneof union {
|
||||
ChatMessage chat_message = 4;
|
||||
@@ -736,6 +751,8 @@ message Misc {
|
||||
bool portable_service_running = 20;
|
||||
SwitchSidesRequest switch_sides_request = 21;
|
||||
SwitchBack switch_back = 22;
|
||||
// Deprecated since 1.2.4, use `change_display_resolution` (36) instead.
|
||||
// But we must keep it for compatibility when peer version < 1.2.4.
|
||||
Resolution change_resolution = 24;
|
||||
PluginRequest plugin_request = 25;
|
||||
PluginFailure plugin_failure = 26;
|
||||
@@ -748,6 +765,9 @@ message Misc {
|
||||
TogglePrivacyMode toggle_privacy_mode = 33;
|
||||
SupportedEncoding supported_encoding = 34;
|
||||
uint32 selected_sid = 35;
|
||||
DisplayResolution change_display_resolution = 36;
|
||||
MessageQuery message_query = 37;
|
||||
int32 follow_current_display = 38;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,6 @@ const SERIAL: i32 = 3;
|
||||
const PASSWORD_ENC_VERSION: &str = "00";
|
||||
const ENCRYPT_MAX_LEN: usize = 128;
|
||||
|
||||
// config2 options
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const CONFIG_OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref ORG: RwLock<String> = RwLock::new("com.carriez".to_owned());
|
||||
@@ -278,9 +274,13 @@ pub struct PeerConfig {
|
||||
#[serde(flatten)]
|
||||
pub disable_clipboard: DisableClipboard,
|
||||
#[serde(flatten)]
|
||||
pub enable_file_transfer: EnableFileTransfer,
|
||||
pub enable_file_copy_paste: EnableFileCopyPaste,
|
||||
#[serde(flatten)]
|
||||
pub show_quality_monitor: ShowQualityMonitor,
|
||||
#[serde(flatten)]
|
||||
pub follow_remote_cursor: FollowRemoteCursor,
|
||||
#[serde(flatten)]
|
||||
pub follow_remote_window: FollowRemoteWindow,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_string",
|
||||
@@ -351,8 +351,10 @@ impl Default for PeerConfig {
|
||||
direct_failures: Default::default(),
|
||||
disable_audio: Default::default(),
|
||||
disable_clipboard: Default::default(),
|
||||
enable_file_transfer: Default::default(),
|
||||
enable_file_copy_paste: Default::default(),
|
||||
show_quality_monitor: Default::default(),
|
||||
follow_remote_cursor: Default::default(),
|
||||
follow_remote_window: Default::default(),
|
||||
keyboard_mode: Default::default(),
|
||||
view_only: Default::default(),
|
||||
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
|
||||
@@ -409,9 +411,7 @@ fn patch(path: PathBuf) -> PathBuf {
|
||||
if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") {
|
||||
if user != "root" {
|
||||
let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user);
|
||||
if let Ok(output) =
|
||||
crate::platform::linux::run_cmds_trim_newline(&cmd)
|
||||
{
|
||||
if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) {
|
||||
return output.into();
|
||||
}
|
||||
return format!("/home/{user}").into();
|
||||
@@ -505,7 +505,7 @@ impl Config {
|
||||
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
|
||||
let file = Self::file_(suffix);
|
||||
if let Err(err) = store_path(file, config) {
|
||||
log::error!("Failed to store config: {}", err);
|
||||
log::error!("Failed to store {suffix} config: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,7 +912,7 @@ impl Config {
|
||||
|
||||
#[inline]
|
||||
fn purify_options(v: &mut HashMap<String, String>) {
|
||||
v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, &DEFAULT_SETTINGS, k, v));
|
||||
v.retain(|k, _| is_option_can_save(&OVERWRITE_SETTINGS, k));
|
||||
}
|
||||
|
||||
pub fn set_options(mut v: HashMap<String, String>) {
|
||||
@@ -936,7 +936,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &DEFAULT_SETTINGS, &k, &v) {
|
||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &k) {
|
||||
return;
|
||||
}
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
@@ -1014,8 +1014,30 @@ impl Config {
|
||||
config.store();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_socks_from_custom_client_advanced_settings(
|
||||
settings: &HashMap<String, String>,
|
||||
) -> Option<Socks5Server> {
|
||||
let url = settings.get(keys::OPTION_PROXY_URL)?;
|
||||
Some(Socks5Server {
|
||||
proxy: url.to_owned(),
|
||||
username: settings
|
||||
.get(keys::OPTION_PROXY_USERNAME)
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default(),
|
||||
password: settings
|
||||
.get(keys::OPTION_PROXY_PASSWORD)
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_socks() -> Option<Socks5Server> {
|
||||
CONFIG2.read().unwrap().socks.clone()
|
||||
Self::get_socks_from_custom_client_advanced_settings(&OVERWRITE_SETTINGS.read().unwrap())
|
||||
.or(CONFIG2.read().unwrap().socks.clone())
|
||||
.or(Self::get_socks_from_custom_client_advanced_settings(
|
||||
&DEFAULT_SETTINGS.read().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -1024,10 +1046,26 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn get_network_type() -> NetworkType {
|
||||
match &CONFIG2.read().unwrap().socks {
|
||||
None => NetworkType::Direct,
|
||||
Some(_) => NetworkType::ProxySocks,
|
||||
if OVERWRITE_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(keys::OPTION_PROXY_URL)
|
||||
.is_some()
|
||||
{
|
||||
return NetworkType::ProxySocks;
|
||||
}
|
||||
if CONFIG2.read().unwrap().socks.is_some() {
|
||||
return NetworkType::ProxySocks;
|
||||
}
|
||||
if DEFAULT_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(keys::OPTION_PROXY_URL)
|
||||
.is_some()
|
||||
{
|
||||
return NetworkType::ProxySocks;
|
||||
}
|
||||
NetworkType::Direct
|
||||
}
|
||||
|
||||
pub fn get() -> Config {
|
||||
@@ -1190,36 +1228,36 @@ impl PeerConfig {
|
||||
serde_field_string!(
|
||||
default_view_style,
|
||||
deserialize_view_style,
|
||||
UserDefaultConfig::read("view_style")
|
||||
UserDefaultConfig::read(keys::OPTION_VIEW_STYLE)
|
||||
);
|
||||
serde_field_string!(
|
||||
default_scroll_style,
|
||||
deserialize_scroll_style,
|
||||
UserDefaultConfig::read("scroll_style")
|
||||
UserDefaultConfig::read(keys::OPTION_SCROLL_STYLE)
|
||||
);
|
||||
serde_field_string!(
|
||||
default_image_quality,
|
||||
deserialize_image_quality,
|
||||
UserDefaultConfig::read("image_quality")
|
||||
UserDefaultConfig::read(keys::OPTION_IMAGE_QUALITY)
|
||||
);
|
||||
serde_field_string!(
|
||||
default_reverse_mouse_wheel,
|
||||
deserialize_reverse_mouse_wheel,
|
||||
UserDefaultConfig::read("reverse_mouse_wheel")
|
||||
UserDefaultConfig::read(keys::OPTION_REVERSE_MOUSE_WHEEL)
|
||||
);
|
||||
serde_field_string!(
|
||||
default_displays_as_individual_windows,
|
||||
deserialize_displays_as_individual_windows,
|
||||
UserDefaultConfig::read("displays_as_individual_windows")
|
||||
UserDefaultConfig::read(keys::OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS)
|
||||
);
|
||||
serde_field_string!(
|
||||
default_use_all_my_displays_for_the_remote_session,
|
||||
deserialize_use_all_my_displays_for_the_remote_session,
|
||||
UserDefaultConfig::read("use_all_my_displays_for_the_remote_session")
|
||||
UserDefaultConfig::read(keys::OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION)
|
||||
);
|
||||
|
||||
fn default_custom_image_quality() -> Vec<i32> {
|
||||
let f: f64 = UserDefaultConfig::read("custom_image_quality")
|
||||
let f: f64 = UserDefaultConfig::read(keys::OPTION_CUSTOM_IMAGE_QUALITY)
|
||||
.parse()
|
||||
.unwrap_or(50.0);
|
||||
vec![f as _]
|
||||
@@ -1240,12 +1278,12 @@ impl PeerConfig {
|
||||
fn default_options() -> HashMap<String, String> {
|
||||
let mut mp: HashMap<String, String> = Default::default();
|
||||
[
|
||||
"codec-preference",
|
||||
"custom-fps",
|
||||
"zoom-cursor",
|
||||
"touch-mode",
|
||||
"i444",
|
||||
"swap-left-right-mouse",
|
||||
keys::OPTION_CODEC_PREFERENCE,
|
||||
keys::OPTION_CUSTOM_FPS,
|
||||
keys::OPTION_ZOOM_CURSOR,
|
||||
keys::OPTION_TOUCH_MODE,
|
||||
keys::OPTION_I444,
|
||||
keys::OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||
]
|
||||
.map(|key| {
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
||||
@@ -1260,6 +1298,19 @@ serde_field_bool!(
|
||||
default_show_remote_cursor,
|
||||
"ShowRemoteCursor::default_show_remote_cursor"
|
||||
);
|
||||
serde_field_bool!(
|
||||
FollowRemoteCursor,
|
||||
"follow_remote_cursor",
|
||||
default_follow_remote_cursor,
|
||||
"FollowRemoteCursor::default_follow_remote_cursor"
|
||||
);
|
||||
|
||||
serde_field_bool!(
|
||||
FollowRemoteWindow,
|
||||
"follow_remote_window",
|
||||
default_follow_remote_window,
|
||||
"FollowRemoteWindow::default_follow_remote_window"
|
||||
);
|
||||
serde_field_bool!(
|
||||
ShowQualityMonitor,
|
||||
"show_quality_monitor",
|
||||
@@ -1273,10 +1324,10 @@ serde_field_bool!(
|
||||
"DisableAudio::default_disable_audio"
|
||||
);
|
||||
serde_field_bool!(
|
||||
EnableFileTransfer,
|
||||
"enable_file_transfer",
|
||||
default_enable_file_transfer,
|
||||
"EnableFileTransfer::default_enable_file_transfer"
|
||||
EnableFileCopyPaste,
|
||||
"enable-file-copy-paste",
|
||||
default_enable_file_copy_paste,
|
||||
"EnableFileCopyPaste::default_enable_file_copy_paste"
|
||||
);
|
||||
serde_field_bool!(
|
||||
DisableClipboard,
|
||||
@@ -1398,10 +1449,17 @@ impl LocalConfig {
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &DEFAULT_LOCAL_SETTINGS, &k, &v) {
|
||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k) {
|
||||
return;
|
||||
}
|
||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||
// The custom client will explictly set "default" as the default language.
|
||||
let is_custom_client_default_lang = k == keys::OPTION_LANGUAGE && v == "default";
|
||||
if is_custom_client_default_lang {
|
||||
config.options.insert(k, "".to_owned());
|
||||
config.store();
|
||||
return;
|
||||
}
|
||||
let v2 = if v.is_empty() { None } else { Some(&v) };
|
||||
if v2 != config.options.get(&k) {
|
||||
if v2.is_none() {
|
||||
@@ -1414,11 +1472,13 @@ impl LocalConfig {
|
||||
}
|
||||
|
||||
pub fn get_flutter_option(k: &str) -> String {
|
||||
if let Some(v) = LOCAL_CONFIG.read().unwrap().ui_flutter.get(k) {
|
||||
v.clone()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
get_or(
|
||||
&OVERWRITE_LOCAL_SETTINGS,
|
||||
&LOCAL_CONFIG.read().unwrap().ui_flutter,
|
||||
&DEFAULT_LOCAL_SETTINGS,
|
||||
k,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn set_flutter_option(k: String, v: String) {
|
||||
@@ -1495,8 +1555,10 @@ impl LanPeers {
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct HwCodecConfig {
|
||||
#[serde(default, deserialize_with = "deserialize_hashmap_string_string")]
|
||||
pub options: HashMap<String, String>,
|
||||
#[serde(default, deserialize_with = "deserialize_string")]
|
||||
pub ram: String,
|
||||
#[serde(default, deserialize_with = "deserialize_string")]
|
||||
pub vram: String,
|
||||
}
|
||||
|
||||
impl HwCodecConfig {
|
||||
@@ -1511,25 +1573,17 @@ impl HwCodecConfig {
|
||||
pub fn clear() {
|
||||
HwCodecConfig::default().store();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct GpucodecConfig {
|
||||
#[serde(default, deserialize_with = "deserialize_string")]
|
||||
pub available: String,
|
||||
}
|
||||
|
||||
impl GpucodecConfig {
|
||||
pub fn load() -> GpucodecConfig {
|
||||
Config::load_::<GpucodecConfig>("_gpucodec")
|
||||
pub fn clear_ram() {
|
||||
let mut c = Self::load();
|
||||
c.ram = Default::default();
|
||||
c.store();
|
||||
}
|
||||
|
||||
pub fn store(&self) {
|
||||
Config::store_(self, "_gpucodec");
|
||||
}
|
||||
|
||||
pub fn clear() {
|
||||
GpucodecConfig::default().store();
|
||||
pub fn clear_vram() {
|
||||
let mut c = Self::load();
|
||||
c.vram = Default::default();
|
||||
c.store();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1561,14 +1615,19 @@ impl UserDefaultConfig {
|
||||
|
||||
pub fn get(&self, key: &str) -> String {
|
||||
match key {
|
||||
"view_style" => self.get_string(key, "original", vec!["adaptive"]),
|
||||
"scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||
"image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]),
|
||||
"codec-preference" => {
|
||||
keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]),
|
||||
keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||
keys::OPTION_IMAGE_QUALITY => {
|
||||
self.get_string(key, "balanced", vec!["best", "low", "custom"])
|
||||
}
|
||||
keys::OPTION_CODEC_PREFERENCE => {
|
||||
self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"])
|
||||
}
|
||||
"custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64),
|
||||
"custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
|
||||
keys::OPTION_CUSTOM_IMAGE_QUALITY => {
|
||||
self.get_double_string(key, 50.0, 10.0, 0xFFF as f64)
|
||||
}
|
||||
keys::OPTION_CUSTOM_FPS => self.get_double_string(key, 30.0, 5.0, 120.0),
|
||||
keys::OPTION_ENABLE_FILE_COPY_PASTE => self.get_string(key, "Y", vec!["", "N"]),
|
||||
_ => self
|
||||
.get_after(key)
|
||||
.map(|v| v.to_string())
|
||||
@@ -1577,12 +1636,7 @@ impl UserDefaultConfig {
|
||||
}
|
||||
|
||||
pub fn set(&mut self, key: String, value: String) {
|
||||
if !is_option_can_save(
|
||||
&OVERWRITE_DISPLAY_SETTINGS,
|
||||
&DEFAULT_DISPLAY_SETTINGS,
|
||||
&key,
|
||||
&value,
|
||||
) {
|
||||
if !is_option_can_save(&OVERWRITE_DISPLAY_SETTINGS, &key) {
|
||||
return;
|
||||
}
|
||||
if value.is_empty() {
|
||||
@@ -1905,18 +1959,10 @@ fn get_or(
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_option_can_save(
|
||||
overwrite: &RwLock<HashMap<String, String>>,
|
||||
defaults: &RwLock<HashMap<String, String>>,
|
||||
k: &str,
|
||||
v: &str,
|
||||
) -> bool {
|
||||
fn is_option_can_save(overwrite: &RwLock<HashMap<String, String>>, k: &str) -> bool {
|
||||
if overwrite.read().unwrap().contains_key(k) {
|
||||
return false;
|
||||
}
|
||||
if defaults.read().unwrap().get(k).map_or(false, |x| x == v) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1972,6 +2018,170 @@ pub fn is_disable_installation() -> bool {
|
||||
is_some_hard_opton("disable-installation")
|
||||
}
|
||||
|
||||
pub mod keys {
|
||||
pub const OPTION_VIEW_ONLY: &str = "view_only";
|
||||
pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar";
|
||||
pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_toolbar";
|
||||
pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor";
|
||||
pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor";
|
||||
pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window";
|
||||
pub const OPTION_ZOOM_CURSOR: &str = "zoom-cursor";
|
||||
pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor";
|
||||
pub const OPTION_DISABLE_AUDIO: &str = "disable_audio";
|
||||
pub const OPTION_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste";
|
||||
pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard";
|
||||
pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end";
|
||||
pub const OPTION_PRIVACY_MODE: &str = "privacy_mode";
|
||||
pub const OPTION_TOUCH_MODE: &str = "touch-mode";
|
||||
pub const OPTION_I444: &str = "i444";
|
||||
pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel";
|
||||
pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse";
|
||||
pub const OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS: &str = "displays_as_individual_windows";
|
||||
pub const OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION: &str =
|
||||
"use_all_my_displays_for_the_remote_session";
|
||||
pub const OPTION_VIEW_STYLE: &str = "view_style";
|
||||
pub const OPTION_SCROLL_STYLE: &str = "scroll_style";
|
||||
pub const OPTION_IMAGE_QUALITY: &str = "image_quality";
|
||||
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
|
||||
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
|
||||
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference";
|
||||
pub const OPTION_THEME: &str = "theme";
|
||||
pub const OPTION_LANGUAGE: &str = "lang";
|
||||
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
|
||||
pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right";
|
||||
pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel";
|
||||
pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs";
|
||||
pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str =
|
||||
"enable-open-new-connections-in-tabs";
|
||||
pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render";
|
||||
pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update";
|
||||
pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions";
|
||||
pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags";
|
||||
pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection";
|
||||
pub const OPTION_ACCESS_MODE: &str = "access-mode";
|
||||
pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard";
|
||||
pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard";
|
||||
pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer";
|
||||
pub const OPTION_ENABLE_AUDIO: &str = "enable-audio";
|
||||
pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel";
|
||||
pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart";
|
||||
pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session";
|
||||
pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input";
|
||||
pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification";
|
||||
pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery";
|
||||
pub const OPTION_DIRECT_SERVER: &str = "direct-server";
|
||||
pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port";
|
||||
pub const OPTION_WHITELIST: &str = "whitelist";
|
||||
pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect";
|
||||
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
||||
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
||||
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
||||
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
||||
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
||||
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
||||
pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render";
|
||||
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
||||
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
|
||||
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
|
||||
|
||||
// flutter local options
|
||||
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||
pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting";
|
||||
pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index";
|
||||
pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order";
|
||||
pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible";
|
||||
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
|
||||
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
|
||||
|
||||
// proxy settings
|
||||
// The following options are not real keys, they are just used for custom client advanced settings.
|
||||
// The real keys are in Config2::socks.
|
||||
pub const OPTION_PROXY_URL: &str = "proxy-url";
|
||||
pub const OPTION_PROXY_USERNAME: &str = "proxy-username";
|
||||
pub const OPTION_PROXY_PASSWORD: &str = "proxy-password";
|
||||
|
||||
// DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS
|
||||
pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[
|
||||
OPTION_VIEW_ONLY,
|
||||
OPTION_SHOW_MONITORS_TOOLBAR,
|
||||
OPTION_COLLAPSE_TOOLBAR,
|
||||
OPTION_SHOW_REMOTE_CURSOR,
|
||||
OPTION_FOLLOW_REMOTE_CURSOR,
|
||||
OPTION_FOLLOW_REMOTE_WINDOW,
|
||||
OPTION_ZOOM_CURSOR,
|
||||
OPTION_SHOW_QUALITY_MONITOR,
|
||||
OPTION_DISABLE_AUDIO,
|
||||
OPTION_ENABLE_FILE_COPY_PASTE,
|
||||
OPTION_DISABLE_CLIPBOARD,
|
||||
OPTION_LOCK_AFTER_SESSION_END,
|
||||
OPTION_PRIVACY_MODE,
|
||||
OPTION_TOUCH_MODE,
|
||||
OPTION_I444,
|
||||
OPTION_REVERSE_MOUSE_WHEEL,
|
||||
OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||
OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS,
|
||||
OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION,
|
||||
OPTION_VIEW_STYLE,
|
||||
OPTION_SCROLL_STYLE,
|
||||
OPTION_IMAGE_QUALITY,
|
||||
OPTION_CUSTOM_IMAGE_QUALITY,
|
||||
OPTION_CUSTOM_FPS,
|
||||
OPTION_CODEC_PREFERENCE,
|
||||
];
|
||||
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
|
||||
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
||||
OPTION_THEME,
|
||||
OPTION_LANGUAGE,
|
||||
OPTION_ENABLE_CONFIRM_CLOSING_TABS,
|
||||
OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS,
|
||||
OPTION_TEXTURE_RENDER,
|
||||
OPTION_SYNC_AB_WITH_RECENT_SESSIONS,
|
||||
OPTION_SYNC_AB_TAGS,
|
||||
OPTION_FILTER_AB_BY_INTERSECTION,
|
||||
OPTION_REMOTE_MENUBAR_DRAG_LEFT,
|
||||
OPTION_REMOTE_MENUBAR_DRAG_RIGHT,
|
||||
OPTION_HIDE_AB_TAGS_PANEL,
|
||||
OPTION_FLUTTER_REMOTE_MENUBAR_STATE,
|
||||
OPTION_FLUTTER_PEER_SORTING,
|
||||
OPTION_FLUTTER_PEER_TAB_INDEX,
|
||||
OPTION_FLUTTER_PEER_TAB_ORDER,
|
||||
OPTION_FLUTTER_PEER_TAB_VISIBLE,
|
||||
OPTION_FLUTTER_PEER_CARD_UI_TYLE,
|
||||
OPTION_FLUTTER_CURRENT_AB_NAME,
|
||||
];
|
||||
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||
pub const KEYS_SETTINGS: &[&str] = &[
|
||||
OPTION_ACCESS_MODE,
|
||||
OPTION_ENABLE_KEYBOARD,
|
||||
OPTION_ENABLE_CLIPBOARD,
|
||||
OPTION_ENABLE_FILE_TRANSFER,
|
||||
OPTION_ENABLE_AUDIO,
|
||||
OPTION_ENABLE_TUNNEL,
|
||||
OPTION_ENABLE_REMOTE_RESTART,
|
||||
OPTION_ENABLE_RECORD_SESSION,
|
||||
OPTION_ENABLE_BLOCK_INPUT,
|
||||
OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION,
|
||||
OPTION_ENABLE_LAN_DISCOVERY,
|
||||
OPTION_DIRECT_SERVER,
|
||||
OPTION_DIRECT_ACCESS_PORT,
|
||||
OPTION_WHITELIST,
|
||||
OPTION_ALLOW_AUTO_DISCONNECT,
|
||||
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
||||
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
||||
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
||||
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||
OPTION_ENABLE_ABR,
|
||||
OPTION_ALLOW_REMOVE_WALLPAPER,
|
||||
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
||||
OPTION_ALLOW_LINUX_HEADLESS,
|
||||
OPTION_ENABLE_HWCODEC,
|
||||
OPTION_APPROVE_MODE,
|
||||
OPTION_PROXY_URL,
|
||||
OPTION_PROXY_USERNAME,
|
||||
OPTION_PROXY_PASSWORD,
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2010,6 +2220,10 @@ mod tests {
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert("b".to_string(), "c".to_string());
|
||||
OVERWRITE_SETTINGS
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert("c".to_string(), "f".to_string());
|
||||
OVERWRITE_SETTINGS
|
||||
.write()
|
||||
.unwrap()
|
||||
@@ -2030,7 +2244,7 @@ mod tests {
|
||||
res.insert("d".to_owned(), "c".to_string());
|
||||
res.insert("c".to_owned(), "a".to_string());
|
||||
res.insert("f".to_owned(), "a".to_string());
|
||||
res.insert("c".to_owned(), "d".to_string());
|
||||
res.insert("e".to_owned(), "d".to_string());
|
||||
Config::purify_options(&mut res);
|
||||
assert!(res.len() == 2);
|
||||
res.insert("b".to_owned(), "c".to_string());
|
||||
@@ -2043,11 +2257,11 @@ mod tests {
|
||||
assert!(res.len() == 2);
|
||||
let res = Config::get_options();
|
||||
assert!(res["a"] == "b");
|
||||
assert!(res["c"] == "a");
|
||||
assert!(res["c"] == "f");
|
||||
assert!(res["b"] == "c");
|
||||
assert!(res["d"] == "c");
|
||||
assert!(Config::get_option("a") == "b");
|
||||
assert!(Config::get_option("c") == "a");
|
||||
assert!(Config::get_option("c") == "f");
|
||||
assert!(Config::get_option("b") == "c");
|
||||
assert!(Config::get_option("d") == "c");
|
||||
DEFAULT_SETTINGS.write().unwrap().clear();
|
||||
|
||||
@@ -16,14 +16,13 @@ use std::{
|
||||
};
|
||||
pub use tokio;
|
||||
pub use tokio_util;
|
||||
pub mod proxy;
|
||||
pub mod socket_client;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
pub use env_logger;
|
||||
pub use log;
|
||||
pub mod bytes_codec;
|
||||
#[cfg(feature = "quic")]
|
||||
pub mod quic;
|
||||
pub use anyhow::{self, bail};
|
||||
pub use futures_util;
|
||||
pub mod config;
|
||||
@@ -42,17 +41,18 @@ pub use chrono;
|
||||
pub use directories_next;
|
||||
pub use libc;
|
||||
pub mod keyboard;
|
||||
pub use base64;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use dlopen;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use machine_uid;
|
||||
pub use serde_derive;
|
||||
pub use serde_json;
|
||||
pub use sysinfo;
|
||||
pub use thiserror;
|
||||
pub use toml;
|
||||
pub use uuid;
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
pub type Stream = quic::Connection;
|
||||
#[cfg(not(feature = "quic"))]
|
||||
pub type Stream = tcp::FramedStream;
|
||||
pub type SessionID = uuid::Uuid;
|
||||
|
||||
@@ -384,7 +384,7 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(6),
|
||||
Cleanup::KeepLogFiles(31),
|
||||
)
|
||||
.start()
|
||||
.ok();
|
||||
|
||||
@@ -49,6 +49,16 @@ pub fn is_x11_or_headless() -> bool {
|
||||
const INVALID_SESSION: &str = "4294967295";
|
||||
|
||||
pub fn get_display_server() -> String {
|
||||
// Check for forced display server environment variable first
|
||||
if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") {
|
||||
return forced_display;
|
||||
}
|
||||
|
||||
// Check if `loginctl` can be called successfully
|
||||
if run_loginctl(None).is_err() {
|
||||
return DISPLAY_SERVER_X11.to_owned();
|
||||
}
|
||||
|
||||
let mut session = get_values_of_seat0(&[0])[0].clone();
|
||||
if session.is_empty() {
|
||||
// loginctl has not given the expected output. try something else.
|
||||
@@ -64,7 +74,7 @@ pub fn get_display_server() -> String {
|
||||
}
|
||||
}
|
||||
if session.is_empty() {
|
||||
"".to_owned()
|
||||
std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned())
|
||||
} else {
|
||||
get_display_server_of_session(&session)
|
||||
}
|
||||
@@ -132,9 +142,17 @@ pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, false)
|
||||
}
|
||||
|
||||
// Ignore "3 sessions listed."
|
||||
fn ignore_loginctl_line(line: &str) -> bool {
|
||||
line.contains("sessions") || line.split(" ").count() < 4
|
||||
}
|
||||
|
||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||
if let Ok(output) = run_loginctl(None) {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if line.contains("seat0") {
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
@@ -153,6 +171,9 @@ fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<Stri
|
||||
|
||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
let d = get_display_server_of_session(sid);
|
||||
@@ -213,8 +234,19 @@ pub fn run_cmds_trim_newline(cmds: &str) -> ResultType<String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flatpak"))]
|
||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
||||
if std::env::var("FLATPAK_ID").is_ok() {
|
||||
let mut l_args = String::from("loginctl");
|
||||
if let Some(a) = args.as_ref() {
|
||||
l_args = format!("{} {}", l_args, a.join(" "));
|
||||
}
|
||||
let res = std::process::Command::new("flatpak-spawn")
|
||||
.args(vec![String::from("--host"), l_args])
|
||||
.output();
|
||||
if res.is_ok() {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
let mut cmd = std::process::Command::new("loginctl");
|
||||
if let Some(a) = args {
|
||||
return cmd.args(a).output();
|
||||
@@ -222,17 +254,6 @@ fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output
|
||||
cmd.output()
|
||||
}
|
||||
|
||||
#[cfg(feature = "flatpak")]
|
||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
||||
let mut l_args = String::from("loginctl");
|
||||
if let Some(a) = args {
|
||||
l_args = format!("{} {}", l_args, a.join(" "));
|
||||
}
|
||||
std::process::Command::new("flatpak-spawn")
|
||||
.args(vec![String::from("--host"), l_args])
|
||||
.output()
|
||||
}
|
||||
|
||||
/// forever: may not work
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
|
||||
@@ -280,6 +301,9 @@ mod tests {
|
||||
fn test_run_cmds_trim_newline() {
|
||||
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
|
||||
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
|
||||
assert_eq!(run_cmds_trim_newline("whoami").unwrap() + "\n", run_cmds("whoami").unwrap());
|
||||
assert_eq!(
|
||||
run_cmds_trim_newline("whoami").unwrap() + "\n",
|
||||
run_cmds("whoami").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
561
libs/hbb_common/src/proxy.rs
Normal file
561
libs/hbb_common/src/proxy.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
use std::{
|
||||
io::Error as IoError,
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use httparse::{Error as HttpParseError, Response, EMPTY_HEADER};
|
||||
use log::info;
|
||||
use thiserror::Error as ThisError;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use tokio_native_tls::{native_tls, TlsConnector, TlsStream};
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
use tokio_rustls::{client::TlsStream, TlsConnector};
|
||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr};
|
||||
use tokio_util::codec::Framed;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
bytes_codec::BytesCodec,
|
||||
config::Socks5Server,
|
||||
tcp::{DynTcpStream, FramedStream},
|
||||
ResultType,
|
||||
};
|
||||
|
||||
#[derive(Debug, ThisError)]
|
||||
pub enum ProxyError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] IoError),
|
||||
#[error("Target parse error: {0}")]
|
||||
TargetParseError(String),
|
||||
#[error("HTTP parse error: {0}")]
|
||||
HttpParseError(#[from] HttpParseError),
|
||||
#[error("The maximum response header length is exceeded: {0}")]
|
||||
MaximumResponseHeaderLengthExceeded(usize),
|
||||
#[error("The end of file is reached")]
|
||||
EndOfFile,
|
||||
#[error("The url is error: {0}")]
|
||||
UrlBadScheme(String),
|
||||
#[error("The url parse error: {0}")]
|
||||
UrlParseScheme(#[from] url::ParseError),
|
||||
#[error("No HTTP code was found in the response")]
|
||||
NoHttpCode,
|
||||
#[error("The HTTP code is not equal 200: {0}")]
|
||||
HttpCode200(u16),
|
||||
#[error("The proxy address resolution failed: {0}")]
|
||||
AddressResolutionFailed(String),
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[error("The native tls error: {0}")]
|
||||
NativeTlsError(#[from] tokio_native_tls::native_tls::Error),
|
||||
}
|
||||
|
||||
const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096;
|
||||
/// The maximum HTTP Headers, which can be parsed.
|
||||
const MAXIMUM_RESPONSE_HEADERS: usize = 16;
|
||||
const DEFINE_TIME_OUT: u64 = 600;
|
||||
|
||||
pub trait IntoUrl {
|
||||
|
||||
// Besides parsing as a valid `Url`, the `Url` must be a valid
|
||||
// `http::Uri`, in that it makes sense to use in a network request.
|
||||
fn into_url(self) -> Result<Url, ProxyError>;
|
||||
|
||||
fn as_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl IntoUrl for Url {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
if self.has_host() {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(ProxyError::UrlBadScheme(self.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for &'a str {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
Url::parse(self)
|
||||
.map_err(ProxyError::UrlParseScheme)?
|
||||
.into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for &'a String {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
(&**self).into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for String {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
(&*self).into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Auth {
|
||||
user_name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
fn get_proxy_authorization(&self) -> String {
|
||||
format!(
|
||||
"Proxy-Authorization: Basic {}\r\n",
|
||||
self.get_basic_authorization()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_basic_authorization(&self) -> String {
|
||||
let authorization = format!("{}:{}", &self.user_name, &self.password);
|
||||
general_purpose::STANDARD.encode(authorization.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ProxyScheme {
|
||||
Http {
|
||||
auth: Option<Auth>,
|
||||
host: String,
|
||||
},
|
||||
Https {
|
||||
auth: Option<Auth>,
|
||||
host: String,
|
||||
},
|
||||
Socks5 {
|
||||
addr: SocketAddr,
|
||||
auth: Option<Auth>,
|
||||
remote_dns: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProxyScheme {
|
||||
pub fn maybe_auth(&self) -> Option<&Auth> {
|
||||
match self {
|
||||
ProxyScheme::Http { auth, .. }
|
||||
| ProxyScheme::Https { auth, .. }
|
||||
| ProxyScheme::Socks5 { auth, .. } => auth.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn socks5(addr: SocketAddr) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Socks5 {
|
||||
addr,
|
||||
auth: None,
|
||||
remote_dns: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn http(host: &str) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Http {
|
||||
auth: None,
|
||||
host: host.to_string(),
|
||||
})
|
||||
}
|
||||
fn https(host: &str) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Https {
|
||||
auth: None,
|
||||
host: host.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
|
||||
let auth = Auth {
|
||||
user_name: username.into(),
|
||||
password: password.into(),
|
||||
};
|
||||
match self {
|
||||
ProxyScheme::Http { auth: a, .. } => *a = Some(auth),
|
||||
ProxyScheme::Https { auth: a, .. } => *a = Some(auth),
|
||||
ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(url: Url) -> Result<Self, ProxyError> {
|
||||
use url::Position;
|
||||
|
||||
// Resolve URL to a host and port
|
||||
let to_addr = || {
|
||||
let addrs = url.socket_addrs(|| match url.scheme() {
|
||||
"socks5" => Some(1080),
|
||||
_ => None,
|
||||
})?;
|
||||
addrs
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost))
|
||||
};
|
||||
|
||||
let mut scheme: Self = match url.scheme() {
|
||||
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||
"socks5" => Self::socks5(to_addr()?)?,
|
||||
e => return Err(ProxyError::UrlBadScheme(e.to_string())),
|
||||
};
|
||||
|
||||
if let Some(pwd) = url.password() {
|
||||
let username = url.username();
|
||||
scheme.set_basic_auth(username, pwd);
|
||||
}
|
||||
|
||||
Ok(scheme)
|
||||
}
|
||||
pub async fn socket_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||
info!("Resolving socket address");
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await,
|
||||
ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await,
|
||||
ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_host(&self, host: &str, default_port: u16) -> Result<SocketAddr, ProxyError> {
|
||||
let (host_str, port) = match host.split_once(':') {
|
||||
Some((h, p)) => (h, p.parse::<u16>().ok()),
|
||||
None => (host, None),
|
||||
};
|
||||
let addr = (host_str, port.unwrap_or(default_port))
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?;
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
pub fn get_domain(&self) -> Result<String, ProxyError> {
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => {
|
||||
let domain = host
|
||||
.split(':')
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?;
|
||||
Ok(domain.to_string())
|
||||
}
|
||||
ProxyScheme::Socks5 { addr, .. } => match addr {
|
||||
SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()),
|
||||
SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn get_host_and_port(&self) -> Result<String, ProxyError> {
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)),
|
||||
ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)),
|
||||
ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)),
|
||||
}
|
||||
}
|
||||
fn append_default_port(&self, host: &str, default_port: u16) -> String {
|
||||
if host.contains(':') {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{}:{}", host, default_port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoProxyScheme {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError>;
|
||||
}
|
||||
|
||||
impl<S: IntoUrl> IntoProxyScheme for S {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||
// validate the URL
|
||||
let url = match self.as_str().into_url() {
|
||||
Ok(ok) => ok,
|
||||
Err(e) => {
|
||||
match e {
|
||||
// If the string does not contain protocol headers, try to parse it using the socks5 protocol
|
||||
ProxyError::UrlParseScheme(_source) => {
|
||||
let try_this = format!("socks5://{}", self.as_str());
|
||||
try_this.into_url()?
|
||||
}
|
||||
_ => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ProxyScheme::parse(url)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProxyScheme for ProxyScheme {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Proxy {
|
||||
pub intercept: ProxyScheme,
|
||||
ms_timeout: u64,
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
pub fn new<U: IntoProxyScheme>(proxy_scheme: U, ms_timeout: u64) -> Result<Self, ProxyError> {
|
||||
Ok(Self {
|
||||
intercept: proxy_scheme.into_proxy_scheme()?,
|
||||
ms_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_http_or_https(&self) -> bool {
|
||||
return match self.intercept {
|
||||
ProxyScheme::Socks5 { .. } => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn from_conf(conf: &Socks5Server, ms_timeout: Option<u64>) -> Result<Self, ProxyError> {
|
||||
let mut proxy;
|
||||
match ms_timeout {
|
||||
None => {
|
||||
proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?;
|
||||
}
|
||||
Some(time_out) => {
|
||||
proxy = Self::new(&conf.proxy, time_out)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !conf.password.is_empty() && !conf.username.is_empty() {
|
||||
proxy = proxy.basic_auth(&conf.username, &conf.password);
|
||||
}
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
pub async fn proxy_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||
self.intercept.socket_addrs().await
|
||||
}
|
||||
|
||||
fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
|
||||
self.intercept.set_basic_auth(username, password);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn connect<'t, T>(
|
||||
self,
|
||||
target: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
) -> ResultType<FramedStream>
|
||||
where
|
||||
T: IntoTargetAddr<'t>,
|
||||
{
|
||||
info!("Connect to proxy server");
|
||||
let proxy = self.proxy_addrs().await?;
|
||||
|
||||
let local = if let Some(addr) = local_addr {
|
||||
addr
|
||||
} else {
|
||||
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
|
||||
};
|
||||
|
||||
let stream = super::timeout(
|
||||
self.ms_timeout,
|
||||
crate::tcp::new_socket(local, true)?.connect(proxy),
|
||||
)
|
||||
.await??;
|
||||
stream.set_nodelay(true).ok();
|
||||
|
||||
let addr = stream.local_addr()?;
|
||||
|
||||
return match self.intercept {
|
||||
ProxyScheme::Http { .. } => {
|
||||
info!("Connect to remote http proxy server: {}", proxy);
|
||||
let stream =
|
||||
super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??;
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
ProxyScheme::Https { .. } => {
|
||||
info!("Connect to remote https proxy server: {}", proxy);
|
||||
let stream =
|
||||
super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??;
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
ProxyScheme::Socks5 { .. } => {
|
||||
info!("Connect to remote socket5 proxy server: {}", proxy);
|
||||
let stream = if let Some(auth) = self.intercept.maybe_auth() {
|
||||
super::timeout(
|
||||
self.ms_timeout,
|
||||
Socks5Stream::connect_with_password_and_socket(
|
||||
stream,
|
||||
target,
|
||||
&auth.user_name,
|
||||
&auth.password,
|
||||
),
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
super::timeout(
|
||||
self.ms_timeout,
|
||||
Socks5Stream::connect_with_socket(stream, target),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
pub async fn https_connect<'a, Input, T>(
|
||||
self,
|
||||
io: Input,
|
||||
target: T,
|
||||
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
T: IntoTargetAddr<'a>,
|
||||
{
|
||||
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
|
||||
let stream = tls_connector
|
||||
.connect(&self.intercept.get_domain()?, io)
|
||||
.await?;
|
||||
self.http_connect(stream, target).await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
pub async fn https_connect<'a, Input, T>(
|
||||
self,
|
||||
io: Input,
|
||||
target: T,
|
||||
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
T: IntoTargetAddr<'a>,
|
||||
{
|
||||
use std::convert::TryFrom;
|
||||
let verifier = rustls_platform_verifier::tls_config();
|
||||
let url_domain = self.intercept.get_domain()?;
|
||||
|
||||
let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str())
|
||||
.map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))?
|
||||
.to_owned();
|
||||
|
||||
let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier));
|
||||
let stream = tls_connector.connect(domain, io).await?;
|
||||
self.http_connect(stream, target).await
|
||||
}
|
||||
|
||||
pub async fn http_connect<'a, Input, T>(
|
||||
self,
|
||||
io: Input,
|
||||
target: T,
|
||||
) -> Result<BufStream<Input>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
T: IntoTargetAddr<'a>,
|
||||
{
|
||||
let mut stream = BufStream::new(io);
|
||||
let (domain, port) = get_domain_and_port(target)?;
|
||||
|
||||
let request = self.make_request(&domain, port);
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
recv_and_check_response(&mut stream).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn make_request(&self, host: &str, port: u16) -> String {
|
||||
let mut request = format!(
|
||||
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n",
|
||||
host = host,
|
||||
port = port
|
||||
);
|
||||
|
||||
if let Some(auth) = self.intercept.maybe_auth() {
|
||||
request = format!("{}{}", request, auth.get_proxy_authorization());
|
||||
}
|
||||
|
||||
request.push_str("\r\n");
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> {
|
||||
let target_addr = target
|
||||
.into_target_addr()
|
||||
.map_err(|e| ProxyError::TargetParseError(e.to_string()))?;
|
||||
match target_addr {
|
||||
tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())),
|
||||
tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_response<IO>(stream: &mut BufStream<IO>) -> Result<String, ProxyError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut response = String::new();
|
||||
|
||||
loop {
|
||||
if stream.read_line(&mut response).await? == 0 {
|
||||
return Err(ProxyError::EndOfFile);
|
||||
}
|
||||
|
||||
if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() {
|
||||
return Err(ProxyError::MaximumResponseHeaderLengthExceeded(
|
||||
response.len(),
|
||||
));
|
||||
}
|
||||
|
||||
if response.ends_with("\r\n\r\n") {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_and_check_response<IO>(stream: &mut BufStream<IO>) -> Result<(), ProxyError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let response_string = get_response(stream).await?;
|
||||
|
||||
let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS];
|
||||
let mut response = Response::new(&mut response_headers);
|
||||
let response_bytes = response_string.into_bytes();
|
||||
response.parse(&response_bytes)?;
|
||||
|
||||
return match response.code {
|
||||
Some(code) => {
|
||||
if code == 200 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProxyError::HttpCode200(code))
|
||||
}
|
||||
}
|
||||
None => Err(ProxyError::NoHttpCode),
|
||||
};
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
use crate::{allow_err, anyhow::anyhow, ResultType};
|
||||
use protobuf::Message;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use tokio::{self, stream::StreamExt, sync::mpsc};
|
||||
|
||||
const QUIC_HBB: &[&[u8]] = &[b"hbb"];
|
||||
const SERVER_NAME: &str = "hbb";
|
||||
|
||||
type Sender = mpsc::UnboundedSender<Value>;
|
||||
type Receiver = mpsc::UnboundedReceiver<Value>;
|
||||
|
||||
pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> {
|
||||
let mut transport_config = quinn::TransportConfig::default();
|
||||
transport_config.stream_window_uni(0);
|
||||
let mut server_config = quinn::ServerConfig::default();
|
||||
server_config.transport = Arc::new(transport_config);
|
||||
let mut server_config = quinn::ServerConfigBuilder::new(server_config);
|
||||
server_config.protocols(QUIC_HBB);
|
||||
// server_config.enable_keylog();
|
||||
// server_config.use_stateless_retry(true);
|
||||
let mut endpoint = quinn::Endpoint::builder();
|
||||
endpoint.listen(server_config.build());
|
||||
let (end, incoming) = endpoint.with_socket(socket)?;
|
||||
Ok((Server { incoming }, end.local_addr()?))
|
||||
}
|
||||
|
||||
pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType<Connection> {
|
||||
let mut endpoint = quinn::Endpoint::builder();
|
||||
let mut client_config = quinn::ClientConfigBuilder::default();
|
||||
client_config.protocols(QUIC_HBB);
|
||||
//client_config.enable_keylog();
|
||||
endpoint.default_client_config(client_config.build());
|
||||
let (endpoint, _) = endpoint.bind(local_addr)?;
|
||||
let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?;
|
||||
Connection::new_for_client(new_conn.connection).await
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
incoming: quinn::Incoming,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> ResultType<Option<Connection>> {
|
||||
Connection::new_for_server(&mut self.incoming).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
conn: quinn::Connection,
|
||||
tx: quinn::SendStream,
|
||||
rx: Receiver,
|
||||
}
|
||||
|
||||
type Value = ResultType<Vec<u8>>;
|
||||
|
||||
impl Connection {
|
||||
async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType<Option<Self>> {
|
||||
if let Some(conn) = incoming.next().await {
|
||||
let quinn::NewConnection {
|
||||
connection: conn,
|
||||
// uni_streams,
|
||||
mut bi_streams,
|
||||
..
|
||||
} = conn.await?;
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Value>();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let stream = bi_streams.next().await;
|
||||
if let Some(stream) = stream {
|
||||
let stream = match stream {
|
||||
Err(e) => {
|
||||
tx.send(Err(e.into())).ok();
|
||||
break;
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let cloned = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(handle_request(stream.1, cloned).await);
|
||||
});
|
||||
} else {
|
||||
tx.send(Err(anyhow!("Reset by the peer"))).ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
log::info!("Exit connection outer loop");
|
||||
});
|
||||
let tx = conn.open_uni().await?;
|
||||
Ok(Some(Self { conn, tx, rx }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn new_for_client(conn: quinn::Connection) -> ResultType<Self> {
|
||||
let (tx, rx_quic) = conn.open_bi().await?;
|
||||
let (tx_mpsc, rx) = mpsc::unbounded_channel::<Value>();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(handle_request(rx_quic, tx_mpsc).await);
|
||||
});
|
||||
Ok(Self { conn, tx, rx })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Value> {
|
||||
// None is returned when all Sender halves have dropped,
|
||||
// indicating that no further values can be sent on the channel.
|
||||
self.rx.recv().await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remote_address(&self) -> SocketAddr {
|
||||
self.conn.remote_address()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> {
|
||||
self.tx.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> {
|
||||
match msg.write_to_bytes() {
|
||||
Ok(bytes) => self.send_raw(&bytes).await?,
|
||||
err => allow_err!(err),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -49,19 +49,27 @@ pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
|
||||
host
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: &str) -> String {
|
||||
pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String {
|
||||
let host = check_port(host, 0);
|
||||
|
||||
use std::net::ToSocketAddrs;
|
||||
match Config::get_network_type() {
|
||||
NetworkType::Direct => match host.to_socket_addrs() {
|
||||
|
||||
if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() {
|
||||
test_if_valid_server_for_proxy_(&host)
|
||||
} else {
|
||||
match host.to_socket_addrs() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
},
|
||||
NetworkType::ProxySocks => match &host.into_target_addr() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn test_if_valid_server_for_proxy_(host: &str) -> String {
|
||||
// `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing,
|
||||
// it can be used for both `socks` and `http` proxy.
|
||||
match &host.into_target_addr() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,15 +115,7 @@ pub async fn connect_tcp_local<
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<FramedStream> {
|
||||
if let Some(conf) = Config::get_socks() {
|
||||
return FramedStream::connect(
|
||||
conf.proxy.as_str(),
|
||||
target,
|
||||
local,
|
||||
conf.username.as_str(),
|
||||
conf.password.as_str(),
|
||||
ms_timeout,
|
||||
)
|
||||
.await;
|
||||
return FramedStream::connect(target, local, &conf, ms_timeout).await;
|
||||
}
|
||||
if let Some(target) = target.resolve() {
|
||||
if let Some(local) = local {
|
||||
@@ -255,10 +255,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_test_if_valid_server() {
|
||||
assert!(!test_if_valid_server("a").is_empty());
|
||||
assert!(!test_if_valid_server("a", false).is_empty());
|
||||
// on Linux, "1" is resolved to "0.0.0.1"
|
||||
assert!(test_if_valid_server("1.1.1.1").is_empty());
|
||||
assert!(test_if_valid_server("1.1.1.1:1").is_empty());
|
||||
assert!(test_if_valid_server("1.1.1.1", false).is_empty());
|
||||
assert!(test_if_valid_server("1.1.1.1:1", false).is_empty());
|
||||
assert!(test_if_valid_server("microsoft.com", false).is_empty());
|
||||
assert!(test_if_valid_server("microsoft.com:1", false).is_empty());
|
||||
|
||||
// with proxy
|
||||
// `:0` indicates `let host = check_port(host, 0);` is called.
|
||||
assert!(test_if_valid_server_for_proxy_("a:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{bail, bytes_codec::BytesCodec, ResultType};
|
||||
use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy};
|
||||
use anyhow::Context as AnyhowCtx;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
@@ -18,20 +18,20 @@ use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
||||
};
|
||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs};
|
||||
use tokio_socks::IntoTargetAddr;
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
||||
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send + Sync>);
|
||||
pub struct DynTcpStream(pub(crate) Box<dyn TcpStreamTrait + Send + Sync>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Encrypt(Key, u64, u64);
|
||||
|
||||
pub struct FramedStream(
|
||||
Framed<DynTcpStream, BytesCodec>,
|
||||
SocketAddr,
|
||||
Option<Encrypt>,
|
||||
u64,
|
||||
pub(crate) Framed<DynTcpStream, BytesCodec>,
|
||||
pub(crate) SocketAddr,
|
||||
pub(crate) Option<Encrypt>,
|
||||
pub(crate) u64,
|
||||
);
|
||||
|
||||
impl Deref for FramedStream {
|
||||
@@ -62,7 +62,7 @@ impl DerefMut for DynTcpStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||
pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
||||
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
|
||||
@@ -109,51 +109,17 @@ impl FramedStream {
|
||||
bail!(format!("Failed to connect to {remote_addr}"));
|
||||
}
|
||||
|
||||
pub async fn connect<'a, 't, P, T>(
|
||||
proxy: P,
|
||||
pub async fn connect<'t, T>(
|
||||
target: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
proxy_conf: &Socks5Server,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self>
|
||||
where
|
||||
P: ToProxyAddrs,
|
||||
T: IntoTargetAddr<'t>,
|
||||
{
|
||||
if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
|
||||
let local = if let Some(addr) = local_addr {
|
||||
addr
|
||||
} else {
|
||||
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
|
||||
};
|
||||
let stream =
|
||||
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
|
||||
stream.set_nodelay(true).ok();
|
||||
let stream = if username.trim().is_empty() {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5Stream::connect_with_socket(stream, target),
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5Stream::connect_with_password_and_socket(
|
||||
stream, target, username, password,
|
||||
),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
let addr = stream.local_addr()?;
|
||||
return Ok(Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
));
|
||||
}
|
||||
bail!("could not resolve to any address");
|
||||
let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?;
|
||||
proxy.connect::<T>(target, local_addr).await
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import optparse
|
||||
from hashlib import md5
|
||||
import brotli
|
||||
import datetime
|
||||
|
||||
# 4GB maximum
|
||||
length_count = 4
|
||||
@@ -22,7 +23,7 @@ def generate_md5_table(folder: str, level) -> dict:
|
||||
for f in files:
|
||||
md5_generator = md5()
|
||||
full_path = os.path.join(root, f)
|
||||
print(f"processing {full_path}...")
|
||||
print(f"Processing {full_path}...")
|
||||
f = open(full_path, "rb")
|
||||
content = f.read()
|
||||
content_compressed = brotli.compress(
|
||||
@@ -34,7 +35,7 @@ def generate_md5_table(folder: str, level) -> dict:
|
||||
return res
|
||||
|
||||
|
||||
def write_metadata(md5_table: dict, output_folder: str, exe: str):
|
||||
def write_package_metadata(md5_table: dict, output_folder: str, exe: str):
|
||||
output_path = os.path.join(output_folder, "data.bin")
|
||||
with open(output_path, "wb") as f:
|
||||
f.write("rustdesk".encode(encoding=encoding))
|
||||
@@ -55,8 +56,13 @@ def write_metadata(md5_table: dict, output_folder: str, exe: str):
|
||||
f.write("rustdesk".encode(encoding=encoding))
|
||||
# executable
|
||||
f.write(exe.encode(encoding='utf-8'))
|
||||
print(f"metadata had written to {output_path}")
|
||||
print(f"Metadata has been written to {output_path}")
|
||||
|
||||
def write_app_metadata(output_folder: str):
|
||||
output_path = os.path.join(output_folder, "app_metadata.toml")
|
||||
with open(output_path, "w") as f:
|
||||
f.write(f"timestamp = {int(datetime.datetime.now().timestamp() * 1000)}\n")
|
||||
print(f"App metadata has been written to {output_path}")
|
||||
|
||||
def build_portable(output_folder: str, target: str):
|
||||
os.chdir(output_folder)
|
||||
@@ -91,11 +97,12 @@ if __name__ == '__main__':
|
||||
options.executable = folder + '/' + options.executable
|
||||
exe: str = os.path.abspath(options.executable)
|
||||
if not exe.startswith(os.path.abspath(folder)):
|
||||
print("the executable must locate in source folder")
|
||||
print("The executable must locate in source folder")
|
||||
exit(-1)
|
||||
exe = '.' + exe[len(os.path.abspath(folder)):]
|
||||
print("executable path: " + exe)
|
||||
print("compression level: " + str(options.level))
|
||||
print("Executable path: " + exe)
|
||||
print("Compression level: " + str(options.level))
|
||||
md5_table = generate_md5_table(folder, options.level)
|
||||
write_metadata(md5_table, output_folder, exe)
|
||||
write_package_metadata(md5_table, output_folder, exe)
|
||||
write_app_metadata(output_folder)
|
||||
build_portable(output_folder, options.target)
|
||||
|
||||
@@ -9,9 +9,52 @@ use bin_reader::BinaryReader;
|
||||
|
||||
pub mod bin_reader;
|
||||
|
||||
#[cfg(windows)]
|
||||
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
|
||||
#[cfg(not(windows))]
|
||||
const APP_METADATA: &[u8] = &[];
|
||||
const APP_METADATA_CONFIG: &str = "meta.toml";
|
||||
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
|
||||
const APP_PREFIX: &str = "rustdesk";
|
||||
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
||||
|
||||
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
|
||||
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
||||
return true;
|
||||
};
|
||||
for line in app_metadata.lines() {
|
||||
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
|
||||
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
|
||||
*ts = stored_ts;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if *ts == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) {
|
||||
for line in content.lines() {
|
||||
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
|
||||
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
|
||||
return *ts == stored_ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn write_meta(dir: &PathBuf, ts: u64) {
|
||||
let meta_file = dir.join(APP_METADATA_CONFIG);
|
||||
if ts != 0 {
|
||||
let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts);
|
||||
// Ignore is ok here
|
||||
let _ = std::fs::write(meta_file, content);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> {
|
||||
let dir = if let Some(dir) = dir {
|
||||
dir
|
||||
@@ -24,12 +67,15 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if clear {
|
||||
|
||||
let mut ts = 0;
|
||||
if clear || !is_timestamp_matches(&dir, &mut ts) {
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
for file in reader.files.iter() {
|
||||
file.write_to_file(&dir);
|
||||
}
|
||||
write_meta(&dir, ts);
|
||||
#[cfg(windows)]
|
||||
windows::copy_runtime_broker(&dir);
|
||||
#[cfg(linux)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user