mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-14 16:51:04 +03:00
Compare commits
431 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f26f880b | ||
|
|
22c6f5e589 | ||
|
|
b828768fa9 | ||
|
|
31e7b6acf1 | ||
|
|
14b505130b | ||
|
|
22f3425ace | ||
|
|
4723d6a830 | ||
|
|
c4f3c0f133 | ||
|
|
de375c91bb | ||
|
|
d3454f07d3 | ||
|
|
cf8ef2533a | ||
|
|
6ad662260e | ||
|
|
2b54a553c7 | ||
|
|
85ded0a3e5 | ||
|
|
5c16a8302e | ||
|
|
04c175c62e | ||
|
|
2be05608d8 | ||
|
|
c3c99ba107 | ||
|
|
a81d6468cc | ||
|
|
48464835f5 | ||
|
|
edc5d86ee7 | ||
|
|
e9c8ba5393 | ||
|
|
a72bc0fb28 | ||
|
|
5a8c8cbf7c | ||
|
|
b68d7a3054 | ||
|
|
9e931a6f04 | ||
|
|
f0587796e2 | ||
|
|
875ac28ab5 | ||
|
|
6821bef5e5 | ||
|
|
930561f431 | ||
|
|
bc672b3367 | ||
|
|
e283d33f28 | ||
|
|
901505e8be | ||
|
|
a4565bf0da | ||
|
|
092e4089c7 | ||
|
|
188f85b042 | ||
|
|
72c96f22b6 | ||
|
|
0143eaf601 | ||
|
|
09466680d3 | ||
|
|
eec879a801 | ||
|
|
3f11d9cdb6 | ||
|
|
8512c2b2b0 | ||
|
|
e2a7e38a39 | ||
|
|
3a0ece1447 | ||
|
|
d0a54a6cc6 | ||
|
|
bed214bd37 | ||
|
|
5f31211db3 | ||
|
|
29b8875c1c | ||
|
|
3367c541b2 | ||
|
|
30afe4f779 | ||
|
|
d18e95703e | ||
|
|
9adc083def | ||
|
|
0dc664474a | ||
|
|
5e8fe239fa | ||
|
|
7a3100a87c | ||
|
|
8a1acedae5 | ||
|
|
f5bcc17636 | ||
|
|
883c630206 | ||
|
|
a95a6ab733 | ||
|
|
46605fab1b | ||
|
|
9d26fec631 | ||
|
|
294a6ce9bc | ||
|
|
183ea47ba4 | ||
|
|
06e04143a8 | ||
|
|
a532b36e28 | ||
|
|
c873b69662 | ||
|
|
b30f84623b | ||
|
|
888e993534 | ||
|
|
1d59a7fe5f | ||
|
|
2c027cdcf5 | ||
|
|
fe513dd967 | ||
|
|
d652b99d5b | ||
|
|
c2716c2509 | ||
|
|
821f7245b0 | ||
|
|
0ea88ce6ff | ||
|
|
21f41e98a0 | ||
|
|
282ea02ebf | ||
|
|
170200fa49 | ||
|
|
d8cee6507d | ||
|
|
2391b18046 | ||
|
|
b5a7165015 | ||
|
|
ef4d84657b | ||
|
|
011647511c | ||
|
|
e2d217a138 | ||
|
|
f07936a911 | ||
|
|
0bb4d43e9e | ||
|
|
6f74080a2d | ||
|
|
8a370e640a | ||
|
|
d007408061 | ||
|
|
02572e9032 | ||
|
|
af66d2a73b | ||
|
|
eb5ab4d7d9 | ||
|
|
c02b4f994a | ||
|
|
d093fdc256 | ||
|
|
2af799f46e | ||
|
|
3c7e24c605 | ||
|
|
7d961d895b | ||
|
|
53dbc2fa6f | ||
|
|
024220e58a | ||
|
|
8621b93436 | ||
|
|
ac88121c4a | ||
|
|
90df80ed78 | ||
|
|
d4f3a87276 | ||
|
|
48efdcf1f0 | ||
|
|
0511cdbb21 | ||
|
|
8747b9847f | ||
|
|
92d0fe1c3f | ||
|
|
a9015bcf70 | ||
|
|
f8f2686267 | ||
|
|
c2bd1b8965 | ||
|
|
4eeee5b7ee | ||
|
|
dfc224ec01 | ||
|
|
86ff768241 | ||
|
|
94addb162b | ||
|
|
bea65f8739 | ||
|
|
92f570831d | ||
|
|
9349210a87 | ||
|
|
95f4274eca | ||
|
|
a6febb2816 | ||
|
|
e294dafe7c | ||
|
|
d00582e929 | ||
|
|
6d2e985593 | ||
|
|
182e8c4ac0 | ||
|
|
40019b80f6 | ||
|
|
2f40b9dc04 | ||
|
|
8602b036bd | ||
|
|
51db8e706d | ||
|
|
a0dc38f749 | ||
|
|
625b610cfd | ||
|
|
62a8349739 | ||
|
|
0ab500c27c | ||
|
|
285e974d1a | ||
|
|
e71d86c124 | ||
|
|
14343e89d4 | ||
|
|
3f2dfa521c | ||
|
|
cd73368cb9 | ||
|
|
84b5cd70ed | ||
|
|
01672bc697 | ||
|
|
763174657b | ||
|
|
15fa80fb26 | ||
|
|
d537e2563d | ||
|
|
1719e478e3 | ||
|
|
1f129e6ef3 | ||
|
|
25d0ced8ba | ||
|
|
2116fec20b | ||
|
|
1252f45506 | ||
|
|
1f4c62e480 | ||
|
|
bd334769fa | ||
|
|
750368af7b | ||
|
|
2fb35c3596 | ||
|
|
5114a9d369 | ||
|
|
4b6ba7938f | ||
|
|
1e400d2a64 | ||
|
|
967e63266f | ||
|
|
f9b0a88213 | ||
|
|
d67afa49b4 | ||
|
|
1fd170b089 | ||
|
|
a632718e80 | ||
|
|
9f72d05749 | ||
|
|
c062813c6d | ||
|
|
3ae1638125 | ||
|
|
96aff38862 | ||
|
|
ed3fb1efa4 | ||
|
|
d689bbf38e | ||
|
|
c1bbdaf9ae | ||
|
|
ab9e1013b2 | ||
|
|
e1140b1bea | ||
|
|
cfd27c8d87 | ||
|
|
a18947eed2 | ||
|
|
f8592e0d5b | ||
|
|
5bfdf05ff2 | ||
|
|
9e851542ec | ||
|
|
e79946b4e4 | ||
|
|
aed212d8f8 | ||
|
|
c5d3c7f390 | ||
|
|
b047730830 | ||
|
|
9c7d4ef1f7 | ||
|
|
12d3c59172 | ||
|
|
ef06b7d5d0 | ||
|
|
f17e17a6b9 | ||
|
|
faf363cfd2 | ||
|
|
dbbd9179b7 | ||
|
|
49f848a453 | ||
|
|
ef56aea74f | ||
|
|
cb5fa85ac2 | ||
|
|
11bdd3cfcd | ||
|
|
f0dcc91907 | ||
|
|
c1c2d26ec7 | ||
|
|
93133b9a6c | ||
|
|
245f08055f | ||
|
|
00ddd63372 | ||
|
|
1765c7bbf4 | ||
|
|
65edd55516 | ||
|
|
4947cf8718 | ||
|
|
65dd2b8993 | ||
|
|
ef82cfa034 | ||
|
|
1a69d525af | ||
|
|
307827be3c | ||
|
|
40cb59336f | ||
|
|
a9e0ea8520 | ||
|
|
baeee642dd | ||
|
|
416efe9fd3 | ||
|
|
8b5ac390d1 | ||
|
|
212e8e7559 | ||
|
|
41a20b50ea | ||
|
|
3742b51d58 | ||
|
|
1a21dff5d4 | ||
|
|
bbf7d9e08a | ||
|
|
ffed29e632 | ||
|
|
0f6538c1a7 | ||
|
|
ff2e055a5a | ||
|
|
cdf97f8717 | ||
|
|
b2af79a3c5 | ||
|
|
74cc5abd09 | ||
|
|
32c4712d5e | ||
|
|
3244395bfb | ||
|
|
1cb0e1ce7b | ||
|
|
42394fcbdd | ||
|
|
0b32e741f7 | ||
|
|
80c5d59916 | ||
|
|
e95823f543 | ||
|
|
06fe972683 | ||
|
|
3057396c02 | ||
|
|
7db9543fee | ||
|
|
58d86acf0d | ||
|
|
859020583d | ||
|
|
0cab620ba5 | ||
|
|
4338fcc51a | ||
|
|
5f6f1e8d36 | ||
|
|
a91f244f35 | ||
|
|
82bf04da81 | ||
|
|
a679e4a5e3 | ||
|
|
3c79404534 | ||
|
|
ba707d1149 | ||
|
|
93d88f30b4 | ||
|
|
f5bc136b07 | ||
|
|
ae69cbb207 | ||
|
|
39e3da1eb0 | ||
|
|
c1322b47c3 | ||
|
|
67f83bd5dd | ||
|
|
e424d01f3d | ||
|
|
a424830893 | ||
|
|
3c5810cc01 | ||
|
|
30bd4e1cef | ||
|
|
7956953669 | ||
|
|
dcba4615a2 | ||
|
|
0bf9de8256 | ||
|
|
77f1c7e74c | ||
|
|
27478946ea | ||
|
|
1f25a8af86 | ||
|
|
d75caad71f | ||
|
|
adf0226641 | ||
|
|
137f58a84a | ||
|
|
7c45a68870 | ||
|
|
99edab4b61 | ||
|
|
e50b72622c | ||
|
|
60dc40f47f | ||
|
|
841c331981 | ||
|
|
4eafa5a585 | ||
|
|
5a740e891e | ||
|
|
1fcc7001bd | ||
|
|
e57854422a | ||
|
|
8c39979848 | ||
|
|
2c38648e39 | ||
|
|
97aa739d69 | ||
|
|
b0042f29fb | ||
|
|
e3ca82945f | ||
|
|
bf6a3a7067 | ||
|
|
d25670c79a | ||
|
|
32b26e4ad3 | ||
|
|
818439db48 | ||
|
|
e23a9da1a8 | ||
|
|
37ebac2a9e | ||
|
|
46bf552afc | ||
|
|
70151e3dd8 | ||
|
|
e933f0baf2 | ||
|
|
f2a612c3d9 | ||
|
|
4a648f0068 | ||
|
|
5b52742cf7 | ||
|
|
237d234277 | ||
|
|
ed0cba281f | ||
|
|
2e0eaed322 | ||
|
|
e2a6d66805 | ||
|
|
8d6de9ca59 | ||
|
|
db108d964b | ||
|
|
f016d453fa | ||
|
|
60ea8d2c2b | ||
|
|
12ff1319f1 | ||
|
|
f224d8872e | ||
|
|
5cf2d5f062 | ||
|
|
92dd0ee1dd | ||
|
|
70c20fc76f | ||
|
|
07e0b5ac10 | ||
|
|
12f7fc3d33 | ||
|
|
60f47cb549 | ||
|
|
d33fa3f073 | ||
|
|
2e4fafcf46 | ||
|
|
ab451b9056 | ||
|
|
bc875a35b0 | ||
|
|
8e12a34634 | ||
|
|
77204127f2 | ||
|
|
65c2ccdc93 | ||
|
|
964d4f1f87 | ||
|
|
f559e9c74a | ||
|
|
610009528b | ||
|
|
0f10a88b23 | ||
|
|
60049c8cc5 | ||
|
|
50aa5880de | ||
|
|
47143318ba | ||
|
|
c27791a9ac | ||
|
|
b19d732a3a | ||
|
|
cd3db3a686 | ||
|
|
35fb9f8897 | ||
|
|
ec042434be | ||
|
|
f8041a3de5 | ||
|
|
dd90096e13 | ||
|
|
9ab5512bfa | ||
|
|
32ab56f864 | ||
|
|
78d7bfac01 | ||
|
|
57570c3ba6 | ||
|
|
ffac670f95 | ||
|
|
be16f1be44 | ||
|
|
fd0f85d565 | ||
|
|
8de5f3f0d3 | ||
|
|
0bb537b872 | ||
|
|
987da00be0 | ||
|
|
e9e2214d29 | ||
|
|
ac9f3317f1 | ||
|
|
7da85d277e | ||
|
|
274244b055 | ||
|
|
8fa611daed | ||
|
|
64d0fb17f7 | ||
|
|
6d1d844b14 | ||
|
|
686dd11d8e | ||
|
|
9d42ee9df8 | ||
|
|
9562768a04 | ||
|
|
54b8daede4 | ||
|
|
bd51afe86c | ||
|
|
a84b9bd2c8 | ||
|
|
ce1dac3b86 | ||
|
|
2dcd9f02cd | ||
|
|
416d57bec6 | ||
|
|
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 |
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -38,10 +38,8 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
|||||||
230
.github/workflows/build-macos-arm64.yml
vendored
230
.github/workflows/build-macos-arm64.yml
vendored
@@ -1,230 +0,0 @@
|
|||||||
name: Flutter Nightly MacOS Arm64 Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
#schedule:
|
|
||||||
# schedule build every night
|
|
||||||
# - cron: "0/6 * * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
|
||||||
CARGO_NDK_VERSION: "3.1.2"
|
|
||||||
LLVM_VERSION: "15.0.6"
|
|
||||||
FLUTTER_VERSION: "3.19.6"
|
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
|
||||||
# for arm64 linux because official Dart SDK does not work
|
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
|
||||||
TAG_NAME: "nightly"
|
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
|
||||||
# vcpkg version: 2024.03.25
|
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
|
||||||
VERSION: "1.2.4"
|
|
||||||
NDK_VERSION: "r26d"
|
|
||||||
#signing keys env variable checks
|
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
|
||||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
|
||||||
# To make a custom build with your own servers set the below secret values
|
|
||||||
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
|
||||||
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
|
||||||
API_SERVER: "${{ secrets.API_SERVER }}"
|
|
||||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
|
||||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-appimage:
|
|
||||||
name: Build image ${{ matrix.job.target }}
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- {
|
|
||||||
target: x86_64-unknown-linux-gnu,
|
|
||||||
arch: x86_64,
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
target: aarch64-unknown-linux-gnu,
|
|
||||||
arch: aarch64,
|
|
||||||
}
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Rename Binary
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y wget libarchive-tools
|
|
||||||
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
|
||||||
|
|
||||||
- name: Patch archlinux PKGBUILD
|
|
||||||
if: matrix.job.arch == 'x86_64'
|
|
||||||
run: |
|
|
||||||
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
|
|
||||||
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
|
|
||||||
sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD
|
|
||||||
fi
|
|
||||||
bsdtar -zxvf rustdesk-${{ env.VERSION }}.deb
|
|
||||||
tar -xvf ./data.tar.xz
|
|
||||||
case ${{ matrix.job.arch }} in
|
|
||||||
aarch64)
|
|
||||||
mkdir -p flutter/build/linux/arm64/release/bundle
|
|
||||||
cp -rf usr/lib/rustdesk/* flutter/build/linux/arm64/release/bundle/
|
|
||||||
;;
|
|
||||||
x86_64)
|
|
||||||
mkdir -p flutter/build/linux/x64/release/bundle
|
|
||||||
cp -rf usr/lib/rustdesk/* flutter/build/linux/x64/release/bundle/
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Build archlinux package
|
|
||||||
if: matrix.job.arch == 'x86_64'
|
|
||||||
uses: rustdesk-org/arch-makepkg-action@master
|
|
||||||
with:
|
|
||||||
packages: >
|
|
||||||
llvm
|
|
||||||
clang
|
|
||||||
libva
|
|
||||||
libvdpau
|
|
||||||
rust
|
|
||||||
gstreamer
|
|
||||||
unzip
|
|
||||||
git
|
|
||||||
cmake
|
|
||||||
gcc
|
|
||||||
curl
|
|
||||||
wget
|
|
||||||
nasm
|
|
||||||
zip
|
|
||||||
make
|
|
||||||
pkg-config
|
|
||||||
clang
|
|
||||||
gtk3
|
|
||||||
xdotool
|
|
||||||
libxcb
|
|
||||||
libxfixes
|
|
||||||
alsa-lib
|
|
||||||
pipewire
|
|
||||||
python
|
|
||||||
ttf-arphic-uming
|
|
||||||
libappindicator-gtk3
|
|
||||||
pam
|
|
||||||
gst-plugins-base
|
|
||||||
gst-plugin-pipewire
|
|
||||||
scripts: |
|
|
||||||
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
|
|
||||||
|
|
||||||
- name: Publish archlinux package
|
|
||||||
if: matrix.job.arch == 'x86_64'
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
res/rustdesk-${{ env.VERSION }}*.zst
|
|
||||||
|
|
||||||
- name: Build appimage package
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
# set-up appimage-builder
|
|
||||||
pushd /tmp
|
|
||||||
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
|
||||||
chmod +x appimage-builder-x86_64.AppImage
|
|
||||||
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
|
||||||
popd
|
|
||||||
# run appimage-builder
|
|
||||||
pushd appimage
|
|
||||||
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
|
|
||||||
|
|
||||||
- name: Publish appimage package
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
|
||||||
|
|
||||||
build-flatpak:
|
|
||||||
name: Build Flatpak ${{ matrix.job.target }}
|
|
||||||
runs-on: ${{ matrix.job.on }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- {
|
|
||||||
target: x86_64-unknown-linux-gnu,
|
|
||||||
distro: ubuntu18.04,
|
|
||||||
on: ubuntu-20.04,
|
|
||||||
arch: x86_64,
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
target: aarch64-unknown-linux-gnu,
|
|
||||||
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
|
|
||||||
distro: ubuntu22.04,
|
|
||||||
on: [self-hosted, Linux, ARM64],
|
|
||||||
arch: aarch64,
|
|
||||||
}
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Rename Binary
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y wget
|
|
||||||
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
|
||||||
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
|
||||||
id: rpm
|
|
||||||
with:
|
|
||||||
arch: ${{ matrix.job.arch }}
|
|
||||||
distro: ${{ matrix.job.distro }}
|
|
||||||
githubToken: ${{ github.token }}
|
|
||||||
setup: |
|
|
||||||
ls -l "${PWD}"
|
|
||||||
dockerRunArgs: |
|
|
||||||
--volume "${PWD}:/workspace"
|
|
||||||
shell: /bin/bash
|
|
||||||
install: |
|
|
||||||
apt-get update -y
|
|
||||||
apt-get install -y \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
rpm \
|
|
||||||
wget
|
|
||||||
run: |
|
|
||||||
# disable git safe.directory
|
|
||||||
git config --global --add safe.directory "*"
|
|
||||||
pushd /workspace
|
|
||||||
# install
|
|
||||||
apt-get update -y
|
|
||||||
apt-get install -y \
|
|
||||||
cmake \
|
|
||||||
curl \
|
|
||||||
flatpak \
|
|
||||||
flatpak-builder \
|
|
||||||
gcc \
|
|
||||||
git \
|
|
||||||
g++ \
|
|
||||||
libgtk-3-dev \
|
|
||||||
nasm \
|
|
||||||
wget
|
|
||||||
# flatpak deps
|
|
||||||
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
||||||
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
|
|
||||||
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
|
|
||||||
# package
|
|
||||||
pushd flatpak
|
|
||||||
git clone https://github.com/flathub/shared-modules.git --depth=1
|
|
||||||
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
|
||||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
|
|
||||||
|
|
||||||
- name: Publish flatpak package
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
|
||||||
34
.github/workflows/fdroid.yml
vendored
Normal file
34
.github/workflows/fdroid.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Fdroid version file generation
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
|
||||||
|
# Finds latest release and transforms F-Droid version code from version as follows:
|
||||||
|
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
|
||||||
|
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="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||||
|
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
|
||||||
|
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
|
||||||
147
.github/workflows/flutter-build.yml
vendored
147
.github/workflows/flutter-build.yml
vendored
@@ -10,19 +10,28 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
default: "nightly"
|
default: "nightly"
|
||||||
|
|
||||||
|
# NOTE: F-Droid builder script 'flutter/build_fdroid.sh' reads environment
|
||||||
|
# variables from this workflow!
|
||||||
|
#
|
||||||
|
# It does NOT read build steps, however, so please fix 'flutter/build_fdroid.sh
|
||||||
|
# whenever you add changes to Android CI build action ('build-rustdesk-android')
|
||||||
|
# in this file!
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||||
|
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||||
CARGO_NDK_VERSION: "3.1.2"
|
CARGO_NDK_VERSION: "3.1.2"
|
||||||
LLVM_VERSION: "15.0.6"
|
LLVM_VERSION: "15.0.6"
|
||||||
FLUTTER_VERSION: "3.19.6"
|
FLUTTER_VERSION: "3.19.6"
|
||||||
|
ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem).
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
# for arm64 linux because official Dart SDK does not work
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.03.25
|
# vcpkg version: 2024.06.15
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
VERSION: "1.2.4"
|
VERSION: "1.2.7"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -56,7 +65,7 @@ jobs:
|
|||||||
job:
|
job:
|
||||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64 }
|
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static }
|
||||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
|
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -84,7 +93,7 @@ jobs:
|
|||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.WIN_RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
@@ -107,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install --triplet x64-windows-static --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
@@ -161,6 +170,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
sed -i '/dpiAware/d' res/manifest.xml
|
||||||
pushd ./libs/portable
|
pushd ./libs/portable
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
|
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
|
||||||
@@ -209,7 +219,7 @@ jobs:
|
|||||||
job:
|
job:
|
||||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86 }
|
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static }
|
||||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -246,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install --triplet x86-windows-static --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
@@ -295,14 +305,13 @@ jobs:
|
|||||||
- name: Build self-extracted executable
|
- name: Build self-extracted executable
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
sed -i '/dpiAware/d' res/manifest.xml
|
||||||
pushd ./libs/portable
|
pushd ./libs/portable
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe
|
python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe
|
||||||
popd
|
popd
|
||||||
mkdir -p ./SignOutput
|
mkdir -p ./SignOutput
|
||||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
|
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
|
||||||
mv ./Release ./rustdesk
|
|
||||||
tar czf rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.tar.gz rustdesk
|
|
||||||
|
|
||||||
- name: Sign rustdesk self-extracted file
|
- name: Sign rustdesk self-extracted file
|
||||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||||
@@ -318,7 +327,6 @@ jobs:
|
|||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
files: |
|
files: |
|
||||||
./SignOutput/rustdesk-*.exe
|
./SignOutput/rustdesk-*.exe
|
||||||
./rustdesk-*.tar.gz
|
|
||||||
|
|
||||||
build-for-macOS-arm64-selfhost:
|
build-for-macOS-arm64-selfhost:
|
||||||
# use build-for-macOS instead
|
# use build-for-macOS instead
|
||||||
@@ -404,6 +412,7 @@ jobs:
|
|||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
target: aarch64-apple-ios,
|
target: aarch64-apple-ios,
|
||||||
os: macos-13,
|
os: macos-13,
|
||||||
|
vcpkg-triplet: arm64-ios,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -431,7 +440,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
@@ -548,13 +557,14 @@ jobs:
|
|||||||
- {
|
- {
|
||||||
target: x86_64-apple-darwin,
|
target: x86_64-apple-darwin,
|
||||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
extra-build-args: "--disable-flutter-texture-render",
|
extra-build-args: "",
|
||||||
arch: x86_64,
|
arch: x86_64,
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-apple-darwin,
|
target: aarch64-apple-darwin,
|
||||||
os: macos-latest,
|
os: macos-latest,
|
||||||
extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
|
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
|
||||||
|
extra-build-args: "",
|
||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
@@ -611,6 +621,14 @@ jobs:
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
|
- name: Workaround for flutter issue
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd "$(dirname "$(which flutter)")"
|
||||||
|
# https://github.com/flutter/flutter/issues/133533
|
||||||
|
sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||||
|
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -783,7 +801,6 @@ jobs:
|
|||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
@@ -797,7 +814,6 @@ jobs:
|
|||||||
libxdo-dev \
|
libxdo-dev \
|
||||||
libxfixes-dev \
|
libxfixes-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openjdk-11-jdk-headless \
|
openjdk-11-jdk-headless \
|
||||||
@@ -811,7 +827,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||||
- uses: nttld/setup-ndk@v1
|
- uses: nttld/setup-ndk@v1
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
with:
|
with:
|
||||||
@@ -853,6 +869,13 @@ jobs:
|
|||||||
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
||||||
key: ${{ matrix.job.target }}
|
key: ${{ matrix.job.target }}
|
||||||
|
|
||||||
|
- name: fix android for flutter 3.13
|
||||||
|
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||||
|
run: |
|
||||||
|
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||||
|
cd flutter/lib
|
||||||
|
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
||||||
|
|
||||||
- name: Build rustdesk lib
|
- name: Build rustdesk lib
|
||||||
env:
|
env:
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
@@ -958,12 +981,16 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu,
|
target: x86_64-unknown-linux-gnu,
|
||||||
distro: ubuntu18.04,
|
distro: ubuntu18.04,
|
||||||
on: ubuntu-20.04,
|
on: ubuntu-20.04,
|
||||||
|
deb_arch: amd64,
|
||||||
|
vcpkg-triplet: x64-linux,
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
target: aarch64-unknown-linux-gnu,
|
target: aarch64-unknown-linux-gnu,
|
||||||
distro: ubuntu18.04,
|
distro: ubuntu18.04,
|
||||||
on: [self-hosted, Linux, ARM64],
|
on: [self-hosted, Linux, ARM64],
|
||||||
|
deb_arch: arm64,
|
||||||
|
vcpkg-triplet: arm64-linux,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -998,6 +1025,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
@@ -1014,31 +1042,27 @@ jobs:
|
|||||||
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@master
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
run: |
|
run: |
|
||||||
case ${{ matrix.job.target }} in
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
aarch64-unknown-linux-gnu)
|
|
||||||
$VCPKG_ROOT/vcpkg install --triplet arm64-linux --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
;;
|
|
||||||
x86_64-unknown-linux-gnu)
|
|
||||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@master
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
@@ -1047,7 +1071,7 @@ jobs:
|
|||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
name: Build rustdesk
|
name: Build rustdesk
|
||||||
id: vcpkg
|
id: vcpkg
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.job.arch }}
|
arch: ${{ matrix.job.arch }}
|
||||||
distro: ${{ matrix.job.distro }}
|
distro: ${{ matrix.job.distro }}
|
||||||
@@ -1073,7 +1097,6 @@ jobs:
|
|||||||
libappindicator3-dev \
|
libappindicator3-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
@@ -1087,7 +1110,6 @@ jobs:
|
|||||||
libxdo-dev \
|
libxdo-dev \
|
||||||
libxfixes-dev \
|
libxfixes-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@@ -1128,7 +1150,7 @@ jobs:
|
|||||||
export JOBS=""
|
export JOBS=""
|
||||||
fi
|
fi
|
||||||
echo $JOBS
|
echo $JOBS
|
||||||
cargo build --lib $JOBS --features hwcodec,flutter,flutter_texture_render --release
|
cargo build --lib $JOBS --features hwcodec,flutter --release
|
||||||
rm -rf target/release/deps target/release/build
|
rm -rf target/release/deps target/release/build
|
||||||
rm -rf ~/.cargo
|
rm -rf ~/.cargo
|
||||||
|
|
||||||
@@ -1140,7 +1162,6 @@ jobs:
|
|||||||
aarch64)
|
aarch64)
|
||||||
export PATH=/opt/flutter-elinux/bin:$PATH
|
export PATH=/opt/flutter-elinux/bin:$PATH
|
||||||
sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py
|
sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py
|
||||||
export ARCH=arm64
|
|
||||||
sed -i "s/x64\/release/arm64\/release/g" ./build.py
|
sed -i "s/x64\/release/arm64\/release/g" ./build.py
|
||||||
;;
|
;;
|
||||||
x86_64)
|
x86_64)
|
||||||
@@ -1172,6 +1193,7 @@ jobs:
|
|||||||
# build flutter
|
# build flutter
|
||||||
pushd /workspace
|
pushd /workspace
|
||||||
export CARGO_INCREMENTAL=0
|
export CARGO_INCREMENTAL=0
|
||||||
|
export DEB_ARCH=${{ matrix.job.deb_arch }}
|
||||||
python3 ./build.py --flutter --skip-cargo
|
python3 ./build.py --flutter --skip-cargo
|
||||||
for name in rustdesk*??.deb; do
|
for name in rustdesk*??.deb; do
|
||||||
mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb"
|
mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb"
|
||||||
@@ -1250,21 +1272,33 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
res/rustdesk-${{ env.VERSION }}*.zst
|
res/rustdesk-${{ env.VERSION }}*.zst
|
||||||
|
|
||||||
build-rustdesk-sciter-arm:
|
build-rustdesk-linux-sciter:
|
||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
|
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
|
||||||
runs-on: [self-hosted, Linux, ARM64]
|
runs-on: ${{ matrix.job.on }}
|
||||||
name: build-rustdesk-sciter-arm ${{ matrix.job.target }}
|
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# use a high level qemu-user-static
|
# use a high level qemu-user-static
|
||||||
job:
|
job:
|
||||||
|
- {
|
||||||
|
arch: x86_64,
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
on: ubuntu-20.04,
|
||||||
|
distro: ubuntu18.04,
|
||||||
|
deb_arch: amd64,
|
||||||
|
sciter_arch: x64,
|
||||||
|
vcpkg-triplet: x64-linux,
|
||||||
|
}
|
||||||
- {
|
- {
|
||||||
arch: armv7,
|
arch: armv7,
|
||||||
target: armv7-unknown-linux-gnueabihf,
|
target: armv7-unknown-linux-gnueabihf,
|
||||||
deb-arch: armhf,
|
on: [self-hosted, Linux, ARM64],
|
||||||
use-cross: true,
|
distro: ubuntu18.04-rustdesk,
|
||||||
|
deb_arch: armhf,
|
||||||
|
sciter_arch: arm32,
|
||||||
|
vcpkg-triplet: arm-linux,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -1320,7 +1354,7 @@ jobs:
|
|||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
|
cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
|
||||||
$VCPKG_ROOT/vcpkg install --triplet arm-linux --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
@@ -1328,7 +1362,7 @@ jobs:
|
|||||||
id: vcpkg
|
id: vcpkg
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.job.arch }}
|
arch: ${{ matrix.job.arch }}
|
||||||
distro: ubuntu18.04-rustdesk
|
distro: ${{ matrix.job.distro }}
|
||||||
githubToken: ${{ github.token }}
|
githubToken: ${{ github.token }}
|
||||||
setup: |
|
setup: |
|
||||||
ls -l "${PWD}"
|
ls -l "${PWD}"
|
||||||
@@ -1394,13 +1428,13 @@ jobs:
|
|||||||
pushd /workspace
|
pushd /workspace
|
||||||
python3 ./res/inline-sciter.py
|
python3 ./res/inline-sciter.py
|
||||||
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
||||||
export ARCH=armhf
|
|
||||||
export CARGO_INCREMENTAL=0
|
export CARGO_INCREMENTAL=0
|
||||||
cargo build --features inline --release --bins --jobs 1
|
cargo build --features inline --release --bins --jobs 1
|
||||||
# package
|
# package
|
||||||
mkdir -p ./Release
|
mkdir -p ./Release
|
||||||
mv ./target/release/rustdesk ./Release/rustdesk
|
mv ./target/release/rustdesk ./Release/rustdesk
|
||||||
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/arm32/libsciter-gtk.so
|
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so
|
||||||
|
export DEB_ARCH=${{ matrix.job.deb_arch }}
|
||||||
./build.py --package ./Release
|
./build.py --package ./Release
|
||||||
|
|
||||||
- name: Rename rustdesk
|
- name: Rename rustdesk
|
||||||
@@ -1420,6 +1454,13 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
|
||||||
|
- name: Upload deb
|
||||||
|
uses: actions/upload-artifact@master
|
||||||
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
|
with:
|
||||||
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
|
||||||
build-appimage:
|
build-appimage:
|
||||||
name: Build appimage ${{ matrix.job.target }}
|
name: Build appimage ${{ matrix.job.target }}
|
||||||
needs: [build-rustdesk-linux]
|
needs: [build-rustdesk-linux]
|
||||||
@@ -1477,8 +1518,10 @@ jobs:
|
|||||||
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
||||||
|
|
||||||
build-flatpak:
|
build-flatpak:
|
||||||
name: Build flatpak ${{ matrix.job.target }}
|
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
|
||||||
needs: [build-rustdesk-linux]
|
needs:
|
||||||
|
- build-rustdesk-linux
|
||||||
|
- build-rustdesk-linux-sciter
|
||||||
runs-on: ${{ matrix.job.on }}
|
runs-on: ${{ matrix.job.on }}
|
||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -1490,6 +1533,14 @@ jobs:
|
|||||||
distro: ubuntu18.04,
|
distro: ubuntu18.04,
|
||||||
on: ubuntu-20.04,
|
on: ubuntu-20.04,
|
||||||
arch: x86_64,
|
arch: x86_64,
|
||||||
|
suffix: "",
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
distro: ubuntu18.04,
|
||||||
|
on: ubuntu-20.04,
|
||||||
|
arch: x86_64,
|
||||||
|
suffix: "-sciter",
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-unknown-linux-gnu,
|
target: aarch64-unknown-linux-gnu,
|
||||||
@@ -1497,6 +1548,7 @@ jobs:
|
|||||||
distro: ubuntu22.04,
|
distro: ubuntu22.04,
|
||||||
on: [self-hosted, Linux, ARM64],
|
on: [self-hosted, Linux, ARM64],
|
||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
|
suffix: "",
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
@@ -1505,12 +1557,12 @@ jobs:
|
|||||||
- name: Download Binary
|
- name: Download Binary
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@master
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
- name: Rename Binary
|
- name: Rename Binary
|
||||||
run: |
|
run: |
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb flatpak/rustdesk.deb
|
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
||||||
@@ -1556,7 +1608,7 @@ jobs:
|
|||||||
pushd flatpak
|
pushd flatpak
|
||||||
git clone https://github.com/flathub/shared-modules.git --depth=1
|
git clone https://github.com/flathub/shared-modules.git --depth=1
|
||||||
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
||||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
|
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
|
||||||
|
|
||||||
- name: Publish flatpak package
|
- name: Publish flatpak package
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
@@ -1564,9 +1616,10 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
files: |
|
files: |
|
||||||
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak
|
||||||
|
|
||||||
build-rustdesk-web:
|
build-rustdesk-web:
|
||||||
|
if: False
|
||||||
name: build-rustdesk-web
|
name: build-rustdesk-web
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
22
.github/workflows/flutter-tag.yml
vendored
22
.github/workflows/flutter-tag.yml
vendored
@@ -15,24 +15,4 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
upload-artifact: true
|
upload-artifact: true
|
||||||
upload-tag: ${{ github.ref_name }}
|
upload-tag: ${{ 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
|
|
||||||
84
.github/workflows/history.yml
vendored
84
.github/workflows/history.yml
vendored
@@ -1,84 +0,0 @@
|
|||||||
name: Flutter Windows History Build
|
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
|
|
||||||
env:
|
|
||||||
LLVM_VERSION: "10.0"
|
|
||||||
FLUTTER_VERSION: "3.16.9"
|
|
||||||
TAG_NAME: "tmp"
|
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
|
||||||
VERSION: "1.2.4"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-for-history-windows:
|
|
||||||
name: ${{ matrix.job.date }}
|
|
||||||
runs-on: ${{ matrix.job.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ matrix.job.ref }}
|
|
||||||
|
|
||||||
- name: Install LLVM and Clang
|
|
||||||
uses: KyleMayes/install-llvm-action@v1
|
|
||||||
with:
|
|
||||||
version: ${{ env.LLVM_VERSION }}
|
|
||||||
|
|
||||||
- name: Install flutter
|
|
||||||
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
target: ${{ matrix.job.target }}
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
profile: minimal # minimal component installation (ie, no documentation)
|
|
||||||
|
|
||||||
- name: Install flutter rust bridge deps
|
|
||||||
run: |
|
|
||||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
|
||||||
Push-Location flutter ; flutter pub get ; Pop-Location
|
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
|
||||||
uses: lukka/run-vcpkg@v11
|
|
||||||
with:
|
|
||||||
vcpkgDirectory: C:\vcpkg
|
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
|
||||||
|
|
||||||
- name: Install vcpkg dependencies
|
|
||||||
run: |
|
|
||||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build rustdesk
|
|
||||||
run: python3 .\build.py --portable --hwcodec --flutter
|
|
||||||
|
|
||||||
- name: Build self-extracted executable
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
pushd ./libs/portable
|
|
||||||
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
|
|
||||||
popd
|
|
||||||
mkdir -p ./SignOutput
|
|
||||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
|
|
||||||
|
|
||||||
- name: Publish Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
./SignOutput/rustdesk-*.exe
|
|
||||||
416
.github/workflows/playground.yml
vendored
Normal file
416
.github/workflows/playground.yml
vendored
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
name: playground
|
||||||
|
|
||||||
|
on:
|
||||||
|
#schedule:
|
||||||
|
# schedule build every night
|
||||||
|
# - cron: "0/6 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
CARGO_NDK_VERSION: "3.1.2"
|
||||||
|
LLVM_VERSION: "15.0.6"
|
||||||
|
FLUTTER_VERSION: "3.22.2"
|
||||||
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
|
# for arm64 linux because official Dart SDK does not work
|
||||||
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
|
TAG_NAME: "nightly"
|
||||||
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
|
# vcpkg version: 2024.06.15
|
||||||
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
|
VERSION: "1.2.7"
|
||||||
|
NDK_VERSION: "r26d"
|
||||||
|
#signing keys env variable checks
|
||||||
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||||
|
# To make a custom build with your own servers set the below secret values
|
||||||
|
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||||
|
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||||
|
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||||
|
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||||
|
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-for-macOS:
|
||||||
|
name: ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.13.9",
|
||||||
|
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231219"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.10.6",
|
||||||
|
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231219"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.10.6",
|
||||||
|
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231119"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.13.9",
|
||||||
|
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231119"
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Export GitHub Actions cache environment variables
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.job.ref }}
|
||||||
|
|
||||||
|
- name: Import the codesign cert
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
uses: apple-actions/import-codesign-certs@v1
|
||||||
|
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: Import notarize key
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
||||||
|
fileName: rustdesk.json
|
||||||
|
fileDir: ${{ github.workspace }}
|
||||||
|
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
|
||||||
|
|
||||||
|
- name: Install rcodesign tool
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pushd /tmp
|
||||||
|
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
|
||||||
|
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
|
||||||
|
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Install build runtime
|
||||||
|
run: |
|
||||||
|
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: ${{ matrix.job.flutter }}
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
targets: ${{ matrix.job.target }}
|
||||||
|
components: "rustfmt"
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: ${{ matrix.job.os }}
|
||||||
|
|
||||||
|
- name: Install flutter rust bridge deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
|
||||||
|
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
|
||||||
|
# below works for mac to make buildable on 3.13.9
|
||||||
|
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
|
||||||
|
pushd flutter && flutter pub get && popd
|
||||||
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||||
|
|
||||||
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
uses: lukka/run-vcpkg@v11
|
||||||
|
with:
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
run: |
|
||||||
|
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||||
|
|
||||||
|
- name: Restore from cache and install vcpkg
|
||||||
|
uses: lukka/run-vcpkg@v7
|
||||||
|
if: false
|
||||||
|
with:
|
||||||
|
setupOnly: true
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
if: false
|
||||||
|
run: |
|
||||||
|
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
|
||||||
|
|
||||||
|
- name: Show version information (Rust, cargo, Clang)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
clang --version || true
|
||||||
|
rustup -V
|
||||||
|
rustup toolchain list
|
||||||
|
rustup default
|
||||||
|
cargo -V
|
||||||
|
rustc -V
|
||||||
|
|
||||||
|
- name: Build rustdesk
|
||||||
|
run: |
|
||||||
|
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
||||||
|
|
||||||
|
- name: create unsigned dmg
|
||||||
|
run: |
|
||||||
|
CREATE_DMG="$(command -v create-dmg)"
|
||||||
|
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||||
|
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||||
|
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||||
|
|
||||||
|
- name: Codesign app and create signed dmg
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
run: |
|
||||||
|
# Patch create-dmg to give more attempts to unmount image
|
||||||
|
CREATE_DMG="$(command -v create-dmg)"
|
||||||
|
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||||
|
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||||
|
# Unlock keychain
|
||||||
|
security default-keychain -s rustdesk.keychain
|
||||||
|
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||||
|
# start sign the rustdesk.app and dmg
|
||||||
|
rm -rf *.dmg || true
|
||||||
|
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
|
||||||
|
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||||
|
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
|
||||||
|
# notarize the rustdesk-${{ env.VERSION }}.dmg
|
||||||
|
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
|
||||||
|
|
||||||
|
- name: Rename rustdesk
|
||||||
|
run: |
|
||||||
|
for name in rustdesk*??.dmg; do
|
||||||
|
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Publish DMG package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
rustdesk*-${{ matrix.job.arch }}*.dmg
|
||||||
|
|
||||||
|
|
||||||
|
build-rustdesk-android:
|
||||||
|
if: false
|
||||||
|
name: build rustdesk android apk ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
arch: aarch64,
|
||||||
|
target: aarch64-linux-android,
|
||||||
|
os: ubuntu-20.04,
|
||||||
|
openssl-arch: android-arm64,
|
||||||
|
ref: master, # latest
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.job.ref }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
clang \
|
||||||
|
cmake \
|
||||||
|
curl \
|
||||||
|
gcc-multilib \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
g++-multilib \
|
||||||
|
libappindicator3-dev \
|
||||||
|
libasound2-dev \
|
||||||
|
libc6-dev \
|
||||||
|
libclang-10-dev \
|
||||||
|
libgstreamer1.0-dev \
|
||||||
|
libgstreamer-plugins-base1.0-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libpulse-dev \
|
||||||
|
libva-dev \
|
||||||
|
libvdpau-dev \
|
||||||
|
libxcb-randr0-dev \
|
||||||
|
libxcb-shape0-dev \
|
||||||
|
libxcb-xfixes0-dev \
|
||||||
|
libxdo-dev \
|
||||||
|
libxfixes-dev \
|
||||||
|
llvm-10-dev \
|
||||||
|
nasm \
|
||||||
|
yasm \
|
||||||
|
ninja-build \
|
||||||
|
openjdk-11-jdk-headless \
|
||||||
|
pkg-config \
|
||||||
|
tree \
|
||||||
|
wget
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
components: "rustfmt"
|
||||||
|
|
||||||
|
- name: Install flutter rust bridge deps
|
||||||
|
run: |
|
||||||
|
git config --global core.longpaths true
|
||||||
|
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||||
|
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||||
|
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
|
||||||
|
pushd flutter ; flutter pub get ; popd
|
||||||
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
|
- uses: nttld/setup-ndk@v1
|
||||||
|
id: setup-ndk
|
||||||
|
with:
|
||||||
|
ndk-version: ${{ env.NDK_VERSION }}
|
||||||
|
add-to-path: true
|
||||||
|
|
||||||
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
uses: lukka/run-vcpkg@v11
|
||||||
|
with:
|
||||||
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
run: |
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
./flutter/build_android_deps.sh arm64-v8a
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
./flutter/build_android_deps.sh armeabi-v7a
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Clone deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pushd /opt
|
||||||
|
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
|
||||||
|
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||||
|
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
|
||||||
|
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||||
|
|
||||||
|
- name: Build rustdesk lib
|
||||||
|
env:
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
run: |
|
||||||
|
rustup target add ${{ matrix.job.target }}
|
||||||
|
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
./flutter/ndk_arm64.sh
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
./flutter/ndk_arm.sh
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Build rustdesk
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||||
|
run: |
|
||||||
|
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||||
|
# temporary use debug sign config
|
||||||
|
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||||
|
# build flutter
|
||||||
|
pushd flutter
|
||||||
|
flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||||
|
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||||
|
# build flutter
|
||||||
|
pushd flutter
|
||||||
|
flutter build apk --release --target-platform android-arm --split-per-abi
|
||||||
|
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
popd
|
||||||
|
mkdir -p signed-apk; pushd signed-apk
|
||||||
|
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
|
||||||
|
|
||||||
|
- uses: r0adkll/sign-android-release@v1
|
||||||
|
name: Sign app APK
|
||||||
|
if: env.ANDROID_SIGNING_KEY != null
|
||||||
|
id: sign-rustdesk
|
||||||
|
with:
|
||||||
|
releaseDirectory: ./signed-apk
|
||||||
|
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ANDROID_ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
# override default build-tools version (29.0.3) -- optional
|
||||||
|
BUILD_TOOLS_VERSION: "30.0.2"
|
||||||
|
|
||||||
|
- name: Publish signed apk package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -4,9 +4,9 @@ on:
|
|||||||
types: [released]
|
types: [released]
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: windows-latest # action can only be run on windows
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
with:
|
with:
|
||||||
identifier: RustDesk.RustDesk
|
identifier: RustDesk.RustDesk
|
||||||
version: ${{ github.event.release.tag_name }}
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -51,4 +51,6 @@ lib/generated_bridge.dart
|
|||||||
# build cache in examples
|
# build cache in examples
|
||||||
examples/**/target/
|
examples/**/target/
|
||||||
# ===
|
# ===
|
||||||
vcpkg_installed
|
vcpkg_installed
|
||||||
|
flutter/lib/generated_plugin_registrant.dart
|
||||||
|
libsciter.dylib
|
||||||
|
|||||||
3123
Cargo.lock
generated
3123
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.4"
|
version = "1.2.7"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -19,7 +19,6 @@ path = "src/naming.rs"
|
|||||||
[features]
|
[features]
|
||||||
inline = []
|
inline = []
|
||||||
cli = []
|
cli = []
|
||||||
flutter_texture_render = []
|
|
||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
@@ -67,7 +66,7 @@ default-net = "0.14"
|
|||||||
wol-rs = "1.0"
|
wol-rs = "1.0"
|
||||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||||
errno = "0.3"
|
errno = "0.3"
|
||||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||||
url = { version = "2.3", features = ["serde"] }
|
url = { version = "2.3", features = ["serde"] }
|
||||||
crossbeam-queue = "0.3"
|
crossbeam-queue = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@@ -90,7 +89,10 @@ sys-locale = "0.3"
|
|||||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||||
clipboard = { path = "libs/clipboard" }
|
clipboard = { path = "libs/clipboard" }
|
||||||
ctrlc = "3.2"
|
ctrlc = "3.2"
|
||||||
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
|
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||||
|
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] }
|
||||||
|
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||||
|
|
||||||
system_shutdown = "4.0"
|
system_shutdown = "4.0"
|
||||||
qrcode-generator = "4.1"
|
qrcode-generator = "4.1"
|
||||||
|
|
||||||
@@ -150,11 +152,11 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
|||||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||||
async-process = "1.7"
|
async-process = "1.7"
|
||||||
mouce = { git="https://github.com/fufesou/mouce.git" }
|
mouce = { git="https://github.com/rustdesk-org/mouce.git" }
|
||||||
evdev = { git="https://github.com/fufesou/evdev" }
|
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
pam = { git="https://github.com/fufesou/pam" }
|
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||||
users = { version = "0.11" }
|
users = { version = "0.11" }
|
||||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ RUN apt update -y && \
|
|||||||
libxcb-shape0-dev \
|
libxcb-shape0-dev \
|
||||||
libxcb-xfixes0-dev \
|
libxcb-xfixes0-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
make \
|
make \
|
||||||
cmake \
|
cmake \
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.4
|
version: 1.2.7
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.4
|
version: 1.2.7
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
23
build.py
23
build.py
@@ -25,8 +25,8 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
|||||||
skip_cargo = False
|
skip_cargo = False
|
||||||
|
|
||||||
|
|
||||||
def get_arch() -> str:
|
def get_deb_arch() -> str:
|
||||||
custom_arch = os.environ.get("ARCH")
|
custom_arch = os.environ.get("DEB_ARCH")
|
||||||
if custom_arch is None:
|
if custom_arch is None:
|
||||||
return "amd64"
|
return "amd64"
|
||||||
return custom_arch
|
return custom_arch
|
||||||
@@ -48,15 +48,7 @@ def get_version():
|
|||||||
|
|
||||||
|
|
||||||
def parse_rc_features(feature):
|
def parse_rc_features(feature):
|
||||||
available_features = {
|
available_features = {}
|
||||||
'PrivacyMode': {
|
|
||||||
'platform': ['windows'],
|
|
||||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
|
|
||||||
'/TempTopMostWindow_x64.zip',
|
|
||||||
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
|
|
||||||
'include': ['WindowInjection.dll'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apply_features = {}
|
apply_features = {}
|
||||||
if not feature:
|
if not feature:
|
||||||
feature = []
|
feature = []
|
||||||
@@ -81,7 +73,6 @@ def parse_rc_features(feature):
|
|||||||
elif isinstance(feature, list):
|
elif isinstance(feature, list):
|
||||||
if windows:
|
if windows:
|
||||||
# download third party is deprecated, we use github ci instead.
|
# download third party is deprecated, we use github ci instead.
|
||||||
# force add PrivacyMode
|
|
||||||
# feature.append('PrivacyMode')
|
# feature.append('PrivacyMode')
|
||||||
pass
|
pass
|
||||||
for feat in feature:
|
for feat in feature:
|
||||||
@@ -108,11 +99,9 @@ def make_parser():
|
|||||||
nargs='+',
|
nargs='+',
|
||||||
default='',
|
default='',
|
||||||
help='Integrate features, windows only.'
|
help='Integrate features, windows only.'
|
||||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
|
||||||
parser.add_argument('--flutter', action='store_true',
|
parser.add_argument('--flutter', action='store_true',
|
||||||
help='Build flutter package', default=False)
|
help='Build flutter package', default=False)
|
||||||
parser.add_argument('--disable-flutter-texture-render', action='store_true',
|
|
||||||
help='Build flutter package', default=False)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--hwcodec',
|
'--hwcodec',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@@ -278,8 +267,6 @@ def get_features(args):
|
|||||||
features.append('vram')
|
features.append('vram')
|
||||||
if args.flutter:
|
if args.flutter:
|
||||||
features.append('flutter')
|
features.append('flutter')
|
||||||
if not args.disable_flutter_texture_render:
|
|
||||||
features.append('flutter_texture_render')
|
|
||||||
if args.unix_file_copy_paste:
|
if args.unix_file_copy_paste:
|
||||||
features.append('unix-file-copy-paste')
|
features.append('unix-file-copy-paste')
|
||||||
print("features:", features)
|
print("features:", features)
|
||||||
@@ -298,7 +285,7 @@ Homepage: https://rustdesk.com
|
|||||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
||||||
Description: A remote control software.
|
Description: A remote control software.
|
||||||
|
|
||||||
""" % (version, get_arch())
|
""" % (version, get_deb_arch())
|
||||||
file = open(control_file_path, "w")
|
file = open(control_file_path, "w")
|
||||||
file.write(content)
|
file.write(content)
|
||||||
file.close()
|
file.close()
|
||||||
|
|||||||
@@ -1,60 +1,73 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
|
||||||
<a href="#free-public-servers">Servers</a> •
|
<a href="#free-public-servers">Servers</a> •
|
||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Structure</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
<a href="#snapshot">Snapshot</a><br>
|
<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-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||||
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
|
<b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。
|
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。
|
RustDeskは皆さんの貢献を歓迎します。
|
||||||
|
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
|
||||||
|
|
||||||
[**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
[**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||||
|
|
||||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
[**パッケージのダウンロード**](https://github.com/rustdesk/rustdesk/releases)
|
||||||
|
|
||||||
|
[**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||||
|
|
||||||
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
|
alt="F-Droidで入手する"
|
||||||
|
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||||
|
|
||||||
## 依存関係
|
## 依存関係
|
||||||
|
|
||||||
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。
|
デスクトップ版ではGUIにFlutterまたはSciter(非推奨)を使用しますが、チュートリアルでは分かりやすく、簡単なSciterのみを対象に解説しています。Flutterでのビルド方法については[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)をご覧ください。
|
||||||
|
|
||||||
|
Sciter dynamic libraryを事前にダウンロードしてください。
|
||||||
|
|
||||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
[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) |
|
[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)
|
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||||
|
|
||||||
モバイル版はFlutterを利用します。デスクトップ版もSciterからFlutterへマイグレーション予定です。
|
|
||||||
|
|
||||||
## ビルド手順
|
## ビルド手順
|
||||||
|
|
||||||
- Rust開発環境とC ++ビルド環境を準備します
|
- Rust開発環境とC++ビルド環境を準備します。
|
||||||
|
|
||||||
- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。
|
- [vcpkg](https://github.com/microsoft/vcpkg)をインストールし、環境変数に`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
|
|
||||||
|
|
||||||
- run `cargo run`
|
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- `cargo run`を実行します。
|
||||||
|
|
||||||
## [ビルド](https://rustdesk.com/docs/en/dev/build/)
|
## [ビルド](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
## Linuxでのビルド手順
|
## Linuxでのビルド方法
|
||||||
|
|
||||||
### Ubuntu 18 (Debian 10)
|
### Ubuntu 18 (Debian 10)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
@@ -69,7 +82,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
|
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install vcpkg
|
### vcpkgのインストール
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/microsoft/vcpkg
|
git clone https://github.com/microsoft/vcpkg
|
||||||
@@ -81,7 +94,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
|||||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fix libvpx (For Fedora)
|
### libvpxの修正 (Fedoraのみ)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd vcpkg/buildtrees/libvpx/src
|
cd vcpkg/buildtrees/libvpx/src
|
||||||
@@ -107,9 +120,9 @@ mv libsciter-gtk.so target/debug
|
|||||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dockerでビルドする方法
|
## Dockerでのビルド方法
|
||||||
|
|
||||||
リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。
|
リポジトリをクローンし、Dockerコンテナを構築します:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/rustdesk/rustdesk
|
git clone https://github.com/rustdesk/rustdesk
|
||||||
@@ -117,44 +130,50 @@ cd rustdesk
|
|||||||
docker build -t "rustdesk-builder" .
|
docker build -t "rustdesk-builder" .
|
||||||
```
|
```
|
||||||
|
|
||||||
その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します。
|
以下のコマンドを実行します:
|
||||||
|
|
||||||
```sh
|
```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
|
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
|
||||||
```
|
```
|
||||||
|
このコマンドはRustDeskをビルドする度に実行する必要があります。
|
||||||
|
|
||||||
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に
|
初回ビルドは時間がかかるかもしれませんが、2回目以降は依存関係がキャッシュされるため、ビルドにかかる時間が短くなります。
|
||||||
`--release` を実行します。できあがった実行ファイルは、システムのターゲット・フォルダに格納され、次のコマンドで実行できます。
|
ビルドコマンドに追加の引数を指定する必要がある場合は、コマンドの最後(`<OPTIONAL-ARGS>`の位置)で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に `--release` を追記し実行します。ビルドされた実行ファイルはあなたのシステムのターゲットフォルダに保存され、下記のコマンドで実行することができます。
|
||||||
|
|
||||||
|
デバッグビルドを起動する場合:
|
||||||
```sh
|
```sh
|
||||||
target/debug/rustdesk
|
target/debug/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
あるいは、リリース用の実行ファイルを実行している場合:
|
リリースビルドを起動する場合:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/release/rustdesk
|
target/release/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
|
コマンドをRustDeskリポジトリのルートから実行していることを確認してください。また、`install` や `run` などの他のcargoサブコマンドは、ホストではなくコンテナ内でプログラムをインストール、実行するため、現在の方法ではサポートされていません。
|
||||||
|
|
||||||
## ファイル構造
|
## ファイル構造
|
||||||
|
|
||||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数
|
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、設定、tcp/udpラッパー、protobuf、ファイル転送に利用されるfs関数やその他のユーティリティ関数
|
||||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
|
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
|
||||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール
|
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウス操作
|
||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS向けのファイルのコピーと貼り付けの実装
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止された Sciter UI (非推奨)
|
||||||
|
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**:
|
||||||
|
オーディオ/クリップボード/入力/ビデオ サービスとネットワーク接続
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
|
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
||||||
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
|
||||||
|
|
||||||
## スナップショット
|
## スクリーンショット
|
||||||
|
|
||||||

|

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

|

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

|

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

|

|
||||||
|
|||||||
@@ -106,5 +106,6 @@ dependencies {
|
|||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||||
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,11 @@
|
|||||||
android:name=".MainService"
|
android:name=".MainService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".FloatingWindowService"
|
||||||
|
android:enabled="true" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Don't delete the meta-data below.
|
Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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 (!isSupportVoiceCall()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// No need to check if video or audio is started here.
|
||||||
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
// Return true if not supported, because is was not started.
|
||||||
|
if (!isSupportVoiceCall()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
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 (isAudioStart() || isVideoStart()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
audioThread = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
Log.d(logTag, "destroy audio record handle")
|
||||||
|
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import com.caverock.androidsvg.SVG
|
||||||
|
import ffi.FFI
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class FloatingWindowService : Service(), View.OnTouchListener {
|
||||||
|
|
||||||
|
private lateinit var windowManager: WindowManager
|
||||||
|
private lateinit var layoutParams: WindowManager.LayoutParams
|
||||||
|
private lateinit var floatingView: ImageView
|
||||||
|
private lateinit var originalDrawable: Drawable
|
||||||
|
private lateinit var leftHalfDrawable: Drawable
|
||||||
|
private lateinit var rightHalfDrawable: Drawable
|
||||||
|
|
||||||
|
private var dragging = false
|
||||||
|
private var lastDownX = 0f
|
||||||
|
private var lastDownY = 0f
|
||||||
|
private var viewCreated = false;
|
||||||
|
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logTag = "floatingService"
|
||||||
|
private var firstCreate = true
|
||||||
|
private var viewWidth = 120
|
||||||
|
private var viewHeight = 120
|
||||||
|
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
|
||||||
|
private const val MAX_VIEW_SIZE = 320
|
||||||
|
private var viewUntouchable = false
|
||||||
|
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
|
||||||
|
private var customSvg = ""
|
||||||
|
private var lastLayoutX = 0
|
||||||
|
private var lastLayoutY = 0
|
||||||
|
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
try {
|
||||||
|
if (firstCreate) {
|
||||||
|
firstCreate = false
|
||||||
|
onFirstCreate(windowManager)
|
||||||
|
}
|
||||||
|
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
|
||||||
|
createView(windowManager)
|
||||||
|
handler.postDelayed(runnable, 1000)
|
||||||
|
Log.d(logTag, "onCreate success")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "onCreate failed: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (viewCreated) {
|
||||||
|
windowManager.removeView(floatingView)
|
||||||
|
}
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun createView(windowManager: WindowManager) {
|
||||||
|
floatingView = ImageView(this)
|
||||||
|
viewCreated = true
|
||||||
|
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
|
||||||
|
if (customSvg.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val svg = SVG.getFromString(customSvg)
|
||||||
|
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
|
||||||
|
// This make the svg render clear
|
||||||
|
svg.documentWidth = viewWidth * 1f
|
||||||
|
svg.documentHeight = viewHeight * 1f
|
||||||
|
originalDrawable = svg.renderToPicture().let {
|
||||||
|
BitmapDrawable(
|
||||||
|
resources,
|
||||||
|
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
|
||||||
|
.also { bitmap ->
|
||||||
|
it.draw(Canvas(bitmap))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||||
|
Log.d(logTag, "custom svg loaded")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val originalBitmap = Bitmap.createBitmap(
|
||||||
|
originalDrawable.intrinsicWidth,
|
||||||
|
originalDrawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(originalBitmap)
|
||||||
|
originalDrawable.setBounds(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
originalDrawable.draw(canvas)
|
||||||
|
val leftHalfBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
val rightHalfBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
|
||||||
|
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
|
||||||
|
|
||||||
|
floatingView.setImageDrawable(rightHalfDrawable)
|
||||||
|
floatingView.setOnTouchListener(this)
|
||||||
|
floatingView.alpha = viewTransparency * 1f
|
||||||
|
|
||||||
|
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
|
||||||
|
if (viewUntouchable || viewTransparency == 0f) {
|
||||||
|
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||||
|
}
|
||||||
|
layoutParams = WindowManager.LayoutParams(
|
||||||
|
viewWidth / 2,
|
||||||
|
viewHeight,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
|
||||||
|
flags,
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
)
|
||||||
|
|
||||||
|
layoutParams.gravity = Gravity.TOP or Gravity.START
|
||||||
|
layoutParams.x = lastLayoutX
|
||||||
|
layoutParams.y = lastLayoutY
|
||||||
|
|
||||||
|
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
|
||||||
|
keepScreenOn = when (keepScreenOnOption) {
|
||||||
|
"never" -> KeepScreenOn.NEVER
|
||||||
|
"service-on" -> KeepScreenOn.SERVICE_ON
|
||||||
|
else -> KeepScreenOn.DURING_CONTROLLED
|
||||||
|
}
|
||||||
|
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
|
||||||
|
updateKeepScreenOnLayoutParams()
|
||||||
|
|
||||||
|
windowManager.addView(floatingView, layoutParams)
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFirstCreate(windowManager: WindowManager) {
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
val w = wh.first
|
||||||
|
val h = wh.second
|
||||||
|
// size
|
||||||
|
FFI.getLocalOption("floating-window-size").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val size = it.toInt()
|
||||||
|
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
|
||||||
|
viewWidth = size
|
||||||
|
viewHeight = size
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// untouchable
|
||||||
|
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
|
||||||
|
// transparency
|
||||||
|
FFI.getLocalOption("floating-window-transparency").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val transparency = it.toInt()
|
||||||
|
if (transparency in 0..10) {
|
||||||
|
viewTransparency = transparency * 1f / 10
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// custom svg
|
||||||
|
FFI.getLocalOption("floating-window-svg").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
customSvg = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// position
|
||||||
|
lastLayoutX = 0
|
||||||
|
lastLayoutY = (wh.second - viewHeight) / 2
|
||||||
|
lastOrientation = resources.configuration.orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun performClick() {
|
||||||
|
showPopupMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||||
|
when (event?.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
dragging = false
|
||||||
|
lastDownX = event.rawX
|
||||||
|
lastDownY = event.rawY
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
val clickDragTolerance = 10f
|
||||||
|
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
|
||||||
|
performClick()
|
||||||
|
} else {
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val dx = event.rawX - lastDownX
|
||||||
|
val dy = event.rawY - lastDownY
|
||||||
|
// ignore too small fist start moving(some time is click)
|
||||||
|
if (!dragging && dx*dx+dy*dy < 25) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
layoutParams.x = event.rawX.toInt()
|
||||||
|
layoutParams.y = event.rawY.toInt()
|
||||||
|
layoutParams.width = viewWidth
|
||||||
|
floatingView.setImageDrawable(originalDrawable)
|
||||||
|
windowManager.updateViewLayout(view, layoutParams)
|
||||||
|
lastLayoutX = layoutParams.x
|
||||||
|
lastLayoutY = layoutParams.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveToScreenSide(center: Boolean = false) {
|
||||||
|
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
val w = wh.first
|
||||||
|
if (layoutParams.x < w / 2) {
|
||||||
|
layoutParams.x = 0
|
||||||
|
floatingView.setImageDrawable(rightHalfDrawable)
|
||||||
|
} else {
|
||||||
|
layoutParams.x = w - viewWidth / 2
|
||||||
|
floatingView.setImageDrawable(leftHalfDrawable)
|
||||||
|
}
|
||||||
|
if (center) {
|
||||||
|
layoutParams.y = (wh.second - viewHeight) / 2
|
||||||
|
}
|
||||||
|
layoutParams.width = viewWidth / 2
|
||||||
|
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||||
|
lastLayoutX = layoutParams.x
|
||||||
|
lastLayoutY = layoutParams.y
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
if (newConfig.orientation != lastOrientation) {
|
||||||
|
lastOrientation = newConfig.orientation
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
|
||||||
|
val newW = wh.first
|
||||||
|
val newH = wh.second
|
||||||
|
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
|
// Proportional change
|
||||||
|
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
|
||||||
|
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
|
||||||
|
}
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPopupMenu() {
|
||||||
|
val popupMenu = PopupMenu(this, floatingView)
|
||||||
|
val idShowRustDesk = 0
|
||||||
|
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
||||||
|
val idStopService = 1
|
||||||
|
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||||
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
idShowRustDesk -> {
|
||||||
|
openMainActivity()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
idStopService -> {
|
||||||
|
stopMainService()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupMenu.setOnDismissListener {
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openMainActivity() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
pendingIntent.send()
|
||||||
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopMainService() {
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class KeepScreenOn {
|
||||||
|
NEVER,
|
||||||
|
DURING_CONTROLLED,
|
||||||
|
SERVICE_ON,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (updateKeepScreenOnLayoutParams()) {
|
||||||
|
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateKeepScreenOnLayoutParams(): Boolean {
|
||||||
|
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
|
||||||
|
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
|
||||||
|
if (oldOn != newOn) {
|
||||||
|
Log.d(logTag, "change keep screen on to $newOn")
|
||||||
|
if (newOn) {
|
||||||
|
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
|
||||||
|
} else {
|
||||||
|
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -18,7 +18,9 @@ import android.widget.EditText
|
|||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import android.view.ViewGroup.LayoutParams
|
import android.view.ViewGroup.LayoutParams
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import android.view.KeyEvent as KeyEventAndroid
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.media.AudioManager
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||||
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||||
|
|
||||||
|
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||||
val x = max(0, _x)
|
val x = max(0, _x)
|
||||||
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||||
|
|
||||||
|
var ke: KeyEventAndroid? = null
|
||||||
|
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
|
||||||
|
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
|
||||||
|
}
|
||||||
|
ke?.let { event ->
|
||||||
|
if (tryHandleVolumeKeyEvent(event)) {
|
||||||
|
return
|
||||||
|
} else if (tryHandlePowerKeyEvent(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
getInputMethod()?.let { inputMethod ->
|
getInputMethod()?.let { inputMethod ->
|
||||||
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
||||||
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
|
|||||||
inputConnection.commitText(text, 1, null)
|
inputConnection.commitText(text, 1, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
|
ke?.let { event ->
|
||||||
inputConnection.sendKeyEvent(event)
|
inputConnection.sendKeyEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
|
|||||||
} else {
|
} else {
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
handler.post {
|
handler.post {
|
||||||
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
|
ke?.let { event ->
|
||||||
val possibleNodes = possibleAccessibiltyNodes()
|
val possibleNodes = possibleAccessibiltyNodes()
|
||||||
Log.d(logTag, "possibleNodes:$possibleNodes")
|
Log.d(logTag, "possibleNodes:$possibleNodes")
|
||||||
for (item in possibleNodes) {
|
for (item in possibleNodes) {
|
||||||
@@ -325,6 +341,43 @@ class InputService : AccessibilityService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
|
||||||
|
when (event.keyCode) {
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
|
||||||
|
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
|
||||||
|
// Perform power dialog action when action is up
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||||
|
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return
|
return
|
||||||
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
|
|||||||
return linkedList
|
return linkedList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||||
node.refresh()
|
node.refresh()
|
||||||
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
||||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||||
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
||||||
it.onPreDraw()
|
it.onPreDraw()
|
||||||
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
val succ = it.onKeyDown(event.getKeyCode(), event)
|
val succ = it.onKeyDown(event.getKeyCode(), event)
|
||||||
Log.d(logTag, "onKeyDown $succ")
|
Log.d(logTag, "onKeyDown $succ")
|
||||||
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
|
} else if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||||
val success = it.onKeyUp(event.getKeyCode(), event)
|
val success = it.onKeyUp(event.getKeyCode(), event)
|
||||||
Log.d(logTag, "keyup $success")
|
Log.d(logTag, "keyup $success")
|
||||||
} else {}
|
} else {}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ object KeyEventConverter {
|
|||||||
action = KeyEvent.ACTION_UP
|
action = KeyEvent.ACTION_UP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: The last parameter is the repeat count, not modifiers ?
|
||||||
|
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
|
||||||
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,10 @@ object KeyEventConverter {
|
|||||||
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
||||||
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
||||||
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
||||||
|
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
|
||||||
|
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
|
||||||
|
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
|
||||||
|
ControlKey.Power -> KeyEvent.KEYCODE_POWER
|
||||||
else -> 0 // Default to unknown.
|
else -> 0 // Default to unknown.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ package com.carriez.flutter_hbb
|
|||||||
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ffi.FFI
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -15,10 +17,20 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
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 com.hjq.permissions.XXPermissions
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
@@ -30,6 +42,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val logTag = "mMainActivity"
|
private val logTag = "mMainActivity"
|
||||||
private var mainService: MainService? = null
|
private var mainService: MainService? = null
|
||||||
|
|
||||||
|
private var isAudioStart = false
|
||||||
|
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
if (MainService.isReady) {
|
if (MainService.isReady) {
|
||||||
@@ -42,6 +57,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
channelTag
|
channelTag
|
||||||
)
|
)
|
||||||
initFlutterChannel(flutterMethodChannel!!)
|
initFlutterChannel(flutterMethodChannel!!)
|
||||||
|
thread { setCodecInfo() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -217,10 +233,159 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GET_VALUE -> {
|
||||||
|
if (call.arguments is String) {
|
||||||
|
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
||||||
|
result.success(isSupportVoiceCall())
|
||||||
|
} else {
|
||||||
|
result.error("-1", "No such key", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"on_voice_call_started" -> {
|
||||||
|
onVoiceCallStarted()
|
||||||
|
}
|
||||||
|
"on_voice_call_closed" -> {
|
||||||
|
onVoiceCallClosed()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
result.error("-1", "No such method", null)
|
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
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
var w = wh.first
|
||||||
|
var h = wh.second
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
|
||||||
|
if (!disableFloatingWindow && MainService.isReady) {
|
||||||
|
startService(Intent(this, FloatingWindowService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,21 +55,18 @@ const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
|||||||
|
|
||||||
// video const
|
// video const
|
||||||
|
|
||||||
|
const val MAX_SCREEN_SIZE = 1200
|
||||||
|
|
||||||
const val VIDEO_KEY_BIT_RATE = 1024_000
|
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||||
const val VIDEO_KEY_FRAME_RATE = 30
|
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() {
|
class MainService : Service() {
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
|
||||||
// turn on screen with LIFT_DOWN when screen off
|
// turn on screen with LIFT_DOWN when screen off
|
||||||
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
|
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
Log.d(logTag, "Turn on Screen, WakeLock release")
|
Log.d(logTag, "Turn on Screen, WakeLock release")
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
@@ -78,10 +75,10 @@ class MainService : Service() {
|
|||||||
wakeLock.acquire(5000)
|
wakeLock.acquire(5000)
|
||||||
} else {
|
} else {
|
||||||
when (kind) {
|
when (kind) {
|
||||||
"touch" -> {
|
0 -> { // touch
|
||||||
InputService.ctx?.onTouchInput(mask, x, y)
|
InputService.ctx?.onTouchInput(mask, x, y)
|
||||||
}
|
}
|
||||||
"mouse" -> {
|
1 -> { // mouse
|
||||||
InputService.ctx?.onMouseInput(mask, x, y)
|
InputService.ctx?.onMouseInput(mask, x, y)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -106,6 +103,9 @@ class MainService : Service() {
|
|||||||
put("scale",SCREEN_INFO.scale)
|
put("scale",SCREEN_INFO.scale)
|
||||||
}.toString()
|
}.toString()
|
||||||
}
|
}
|
||||||
|
"is_start" -> {
|
||||||
|
isStart.toString()
|
||||||
|
}
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,10 +138,51 @@ class MainService : Service() {
|
|||||||
e.printStackTrace()
|
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" -> {
|
"stop_capture" -> {
|
||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
stopCapture()
|
stopCapture()
|
||||||
}
|
}
|
||||||
|
"half_scale" -> {
|
||||||
|
val halfScale = arg1.toBoolean()
|
||||||
|
if (isHalfScale != halfScale) {
|
||||||
|
isHalfScale = halfScale
|
||||||
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,18 +194,16 @@ class MainService : Service() {
|
|||||||
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||||
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
||||||
|
|
||||||
private fun translate(input: String): String {
|
|
||||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
|
||||||
return FFI.translateLocale(LOCAL_NAME, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _isReady = false // media permission ready status
|
private var _isReady = false // media permission ready status
|
||||||
private var _isStart = false // screen capture start status
|
private var _isStart = false // screen capture start status
|
||||||
|
private var _isAudioStart = false // audio capture start status
|
||||||
val isReady: Boolean
|
val isReady: Boolean
|
||||||
get() = _isReady
|
get() = _isReady
|
||||||
val isStart: Boolean
|
val isStart: Boolean
|
||||||
get() = _isStart
|
get() = _isStart
|
||||||
|
val isAudioStart: Boolean
|
||||||
|
get() = _isAudioStart
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logTag = "LOG_SERVICE"
|
private val logTag = "LOG_SERVICE"
|
||||||
@@ -182,10 +221,7 @@ class MainService : Service() {
|
|||||||
private var virtualDisplay: VirtualDisplay? = null
|
private var virtualDisplay: VirtualDisplay? = null
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
private var audioRecorder: AudioRecord? = null
|
private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
|
||||||
private var audioReader: AudioReader? = null
|
|
||||||
private var minBufferSize = 0
|
|
||||||
private var audioRecordStat = false
|
|
||||||
|
|
||||||
// notification
|
// notification
|
||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
@@ -214,9 +250,11 @@ class MainService : Service() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isHalfScale: Boolean? = null;
|
||||||
private fun updateScreenInfo(orientation: Int) {
|
private fun updateScreenInfo(orientation: Int) {
|
||||||
var w: Int
|
var w: Int
|
||||||
var h: Int
|
var h: Int
|
||||||
@@ -249,6 +287,12 @@ class MainService : Service() {
|
|||||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||||
var scale = 1
|
var scale = 1
|
||||||
if (w != 0 && h != 0) {
|
if (w != 0 && h != 0) {
|
||||||
|
if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||||
|
scale = 2
|
||||||
|
w /= scale
|
||||||
|
h /= scale
|
||||||
|
dpi /= scale
|
||||||
|
}
|
||||||
if (SCREEN_INFO.width != w) {
|
if (SCREEN_INFO.width != w) {
|
||||||
SCREEN_INFO.width = w
|
SCREEN_INFO.width = w
|
||||||
SCREEN_INFO.height = h
|
SCREEN_INFO.height = h
|
||||||
@@ -349,6 +393,14 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallStarted(): Boolean {
|
||||||
|
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallClosed(): Boolean {
|
||||||
|
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||||
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
@@ -369,12 +421,16 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
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()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
FFI.setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
FFI.setFrameRawEnable("audio",true)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +438,6 @@ class MainService : Service() {
|
|||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
FFI.setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
FFI.setFrameRawEnable("audio",false)
|
|
||||||
_isStart = false
|
_isStart = false
|
||||||
// release video
|
// release video
|
||||||
if (reuseVirtualDisplay) {
|
if (reuseVirtualDisplay) {
|
||||||
@@ -411,12 +466,14 @@ class MainService : Service() {
|
|||||||
surface?.release()
|
surface?.release()
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
audioRecordStat = false
|
_isAudioStart = false
|
||||||
|
audioRecordHandle.tryReleaseAudio()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
Log.d(logTag, "destroy service")
|
Log.d(logTag, "destroy service")
|
||||||
_isReady = false
|
_isReady = false
|
||||||
|
_isAudioStart = false
|
||||||
|
|
||||||
stopCapture()
|
stopCapture()
|
||||||
|
|
||||||
@@ -428,6 +485,7 @@ class MainService : Service() {
|
|||||||
mediaProjection = null
|
mediaProjection = null
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +572,6 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createMediaCodec() {
|
private fun createMediaCodec() {
|
||||||
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
||||||
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
||||||
@@ -534,80 +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 {
|
|
||||||
FFI.onAudioFrameUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// let's release here rather than onDestroy to avoid threading issue
|
|
||||||
audioRecorder?.release()
|
|
||||||
audioRecorder = null
|
|
||||||
minBufferSize = 0
|
|
||||||
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() {
|
private fun initNotification() {
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -692,6 +675,21 @@ class MainService : Service() {
|
|||||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
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 {
|
private fun getClientNotifyID(clientID: Int): Int {
|
||||||
return clientID + NOTIFY_ID_OFFSET
|
return clientID + NOTIFY_ID_OFFSET
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
|
||||||
|
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class VolumeController(private val audioManager: AudioManager) {
|
||||||
|
private val logTag = "volume controller"
|
||||||
|
|
||||||
|
fun getVolume(streamType: Int): Double {
|
||||||
|
val current = audioManager.getStreamVolume(streamType)
|
||||||
|
val max = audioManager.getStreamMaxVolume(streamType)
|
||||||
|
return current.toDouble() / max
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
val max = audioManager.getStreamMaxVolume(streamType)
|
||||||
|
audioManager.setStreamVolume(
|
||||||
|
streamType,
|
||||||
|
(max * volume).toInt(),
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (step == null) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
AudioManager.ADJUST_RAISE,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val target = getVolume(streamType) + step
|
||||||
|
setVolume(target, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (step == null) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
AudioManager.ADJUST_LOWER,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val target = getVolume(streamType) - step
|
||||||
|
setVolume(target, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMute(streamType: Int): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
audioManager.isStreamMute(streamType)
|
||||||
|
} else {
|
||||||
|
audioManager.getStreamVolume(streamType) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
audioManager.setStreamMute(streamType, isMuted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
|
||||||
|
val isMuted = getMute(streamType)
|
||||||
|
setMute(!isMuted, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,10 +15,14 @@ import android.os.Looper
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.*
|
import android.provider.Settings.*
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import ffi.FFI
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
|
|||||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||||
|
const val GET_VALUE = "get_value"
|
||||||
|
|
||||||
|
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
||||||
|
|
||||||
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
||||||
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
||||||
@@ -56,6 +63,11 @@ data class Info(
|
|||||||
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun isSupportVoiceCall(): Boolean {
|
||||||
|
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
}
|
||||||
|
|
||||||
fun requestPermission(context: Context, type: String) {
|
fun requestPermission(context: Context, type: String) {
|
||||||
XXPermissions.with(context)
|
XXPermissions.with(context)
|
||||||
.permission(type)
|
.permission(type)
|
||||||
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val m = windowManager.maximumWindowMetrics
|
||||||
|
w = m.bounds.width()
|
||||||
|
h = m.bounds.height()
|
||||||
|
} else {
|
||||||
|
val dm = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||||
|
w = dm.widthPixels
|
||||||
|
h = dm.heightPixels
|
||||||
|
}
|
||||||
|
return Pair(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(input: String): String {
|
||||||
|
Log.d("common", "translate:$LOCAL_NAME")
|
||||||
|
return FFI.translateLocale(LOCAL_NAME, input)
|
||||||
|
}
|
||||||
@@ -18,4 +18,6 @@ object FFI {
|
|||||||
external fun translateLocale(localeName: String, input: String): String
|
external fun translateLocale(localeName: String, input: String): String
|
||||||
external fun refreshScreen()
|
external fun refreshScreen()
|
||||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
|
external fun setCodecInfo(info: String)
|
||||||
|
external fun getLocalOption(key: String): String
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
565
flutter/build_fdroid.sh
Executable file
565
flutter/build_fdroid.sh
Executable file
@@ -0,0 +1,565 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
#
|
||||||
|
# Script to build F-Droid release of RustDesk
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024, The RustDesk Authors
|
||||||
|
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||||
|
#
|
||||||
|
|
||||||
|
# The script is invoked by F-Droid builder system ste-by-step.
|
||||||
|
#
|
||||||
|
# It accepts the following arguments:
|
||||||
|
#
|
||||||
|
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||||
|
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||||
|
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||||
|
# - The build step to execute:
|
||||||
|
#
|
||||||
|
# + sudo-deps: as root, install needed Debian packages into builder VM
|
||||||
|
# + prebuild: patch sources and do other stuff before the build
|
||||||
|
# + build: perform actual build of APK file
|
||||||
|
#
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
|
||||||
|
VERNAME="${1}"
|
||||||
|
VERCODE="${2}"
|
||||||
|
ANDROID_ABI="${3}"
|
||||||
|
BUILDSTEP="${4}"
|
||||||
|
|
||||||
|
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
|
||||||
|
[ -z "${BUILDSTEP}" ]; then
|
||||||
|
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set various architecture-specific identifiers
|
||||||
|
|
||||||
|
case "${ANDROID_ABI}" in
|
||||||
|
arm64-v8a)
|
||||||
|
FLUTTER_TARGET=android-arm64
|
||||||
|
NDK_TARGET=aarch64-linux-android
|
||||||
|
RUST_TARGET=aarch64-linux-android
|
||||||
|
# RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
armeabi-v7a)
|
||||||
|
FLUTTER_TARGET=android-arm
|
||||||
|
NDK_TARGET=arm-linux-androideabi
|
||||||
|
RUST_TARGET=armv7-linux-androideabi
|
||||||
|
# RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
FLUTTER_TARGET=android-x64
|
||||||
|
NDK_TARGET=x86_64-linux-android
|
||||||
|
RUST_TARGET=x86_64-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
x86)
|
||||||
|
FLUTTER_TARGET=android-x86
|
||||||
|
NDK_TARGET=i686-linux-android
|
||||||
|
RUST_TARGET=i686-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
|
||||||
|
|
||||||
|
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
|
||||||
|
echo "ERROR: Can not find Android SDK!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export necessary variables
|
||||||
|
|
||||||
|
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
|
||||||
|
|
||||||
|
export VCPKG_ROOT="${HOME}/vcpkg"
|
||||||
|
|
||||||
|
# Now act depending on build step
|
||||||
|
|
||||||
|
# NOTE: F-Droid maintainers require explicit declaration of dependencies
|
||||||
|
# as root via `Builds.sudo` F-Droid metadata directive:
|
||||||
|
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
|
||||||
|
|
||||||
|
case "${BUILDSTEP}" in
|
||||||
|
prebuild)
|
||||||
|
# prebuild: patch sources and do other stuff before the build
|
||||||
|
|
||||||
|
#
|
||||||
|
# Extract required versions for NDK, Rust, Flutter from
|
||||||
|
# '.github/workflows/flutter-build.yml'
|
||||||
|
#
|
||||||
|
|
||||||
|
CARGO_NDK_VERSION="$(yq -r \
|
||||||
|
.env.CARGO_NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
NDK_VERSION="$(yq -r \
|
||||||
|
.env.NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
RUST_VERSION="$(yq -r \
|
||||||
|
.env.RUST_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
VCPKG_COMMIT_ID="$(yq -r \
|
||||||
|
.env.VCPKG_COMMIT_ID \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
||||||
|
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
||||||
|
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
||||||
|
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
||||||
|
echo "ERROR: Can not identify all required versions!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Map NDK version to revision
|
||||||
|
|
||||||
|
NDK_VERSION="$(wget \
|
||||||
|
-qO- \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
'https://api.github.com/repos/android/ndk/releases' |
|
||||||
|
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||||
|
|
||||||
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Install the components
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install Android NDK
|
||||||
|
|
||||||
|
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
|
||||||
|
sdkmanager --install "ndk;${NDK_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Flutter
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone https://github.com/flutter/flutter
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
git reset --hard "${FLUTTER_VERSION}"
|
||||||
|
|
||||||
|
flutter config --no-analytics
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Rust
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone --depth 1 https://github.com/rust-lang/rustup
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd "${HOME}/rustup"
|
||||||
|
bash rustup-init.sh -y \
|
||||||
|
--target "${RUST_TARGET}" \
|
||||||
|
--default-toolchain "${RUST_VERSION}"
|
||||||
|
popd
|
||||||
|
|
||||||
|
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||||
|
. "${HOME}/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install cargo-ndk
|
||||||
|
|
||||||
|
cargo install \
|
||||||
|
cargo-ndk \
|
||||||
|
--version "${CARGO_NDK_VERSION}"
|
||||||
|
|
||||||
|
# Install rust bridge generator
|
||||||
|
|
||||||
|
cargo install cargo-expand
|
||||||
|
cargo install flutter_rust_bridge_codegen \
|
||||||
|
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||||
|
--features "uuid"
|
||||||
|
|
||||||
|
# Populate native vcpkg dependencies
|
||||||
|
|
||||||
|
if [ ! -d "${VCPKG_ROOT}" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
https://github.com/Microsoft/vcpkg.git
|
||||||
|
git clone \
|
||||||
|
https://github.com/Microsoft/vcpkg-tool.git
|
||||||
|
|
||||||
|
pushd vcpkg-tool
|
||||||
|
|
||||||
|
mkdir build
|
||||||
|
|
||||||
|
pushd build
|
||||||
|
|
||||||
|
cmake \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-G 'Ninja' \
|
||||||
|
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
|
||||||
|
..
|
||||||
|
|
||||||
|
cmake --build .
|
||||||
|
|
||||||
|
popd # build
|
||||||
|
|
||||||
|
popd # vcpkg-tool
|
||||||
|
|
||||||
|
pushd vcpkg
|
||||||
|
|
||||||
|
git reset --hard "${VCPKG_COMMIT_ID}"
|
||||||
|
|
||||||
|
cp -a ../vcpkg-tool/build/vcpkg vcpkg
|
||||||
|
|
||||||
|
# disable telemetry
|
||||||
|
|
||||||
|
touch "vcpkg.disable-metrics"
|
||||||
|
|
||||||
|
popd # vcpkg
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install depot-tools for x86
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
if [ ! -d "${HOME}/depot_tools" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
--depth 1 \
|
||||||
|
https://chromium.googlesource.com/chromium/tools/depot_tools.git
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Patch the RustDesk sources
|
||||||
|
|
||||||
|
git apply res/fdroid/patches/*.patch
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/gms/d' \
|
||||||
|
flutter/android/build.gradle \
|
||||||
|
flutter/android/app/build.gradle
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/firebase_analytics/d' \
|
||||||
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/ firebase/,/ version/d' \
|
||||||
|
flutter/pubspec.lock
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/firebase/Id' \
|
||||||
|
flutter/lib/main.dart
|
||||||
|
|
||||||
|
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
|
||||||
|
# Fix for android 3.13.9
|
||||||
|
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
||||||
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
set --
|
||||||
|
|
||||||
|
while read -r _1; do
|
||||||
|
set -- "$@" "${_1}"
|
||||||
|
done 0<<.a
|
||||||
|
$(find flutter/lib/ -type f -name "*dart*")
|
||||||
|
.a
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
set --
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
|
||||||
|
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
# build: perform actual build of APK file
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# Extract required versions for NDK, Rust, Flutter from
|
||||||
|
# '.github/workflows/flutter-build.yml'
|
||||||
|
#
|
||||||
|
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NDK_VERSION="$(yq -r \
|
||||||
|
.env.NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
# Map NDK version to revision
|
||||||
|
|
||||||
|
NDK_VERSION="$(wget \
|
||||||
|
-qO- \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
'https://api.github.com/repos/android/ndk/releases' |
|
||||||
|
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||||
|
|
||||||
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
|
||||||
|
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||||
|
. "${HOME}/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download Flutter dependencies
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
flutter packages pub get
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
# Generate FFI bindings
|
||||||
|
|
||||||
|
flutter_rust_bridge_codegen \
|
||||||
|
--rust-input ./src/flutter_ffi.rs \
|
||||||
|
--dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
|
# Build host android deps
|
||||||
|
|
||||||
|
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
||||||
|
|
||||||
|
# Build rustdesk lib
|
||||||
|
|
||||||
|
cargo ndk \
|
||||||
|
--platform 21 \
|
||||||
|
--target "${RUST_TARGET}" \
|
||||||
|
--bindgen \
|
||||||
|
build \
|
||||||
|
--release \
|
||||||
|
--features "${RUSTDESK_FEATURES}"
|
||||||
|
|
||||||
|
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
|
||||||
|
|
||||||
|
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
|
||||||
|
|
||||||
|
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
|
||||||
|
|
||||||
|
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
|
||||||
|
|
||||||
|
# Build flutter-jit-release for x86
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
pushd flutter-sdk
|
||||||
|
|
||||||
|
echo "## Sync flutter engine sources"
|
||||||
|
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
|
||||||
|
|
||||||
|
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
|
||||||
|
|
||||||
|
export FAKEROOTDONTTRYCHOWN=1
|
||||||
|
|
||||||
|
fakeroot gclient sync
|
||||||
|
|
||||||
|
unset FAKEROOTDONTTRYCHOWN
|
||||||
|
|
||||||
|
pushd src
|
||||||
|
|
||||||
|
echo "## Patch away Google Play dependencies"
|
||||||
|
|
||||||
|
rm \
|
||||||
|
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
|
||||||
|
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/PlayStore/d' \
|
||||||
|
flutter/tools/android_lint/project.xml \
|
||||||
|
flutter/shell/platform/android/BUILD.gn
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/com.google.android.play/d' \
|
||||||
|
flutter/tools/androidx/files.json
|
||||||
|
|
||||||
|
echo "## Configure android engine build"
|
||||||
|
|
||||||
|
flutter/tools/gn \
|
||||||
|
--android --android-cpu x86 --runtime-mode=jit_release \
|
||||||
|
--no-goma --no-enable-unittests
|
||||||
|
|
||||||
|
echo "## Perform android engine build"
|
||||||
|
|
||||||
|
ninja -C out/android_jit_release_x86
|
||||||
|
|
||||||
|
echo "## Configure host engine build"
|
||||||
|
|
||||||
|
flutter/tools/gn \
|
||||||
|
--android-cpu x86 --runtime-mode=jit_release \
|
||||||
|
--no-goma --no-enable-unittests
|
||||||
|
|
||||||
|
echo "## Perform android engine build"
|
||||||
|
|
||||||
|
ninja -C out/host_jit_release_x86
|
||||||
|
|
||||||
|
echo "## Rename host engine"
|
||||||
|
|
||||||
|
mv out/host_jit_release_x86 out/host_jit_release
|
||||||
|
|
||||||
|
echo "## Mimic jit_release engine to debug to use with flutter build apk"
|
||||||
|
|
||||||
|
pushd out/android_jit_release_x86
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
flutter_embedding_jit_release.maven-metadata.xml \
|
||||||
|
1>flutter_embedding_debug.maven-metadata.xml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
flutter_embedding_jit_release.pom \
|
||||||
|
1>flutter_embedding_debug.pom
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
x86_jit_release.maven-metadata.xml \
|
||||||
|
1>x86_debug.maven-metadata.xml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
x86_jit_release.pom \
|
||||||
|
1>x86_debug.pom
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
flutter_embedding_jit_release-sources.jar \
|
||||||
|
flutter_embedding_debug-sources.jar
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
flutter_embedding_jit_release.jar \
|
||||||
|
flutter_embedding_debug.jar
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
x86_jit_release.jar \
|
||||||
|
x86_debug.jar
|
||||||
|
|
||||||
|
popd # out/android_jit_release_x86
|
||||||
|
|
||||||
|
popd # src
|
||||||
|
|
||||||
|
popd # flutter-sdk
|
||||||
|
|
||||||
|
echo "# Clean up intermediate engine files and show free space"
|
||||||
|
|
||||||
|
rm -rf \
|
||||||
|
flutter-sdk/src/out/android_jit_release_x86/obj \
|
||||||
|
flutter-sdk/src/out/host_jit_release/obj
|
||||||
|
|
||||||
|
mv flutter-sdk/src/out flutter-out
|
||||||
|
|
||||||
|
rm -rf flutter-sdk
|
||||||
|
|
||||||
|
mkdir -p flutter-sdk/src/
|
||||||
|
|
||||||
|
mv flutter-out flutter-sdk/src/out
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the apk
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
flutter build apk \
|
||||||
|
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
|
||||||
|
--local-engine=android_jit_release_x86 \
|
||||||
|
--debug \
|
||||||
|
--build-number="${VERCODE}" \
|
||||||
|
--build-name="${VERNAME}" \
|
||||||
|
--target-platform "${FLUTTER_TARGET}"
|
||||||
|
else
|
||||||
|
flutter build apk \
|
||||||
|
--release \
|
||||||
|
--build-number="${VERCODE}" \
|
||||||
|
--build-name="${VERNAME}" \
|
||||||
|
--target-platform "${FLUTTER_TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
rm -rf flutter-sdk
|
||||||
|
|
||||||
|
# Special step for fdroiddata CI builds to remove .gitconfig
|
||||||
|
|
||||||
|
rm -f /home/vagrant/.gitconfig
|
||||||
|
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Report success
|
||||||
|
|
||||||
|
echo "All done!"
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
<key>com.apple.developer.networking.wifi-info</key>
|
<key>com.apple.developer.networking.wifi-info</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo build --features flutter --release --target aarch64-apple-ios --lib
|
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||||
|
|||||||
@@ -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/refresh_wrapper.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/main.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/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uni_links/uni_links.dart';
|
import 'package:uni_links/uni_links.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -550,7 +550,8 @@ class MyTheme {
|
|||||||
Get.changeThemeMode(mode);
|
Get.changeThemeMode(mode);
|
||||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||||
if (mode == ThemeMode.system) {
|
if (mode == ThemeMode.system) {
|
||||||
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
|
await bind.mainSetLocalOption(
|
||||||
|
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||||
} else {
|
} else {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||||
@@ -717,7 +718,21 @@ class OverlayDialogManager {
|
|||||||
int _tagCount = 0;
|
int _tagCount = 0;
|
||||||
|
|
||||||
OverlayEntry? _mobileActionsOverlayEntry;
|
OverlayEntry? _mobileActionsOverlayEntry;
|
||||||
RxBool mobileActionsOverlayVisible = false.obs;
|
RxBool mobileActionsOverlayVisible = true.obs;
|
||||||
|
|
||||||
|
setMobileActionsOverlayVisible(bool v, {store = true}) {
|
||||||
|
if (store) {
|
||||||
|
bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
|
||||||
|
}
|
||||||
|
// No need to read the value from local storage after setting it.
|
||||||
|
// It better to toggle the value directly.
|
||||||
|
mobileActionsOverlayVisible.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMobileActionsOverlayVisible() {
|
||||||
|
mobileActionsOverlayVisible.value =
|
||||||
|
bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
|
||||||
|
}
|
||||||
|
|
||||||
void setOverlayState(OverlayKeyState overlayKeyState) {
|
void setOverlayState(OverlayKeyState overlayKeyState) {
|
||||||
_overlayKeyState = overlayKeyState;
|
_overlayKeyState = overlayKeyState;
|
||||||
@@ -858,40 +873,20 @@ class OverlayDialogManager {
|
|||||||
final overlayState = _overlayKeyState.state;
|
final overlayState = _overlayKeyState.state;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
|
|
||||||
// compute overlay position
|
final overlay = makeMobileActionsOverlayEntry(
|
||||||
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
() => hideMobileActionsOverlay(),
|
||||||
final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
|
ffi: ffi,
|
||||||
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(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
overlayState.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
_mobileActionsOverlayEntry = overlay;
|
_mobileActionsOverlayEntry = overlay;
|
||||||
mobileActionsOverlayVisible.value = true;
|
setMobileActionsOverlayVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void hideMobileActionsOverlay() {
|
void hideMobileActionsOverlay({store = true}) {
|
||||||
if (_mobileActionsOverlayEntry != null) {
|
if (_mobileActionsOverlayEntry != null) {
|
||||||
_mobileActionsOverlayEntry!.remove();
|
_mobileActionsOverlayEntry!.remove();
|
||||||
_mobileActionsOverlayEntry = null;
|
_mobileActionsOverlayEntry = null;
|
||||||
mobileActionsOverlayVisible.value = false;
|
setMobileActionsOverlayVisible(false, store: store);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -909,6 +904,47 @@ class OverlayDialogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||||
|
makeMobileActions(BuildContext context, double s) {
|
||||||
|
final scale = s < 0.85 ? 0.85 : s;
|
||||||
|
final session = ffi ?? gFFI;
|
||||||
|
const double overlayW = 200;
|
||||||
|
const double overlayH = 45;
|
||||||
|
computeOverlayPosition() {
|
||||||
|
final screenW = MediaQuery.of(context).size.width;
|
||||||
|
final screenH = MediaQuery.of(context).size.height;
|
||||||
|
final left = (screenW - overlayW * scale) / 2;
|
||||||
|
final top = screenH - (overlayH + 80) * scale;
|
||||||
|
return Offset(left, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggablePositions.mobileActions.isInvalid()) {
|
||||||
|
draggablePositions.mobileActions.update(computeOverlayPosition());
|
||||||
|
} else {
|
||||||
|
draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
|
||||||
|
}
|
||||||
|
return DraggableMobileActions(
|
||||||
|
scale: scale,
|
||||||
|
position: draggablePositions.mobileActions,
|
||||||
|
width: overlayW,
|
||||||
|
height: overlayH,
|
||||||
|
onBackPressed: session.inputModel.onMobileBack,
|
||||||
|
onHomePressed: session.inputModel.onMobileHome,
|
||||||
|
onRecentPressed: session.inputModel.onMobileApps,
|
||||||
|
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)}) {
|
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
||||||
final overlayState = globalKey.currentState?.overlay;
|
final overlayState = globalKey.currentState?.overlay;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
@@ -988,7 +1024,8 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||||
} else if (!tabTapped &&
|
} else if (!tabTapped &&
|
||||||
onSubmit != null &&
|
onSubmit != null &&
|
||||||
key.logicalKey == LogicalKeyboardKey.enter) {
|
(key.logicalKey == LogicalKeyboardKey.enter ||
|
||||||
|
key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
||||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
@@ -1025,7 +1062,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
bool hasOk = false;
|
bool hasOk = false;
|
||||||
submit() {
|
submit() {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||||
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
||||||
closeConnection();
|
closeConnection();
|
||||||
}
|
}
|
||||||
@@ -1059,21 +1096,33 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (reconnect != null &&
|
if (reconnect != null && title == "Connection Error") {
|
||||||
title == "Connection Error" &&
|
|
||||||
reconnectTimeout != null) {
|
|
||||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||||
final enabled = true.obs;
|
final enabled = true.obs;
|
||||||
final button = Obx(() => _ReconnectCountDownButton(
|
final button = reconnectTimeout != null
|
||||||
second: reconnectTimeout,
|
? Obx(() => _ReconnectCountDownButton(
|
||||||
onPressed: enabled.isTrue
|
second: reconnectTimeout,
|
||||||
? () {
|
onPressed: enabled.isTrue
|
||||||
// Disable the button
|
? () {
|
||||||
enabled.value = false;
|
// Disable the button
|
||||||
reconnect(dialogManager, sessionId, false);
|
enabled.value = false;
|
||||||
}
|
reconnect(dialogManager, sessionId, false);
|
||||||
: null,
|
}
|
||||||
));
|
: null,
|
||||||
|
))
|
||||||
|
: Obx(
|
||||||
|
() => dialogButton(
|
||||||
|
'Reconnect',
|
||||||
|
isOutline: true,
|
||||||
|
onPressed: enabled.isTrue
|
||||||
|
? () {
|
||||||
|
// Disable the button
|
||||||
|
enabled.value = false;
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
buttons.insert(0, button);
|
buttons.insert(0, button);
|
||||||
}
|
}
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
@@ -1391,7 +1440,7 @@ Future<void> initGlobalFFI() async {
|
|||||||
_globalFFI = FFI(null);
|
_globalFFI = FFI(null);
|
||||||
debugPrint("_globalFFI init end");
|
debugPrint("_globalFFI init end");
|
||||||
// after `put`, can also be globally found by Get.find<FFI>();
|
// after `put`, can also be globally found by Get.find<FFI>();
|
||||||
Get.put(_globalFFI, permanent: true);
|
Get.put<FFI>(_globalFFI, permanent: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
String translate(String name) {
|
String translate(String name) {
|
||||||
@@ -1401,14 +1450,16 @@ String translate(String name) {
|
|||||||
return platformFFI.translate(name, localeName);
|
return platformFFI.translate(name, localeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function must be kept the same as the one in rust and sciter code.
|
||||||
|
// rust: libs/hbb_common/src/config.rs -> option2bool()
|
||||||
|
// sciter: Does not have the function, but it should be kept the same.
|
||||||
bool option2bool(String option, String value) {
|
bool option2bool(String option, String value) {
|
||||||
bool res;
|
bool res;
|
||||||
if (option.startsWith("enable-")) {
|
if (option.startsWith("enable-")) {
|
||||||
res = value != "N";
|
res = value != "N";
|
||||||
} else if (option.startsWith("allow-") ||
|
} else if (option.startsWith("allow-") ||
|
||||||
option == "stop-service" ||
|
option == kOptionStopService ||
|
||||||
option == "direct-server" ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = value == "Y";
|
res = value == "Y";
|
||||||
} else {
|
} else {
|
||||||
@@ -1421,13 +1472,12 @@ bool option2bool(String option, String value) {
|
|||||||
String bool2option(String option, bool b) {
|
String bool2option(String option, bool b) {
|
||||||
String res;
|
String res;
|
||||||
if (option.startsWith('enable-')) {
|
if (option.startsWith('enable-')) {
|
||||||
res = b ? '' : 'N';
|
res = b ? defaultOptionYes : 'N';
|
||||||
} else if (option.startsWith('allow-') ||
|
} else if (option.startsWith('allow-') ||
|
||||||
option == "stop-service" ||
|
option == kOptionStopService ||
|
||||||
option == "direct-server" ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = b ? 'Y' : '';
|
res = b ? 'Y' : defaultOptionNo;
|
||||||
} else {
|
} else {
|
||||||
assert(false);
|
assert(false);
|
||||||
res = b ? 'Y' : 'N';
|
res = b ? 'Y' : 'N';
|
||||||
@@ -1461,9 +1511,9 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
|
|||||||
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
|
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
|
||||||
}
|
}
|
||||||
|
|
||||||
mainSetPeerBoolOptionSync(String id, String key, bool v) {
|
// Don't use `option2bool()` and `bool2option()` to convert the session option.
|
||||||
bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v));
|
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
|
||||||
}
|
// Because all session options use `Y` and `<Empty>` as values.
|
||||||
|
|
||||||
Future<bool> matchPeer(String searchText, Peer peer) async {
|
Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||||
if (searchText.isEmpty) {
|
if (searchText.isEmpty) {
|
||||||
@@ -1499,6 +1549,12 @@ Widget getPlatformImage(String platform, {double size = 50}) {
|
|||||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OffsetDevicePixelRatio {
|
||||||
|
Offset offset;
|
||||||
|
final double devicePixelRatio;
|
||||||
|
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
class LastWindowPosition {
|
class LastWindowPosition {
|
||||||
double? width;
|
double? width;
|
||||||
double? height;
|
double? height;
|
||||||
@@ -1560,16 +1616,13 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
late Offset position;
|
late Offset position;
|
||||||
late Size sz;
|
late Size sz;
|
||||||
late bool isMaximized;
|
late bool isMaximized;
|
||||||
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
|
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||||
(isMacOS && stateGlobal.closeOnFullscreen == true);
|
setPreFrame() {
|
||||||
setFrameIfMaximized() {
|
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||||
if (isMaximized) {
|
var lpos = LastWindowPosition.loadFromString(pos);
|
||||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
position = Offset(
|
||||||
var lpos = LastWindowPosition.loadFromString(pos);
|
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||||
position = Offset(
|
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
|
||||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -1581,23 +1634,30 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
||||||
isMaximized =
|
isMaximized =
|
||||||
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
||||||
position = await windowManager.getPosition();
|
if (isFullscreen || isMaximized) {
|
||||||
sz = await windowManager.getSize();
|
setPreFrame();
|
||||||
setFrameIfMaximized();
|
} else {
|
||||||
|
position = await windowManager.getPosition();
|
||||||
|
sz = await windowManager.getSize();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
isMaximized = await wc.isMaximized();
|
isMaximized = await wc.isMaximized();
|
||||||
final Rect frame;
|
if (isFullscreen || isMaximized) {
|
||||||
try {
|
setPreFrame();
|
||||||
frame = await wc.getFrame();
|
} else {
|
||||||
} catch (e) {
|
final Rect frame;
|
||||||
debugPrint("Failed to get frame of window $windowId, it may be hidden");
|
try {
|
||||||
return;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@@ -1631,7 +1691,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
|
|||||||
final remoteList = await DesktopMultiWindow.invokeMethod(
|
final remoteList = await DesktopMultiWindow.invokeMethod(
|
||||||
windowId, kWindowEventGetRemoteList, null);
|
windowId, kWindowEventGetRemoteList, null);
|
||||||
getPeerPos(String peerId) {
|
getPeerPos(String peerId) {
|
||||||
if (isMaximized) {
|
if (isMaximized || isFullscreen) {
|
||||||
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||||
id: peerId, k: windowFramePrefix + windowType.name);
|
id: peerId, k: windowFramePrefix + windowType.name);
|
||||||
var lpos = LastWindowPosition.loadFromString(peerPos);
|
var lpos = LastWindowPosition.loadFromString(peerPos);
|
||||||
@@ -1688,8 +1748,15 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
|
|||||||
return Size(restoreWidth, restoreHeight);
|
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
|
/// return null means center
|
||||||
Future<Offset?> _adjustRestoreMainWindowOffset(
|
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||||
double? left,
|
double? left,
|
||||||
double? top,
|
double? top,
|
||||||
double? width,
|
double? width,
|
||||||
@@ -1703,9 +1770,13 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
|||||||
double? frameTop;
|
double? frameTop;
|
||||||
double? frameRight;
|
double? frameRight;
|
||||||
double? frameBottom;
|
double? frameBottom;
|
||||||
|
double devicePixelRatio = 1.0;
|
||||||
|
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
for (final screen in await window_size.getScreenList()) {
|
for (final screen in await window_size.getScreenList()) {
|
||||||
|
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
|
||||||
|
devicePixelRatio = screen.scaleFactor;
|
||||||
|
}
|
||||||
frameLeft = frameLeft == null
|
frameLeft = frameLeft == null
|
||||||
? screen.visibleFrame.left
|
? screen.visibleFrame.left
|
||||||
: min(screen.visibleFrame.left, frameLeft);
|
: min(screen.visibleFrame.left, frameLeft);
|
||||||
@@ -1739,7 +1810,7 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
|||||||
top < frameTop!) {
|
top < frameTop!) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return Offset(left, top);
|
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,15 +1836,10 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||||
// Though "open in tabs" is true and the new window restore peer position, it's ok.
|
// 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 (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
|
||||||
// If the restore position is called by main window, and the peer id is not null
|
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||||
// then we may need to get the position by reading the peer config.
|
id: peerId, k: windowFramePrefix + type.name);
|
||||||
// Because the session may not be read at this time.
|
if (peerPos.isNotEmpty) {
|
||||||
if (desktopType == DesktopType.main) {
|
pos = peerPos;
|
||||||
pos = bind.mainGetPeerFlutterOptionSync(
|
|
||||||
id: peerId, k: windowFramePrefix + type.name);
|
|
||||||
} else {
|
|
||||||
pos = await bind.sessionGetFlutterOptionByPeerId(
|
|
||||||
id: peerId, k: windowFramePrefix + type.name);
|
|
||||||
}
|
}
|
||||||
isRemotePeerPos = pos != null;
|
isRemotePeerPos = pos != null;
|
||||||
}
|
}
|
||||||
@@ -1804,22 +1870,47 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
}
|
}
|
||||||
|
|
||||||
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
||||||
final offset = await _adjustRestoreMainWindowOffset(
|
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
|
||||||
lpos.offsetWidth,
|
lpos.offsetWidth,
|
||||||
lpos.offsetHeight,
|
lpos.offsetHeight,
|
||||||
size.width,
|
size.width,
|
||||||
size.height,
|
size.height,
|
||||||
);
|
);
|
||||||
debugPrint(
|
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) {
|
switch (type) {
|
||||||
case WindowType.Main:
|
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 {
|
restorePos() async {
|
||||||
if (offset == null) {
|
if (offsetDevicePixelRatio == null) {
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
} else {
|
} else {
|
||||||
await windowManager.setPosition(offset);
|
await windowManager.setPosition(offsetDevicePixelRatio.offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isMaximized == true) {
|
if (lpos.isMaximized == true) {
|
||||||
@@ -1837,19 +1928,27 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
restoreFrame() async {
|
restoreFrame() async {
|
||||||
if (offset == null) {
|
if (offsetDevicePixelRatio == null) {
|
||||||
await wc.center();
|
await wc.center();
|
||||||
} else {
|
} else {
|
||||||
final frame =
|
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
|
||||||
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
offsetDevicePixelRatio.offset.dy, size.width, size.height);
|
||||||
await wc.setFrame(frame);
|
await wc.setFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isFullscreen == true) {
|
if (lpos.isFullscreen == true) {
|
||||||
await restoreFrame();
|
if (!isMacOS) {
|
||||||
|
await restoreFrame();
|
||||||
|
}
|
||||||
// An duration is needed to avoid the window being restored after fullscreen.
|
// An duration is needed to avoid the window being restored after fullscreen.
|
||||||
Future.delayed(Duration(milliseconds: 300), () async {
|
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) {
|
} else if (lpos.isMaximized == true) {
|
||||||
await restoreFrame();
|
await restoreFrame();
|
||||||
@@ -2315,7 +2414,13 @@ Future<void> onActiveWindowChanged() async {
|
|||||||
await windowManager.setPreventClose(false);
|
await windowManager.setPreventClose(false);
|
||||||
await windowManager.close();
|
await windowManager.close();
|
||||||
if (isMacOS) {
|
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2593,36 +2698,55 @@ Future<void> start_service(bool is_start) async {
|
|||||||
!isMacOS ||
|
!isMacOS ||
|
||||||
await callMainCheckSuperUserPermission();
|
await callMainCheckSuperUserPermission();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
|
mainSetBoolOption(kOptionStopService, !is_start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> canBeBlocked() async {
|
||||||
|
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||||
|
var option = option2bool(kOptionAllowRemoteConfigModification,
|
||||||
|
await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
|
||||||
|
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
|
||||||
|
if (use != null && !await use()) {
|
||||||
|
block.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var time0 = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
await bind.mainCheckMouseTime();
|
||||||
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
|
var d = time0 - await bind.mainGetMouseTime();
|
||||||
|
if (d < 120) {
|
||||||
|
block.value = true;
|
||||||
|
} else {
|
||||||
|
block.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
||||||
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
Widget buildRemoteBlock(
|
||||||
var block = false.obs;
|
{required Widget child,
|
||||||
|
required RxBool block,
|
||||||
|
required bool mask,
|
||||||
|
WhetherUseRemoteBlock? use}) {
|
||||||
return Obx(() => MouseRegion(
|
return Obx(() => MouseRegion(
|
||||||
onEnter: (_) async {
|
onEnter: (_) async {
|
||||||
if (use != null && !await use()) {
|
await shouldBeBlocked(block, use);
|
||||||
block.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var time0 = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
await bind.mainCheckMouseTime();
|
|
||||||
Timer(const Duration(milliseconds: 120), () async {
|
|
||||||
var d = time0 - await bind.mainGetMouseTime();
|
|
||||||
if (d < 120) {
|
|
||||||
block.value = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onExit: (event) => block.value = false,
|
onExit: (event) => block.value = false,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
child,
|
// scope block tab
|
||||||
Offstage(
|
FocusScope(child: child, canRequestFocus: !block.value),
|
||||||
offstage: !block.value,
|
// mask block click, cm not block click and still use check_click_time to avoid block local click
|
||||||
child: Container(
|
if (mask)
|
||||||
color: Colors.black.withOpacity(0.5),
|
Offstage(
|
||||||
)),
|
offstage: !block.value,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
)),
|
||||||
]),
|
]),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -2670,12 +2794,10 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
required RxString err,
|
required RxString err,
|
||||||
required Function? retry,
|
required Function? retry,
|
||||||
required Function close}) {
|
required Function close}) {
|
||||||
const double height = 25;
|
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
offstage: !(!loading.value && err.value.isNotEmpty),
|
offstage: !(!loading.value && err.value.isNotEmpty),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: height,
|
|
||||||
color: MyTheme.color(context).errorBannerBg,
|
color: MyTheme.color(context).errorBannerBg,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -2694,7 +2816,6 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
message: translate(err.value),
|
message: translate(err.value),
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(err.value),
|
translate(err.value),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
)).marginSymmetric(vertical: 2),
|
)).marginSymmetric(vertical: 2),
|
||||||
),
|
),
|
||||||
@@ -2749,11 +2870,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 {
|
Future<List<Rect>> getScreenListWayland() async {
|
||||||
final screenRectList = <Rect>[];
|
final screenRectList = <Rect>[];
|
||||||
if (isMainDesktopWindow) {
|
if (isMainDesktopWindow) {
|
||||||
@@ -2852,6 +2968,16 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
|||||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNewConnectWindowFrame(
|
||||||
|
int windowId, String peerId, int? display, Rect? screenRect) async {
|
||||||
|
if (screenRect == null) {
|
||||||
|
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||||
|
windowId: windowId, display: display, peerId: peerId);
|
||||||
|
} else {
|
||||||
|
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
||||||
if (screenRect == null) {
|
if (screenRect == null) {
|
||||||
return;
|
return;
|
||||||
@@ -2972,9 +3098,16 @@ Future<bool> setServerConfig(
|
|||||||
List<RxString>? errMsgs,
|
List<RxString>? errMsgs,
|
||||||
ServerConfig config,
|
ServerConfig config,
|
||||||
) async {
|
) async {
|
||||||
config.idServer = config.idServer.trim();
|
String removeEndSlash(String input) {
|
||||||
config.relayServer = config.relayServer.trim();
|
if (input.endsWith('/')) {
|
||||||
config.apiServer = config.apiServer.trim();
|
return input.substring(0, input.length - 1);
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.idServer = removeEndSlash(config.idServer.trim());
|
||||||
|
config.relayServer = removeEndSlash(config.relayServer.trim());
|
||||||
|
config.apiServer = removeEndSlash(config.apiServer.trim());
|
||||||
config.key = config.key.trim();
|
config.key = config.key.trim();
|
||||||
if (controllers != null) {
|
if (controllers != null) {
|
||||||
controllers[0].text = config.idServer;
|
controllers[0].text = config.idServer;
|
||||||
@@ -3193,6 +3326,11 @@ Widget buildPresetPasswordWarning() {
|
|||||||
return Text(
|
return Text(
|
||||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||||
} else if (snapshot.hasData && snapshot.data == true) {
|
} else if (snapshot.hasData && snapshot.data == true) {
|
||||||
|
if (bind.mainGetBuildinOption(
|
||||||
|
key: kOptionRemovePresetPasswordWarning) !=
|
||||||
|
'N') {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.yellow,
|
color: Colors.yellow,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -3202,7 +3340,8 @@ Widget buildPresetPasswordWarning() {
|
|||||||
translate("Security Alert"),
|
translate("Security Alert"),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontSize: 20,
|
fontSize:
|
||||||
|
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
)).paddingOnly(bottom: 8),
|
)).paddingOnly(bottom: 8),
|
||||||
@@ -3281,3 +3420,44 @@ setResizable(bool resizable) {
|
|||||||
windowManager.setResizable(resizable);
|
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' : '';
|
||||||
|
|
||||||
|
bool whitelistNotEmpty() {
|
||||||
|
// https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
|
||||||
|
final v = bind.mainGetOptionSync(key: kOptionWhitelist);
|
||||||
|
return v != '' && v != ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
// `setMovable()` is only supported on macOS.
|
||||||
|
//
|
||||||
|
// On macOS, the window can be dragged by the tab bar by default.
|
||||||
|
// We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
|
||||||
|
//
|
||||||
|
// When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
|
||||||
|
//
|
||||||
|
// See the following code for more details:
|
||||||
|
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
|
||||||
|
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
|
||||||
|
//
|
||||||
|
// @platforms macos
|
||||||
|
disableWindowMovable(int? windowId) {
|
||||||
|
if (!isMacOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowId == null) {
|
||||||
|
windowManager.setMovable(false);
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(windowId).setMovable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ class PrivacyModeState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString state = ''.obs;
|
final RxString state = ''.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxString>(state, tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = '';
|
Get.find<RxString>(tag: key).value = '';
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,9 @@ class BlockInputState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ class BlockInputState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +56,9 @@ class CurrentDisplayState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 0;
|
Get.find<RxInt>(tag: key).value = 0;
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,8 @@ class CurrentDisplayState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,16 +105,16 @@ class ConnectionTypeState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<ConnectionType>(tag: key)) {
|
||||||
final ConnectionType collectionType = ConnectionType();
|
final ConnectionType collectionType = ConnectionType();
|
||||||
Get.put(collectionType, tag: key);
|
Get.put<ConnectionType>(collectionType, tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<ConnectionType>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<ConnectionType>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ class FingerprintState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString state = ''.obs;
|
final RxString state = ''.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxString>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = '';
|
Get.find<RxString>(tag: key).value = '';
|
||||||
}
|
}
|
||||||
@@ -137,8 +137,8 @@ class FingerprintState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -160,8 +160,8 @@ class ShowRemoteCursorState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +173,9 @@ class ShowRemoteCursorLockState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -183,8 +183,8 @@ class ShowRemoteCursorLockState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +196,10 @@ class KeyboardEnabledState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
// Server side, default true
|
// Server side, default true
|
||||||
final RxBool state = true.obs;
|
final RxBool state = true.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = true;
|
Get.find<RxBool>(tag: key).value = true;
|
||||||
}
|
}
|
||||||
@@ -207,8 +207,8 @@ class KeyboardEnabledState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,9 +220,9 @@ class RemoteCursorMovedState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -230,8 +230,8 @@ class RemoteCursorMovedState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +243,9 @@ class RemoteCountState {
|
|||||||
|
|
||||||
static void init() {
|
static void init() {
|
||||||
final key = tag();
|
final key = tag();
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = 1.obs;
|
final RxInt state = 1.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 1;
|
Get.find<RxInt>(tag: key).value = 1;
|
||||||
}
|
}
|
||||||
@@ -253,8 +253,8 @@ class RemoteCountState {
|
|||||||
|
|
||||||
static void delete() {
|
static void delete() {
|
||||||
final key = tag();
|
final key = tag();
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +266,9 @@ class PeerBoolOption {
|
|||||||
|
|
||||||
static void init(String id, String opt, bool Function() init_getter) {
|
static void init(String id, String opt, bool Function() init_getter) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool value = RxBool(init_getter());
|
final RxBool value = RxBool(init_getter());
|
||||||
Get.put(value, tag: key);
|
Get.put<RxBool>(value, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = init_getter();
|
Get.find<RxBool>(tag: key).value = init_getter();
|
||||||
}
|
}
|
||||||
@@ -276,8 +276,8 @@ class PeerBoolOption {
|
|||||||
|
|
||||||
static void delete(String id, String opt) {
|
static void delete(String id, String opt) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,9 +290,9 @@ class PeerStringOption {
|
|||||||
|
|
||||||
static void init(String id, String opt, String Function() init_getter) {
|
static void init(String id, String opt, String Function() init_getter) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString value = RxString(init_getter());
|
final RxString value = RxString(init_getter());
|
||||||
Get.put(value, tag: key);
|
Get.put<RxString>(value, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = init_getter();
|
Get.find<RxString>(tag: key).value = init_getter();
|
||||||
}
|
}
|
||||||
@@ -300,8 +300,8 @@ class PeerStringOption {
|
|||||||
|
|
||||||
static void delete(String id, String opt) {
|
static void delete(String id, String opt) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +314,9 @@ class UnreadChatCountState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 0;
|
Get.find<RxInt>(tag: key).value = 0;
|
||||||
}
|
}
|
||||||
@@ -324,8 +324,8 @@ class UnreadChatCountState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ initSharedStates(String id) {
|
|||||||
ShowRemoteCursorLockState.init(id);
|
ShowRemoteCursorLockState.init(id);
|
||||||
RemoteCursorMovedState.init(id);
|
RemoteCursorMovedState.init(id);
|
||||||
FingerprintState.init(id);
|
FingerprintState.init(id);
|
||||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
PeerBoolOption.init(id, kOptionZoomCursor, () => false);
|
||||||
UnreadChatCountState.init(id);
|
UnreadChatCountState.init(id);
|
||||||
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ removeSharedStates(String id) {
|
|||||||
KeyboardEnabledState.delete(id);
|
KeyboardEnabledState.delete(id);
|
||||||
RemoteCursorMovedState.delete(id);
|
RemoteCursorMovedState.delete(id);
|
||||||
FingerprintState.delete(id);
|
FingerprintState.delete(id);
|
||||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
PeerBoolOption.delete(id, kOptionZoomCursor);
|
||||||
UnreadChatCountState.delete(id);
|
UnreadChatCountState.delete(id);
|
||||||
if (isMobile) ConnectionTypeState.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/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.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/desktop/widgets/popup_menu.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
@@ -109,6 +110,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddressBookMobile() {
|
Widget _buildAddressBookMobile() {
|
||||||
|
const padding = 8.0;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -119,7 +121,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.background)),
|
color: Theme.of(context).colorScheme.background)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding:
|
||||||
|
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -129,7 +132,6 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: _buildTags(),
|
child: _buildTags(),
|
||||||
),
|
),
|
||||||
_buildAbPermission(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -189,42 +191,73 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (!names.contains(gFFI.abModel.currentName.value)) {
|
if (!names.contains(gFFI.abModel.currentName.value)) {
|
||||||
return Offstage();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Row buildItem(String e, {bool button = false}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: gFFI.abModel.translatedName(e),
|
||||||
|
child: Text(
|
||||||
|
gFFI.abModel.translatedName(e),
|
||||||
|
style: button ? null : TextStyle(fontSize: 14.0),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: button ? TextAlign.center : null,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = names
|
||||||
|
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
|
||||||
|
.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 TextEditingController textEditingController = TextEditingController();
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
|
||||||
return DropdownButton2<String>(
|
return DropdownButton2<String>(
|
||||||
value: gFFI.abModel.currentName.value,
|
value: gFFI.abModel.currentName.value,
|
||||||
onChanged: (value) {
|
onChanged: isOptFixed
|
||||||
if (value != null) {
|
? null
|
||||||
gFFI.abModel.setCurrentName(value);
|
: (value) {
|
||||||
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
|
if (value != null) {
|
||||||
}
|
gFFI.abModel.setCurrentName(value);
|
||||||
},
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customButton: Container(
|
||||||
|
height: isDesktop ? 48 : 40,
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||||
|
Icon(Icons.arrow_drop_down),
|
||||||
|
]),
|
||||||
|
),
|
||||||
underline: Container(
|
underline: Container(
|
||||||
height: 0.7,
|
height: 0.7,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
buttonStyleData: ButtonStyleData(height: 48),
|
menuItemStyleData: menuItemStyleData,
|
||||||
menuItemStyleData: MenuItemStyleData(height: 36),
|
items: items,
|
||||||
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(),
|
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
isDense: true,
|
||||||
dropdownSearchData: DropdownSearchData(
|
dropdownSearchData: DropdownSearchData(
|
||||||
searchController: textEditingController,
|
searchController: textEditingController,
|
||||||
searchInnerWidgetHeight: 50,
|
searchInnerWidgetHeight: 50,
|
||||||
@@ -333,6 +366,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> syncMenuItem() {
|
MenuEntryBase<String> syncMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(syncAbOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Sync with recent sessions'),
|
text: translate('Sync with recent sessions'),
|
||||||
@@ -343,11 +377,13 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
gFFI.abModel.setShouldAsync(v);
|
gFFI.abModel.setShouldAsync(v);
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> sortMenuItem() {
|
MenuEntryBase<String> sortMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(sortAbTagsOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Sort tags'),
|
text: translate('Sort tags'),
|
||||||
@@ -355,15 +391,18 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return shouldSortTags();
|
return shouldSortTags();
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
|
bind.mainSetLocalOption(
|
||||||
|
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
gFFI.abModel.sortTags.value = v;
|
gFFI.abModel.sortTags.value = v;
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> filterMenuItem() {
|
MenuEntryBase<String> filterMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(filterAbTagOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Filter by intersection'),
|
text: translate('Filter by intersection'),
|
||||||
@@ -371,10 +410,12 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return filterAbTagByIntersection();
|
return filterAbTagByIntersection();
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
|
bind.mainSetLocalOption(
|
||||||
|
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
gFFI.abModel.filterByIntersection.value = v;
|
gFFI.abModel.filterByIntersection.value = v;
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
|
const _kWindowsSystemSound = 'System Sound';
|
||||||
|
|
||||||
typedef AudioINputSetDevice = void Function(String device);
|
typedef AudioINputSetDevice = void Function(String device);
|
||||||
typedef AudioInputBuilder = Widget Function(
|
typedef AudioInputBuilder = Widget Function(
|
||||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||||
|
|
||||||
class AudioInput extends StatelessWidget {
|
class AudioInput extends StatelessWidget {
|
||||||
final AudioInputBuilder builder;
|
final AudioInputBuilder builder;
|
||||||
|
final bool isCm;
|
||||||
|
final bool isVoiceCall;
|
||||||
|
|
||||||
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
const AudioInput(
|
||||||
|
{Key? key,
|
||||||
|
required this.builder,
|
||||||
|
required this.isCm,
|
||||||
|
required this.isVoiceCall})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
static String getDefault() {
|
static String getDefault() {
|
||||||
if (isWindows) return translate('System Sound');
|
if (isWindows) return translate('System Sound');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> getValue() async {
|
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
|
||||||
String device = await bind.mainGetOption(key: 'audio-input');
|
if (isVoiceCall) {
|
||||||
|
return bind.getVoiceCallInputDevice(isCm: isCm);
|
||||||
|
} else {
|
||||||
|
return bind.mainGetOption(key: 'audio-input');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
|
||||||
|
String device = await getAudioInput(isCm, isVoiceCall);
|
||||||
if (device.isNotEmpty) {
|
if (device.isNotEmpty) {
|
||||||
return device;
|
return device;
|
||||||
} else {
|
} else {
|
||||||
@@ -25,31 +42,39 @@ class AudioInput extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setDevice(String device) async {
|
static Future<void> setDevice(
|
||||||
|
String device, bool isCm, bool isVoiceCall) async {
|
||||||
if (device == getDefault()) device = '';
|
if (device == getDefault()) device = '';
|
||||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
if (isVoiceCall) {
|
||||||
|
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
|
||||||
|
} else {
|
||||||
|
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, Object>> getDevicesInfo() async {
|
static Future<Map<String, Object>> getDevicesInfo(
|
||||||
|
bool isCm, bool isVoiceCall) async {
|
||||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
devices.insert(0, translate('System Sound'));
|
devices.insert(0, translate(_kWindowsSystemSound));
|
||||||
}
|
}
|
||||||
String current = await getValue();
|
String current = await getValue(isCm, isVoiceCall);
|
||||||
return {'devices': devices, 'current': current};
|
return {'devices': devices, 'current': current};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return futureBuilder(
|
return futureBuilder(
|
||||||
future: getDevicesInfo(),
|
future: getDevicesInfo(isCm, isVoiceCall),
|
||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
String currentDevice = data['current'];
|
String currentDevice = data['current'];
|
||||||
List<String> devices = data['devices'] as List<String>;
|
List<String> devices = data['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return builder(devices, currentDevice, setDevice);
|
return builder(devices, currentDevice, (devices) {
|
||||||
|
setDevice(devices, isCm, isVoiceCall);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@@ -177,11 +178,14 @@ void changeIdDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void changeWhiteList({Function()? callback}) async {
|
void changeWhiteList({Function()? callback}) async {
|
||||||
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(',');
|
final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
|
||||||
var newWhiteListField = newWhiteList.join('\n');
|
var newWhiteListField = curWhiteList == defaultOptionWhitelist
|
||||||
|
? ''
|
||||||
|
: curWhiteList.split(',').join('\n');
|
||||||
var controller = TextEditingController(text: newWhiteListField);
|
var controller = TextEditingController(text: newWhiteListField);
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("IP Whitelisting")),
|
title: Text(translate("IP Whitelisting")),
|
||||||
@@ -201,6 +205,7 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
enabled: !isOptFixed,
|
||||||
autofocus: true),
|
autofocus: true),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -214,45 +219,53 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("Clear", onPressed: () async {
|
if (!isOptFixed)
|
||||||
await bind.mainSetOption(key: 'whitelist', value: '');
|
dialogButton("Clear", onPressed: () async {
|
||||||
callback?.call();
|
await bind.mainSetOption(
|
||||||
close();
|
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||||
}, isOutline: true),
|
|
||||||
dialogButton(
|
|
||||||
"OK",
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
|
||||||
msg = "";
|
|
||||||
isInProgress = true;
|
|
||||||
});
|
|
||||||
newWhiteListField = controller.text.trim();
|
|
||||||
var newWhiteList = "";
|
|
||||||
if (newWhiteListField.isEmpty) {
|
|
||||||
// pass
|
|
||||||
} else {
|
|
||||||
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
|
||||||
// test ip
|
|
||||||
final ipMatch = RegExp(
|
|
||||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
|
||||||
final ipv6Match = RegExp(
|
|
||||||
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
|
||||||
for (final ip in ips) {
|
|
||||||
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
|
||||||
msg = "${translate("Invalid IP")} $ip";
|
|
||||||
setState(() {
|
|
||||||
isInProgress = false;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newWhiteList = ips.join(',');
|
|
||||||
}
|
|
||||||
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
|
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
},
|
}, isOutline: true),
|
||||||
),
|
if (!isOptFixed)
|
||||||
|
dialogButton(
|
||||||
|
"OK",
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
msg = "";
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
newWhiteListField = controller.text.trim();
|
||||||
|
var newWhiteList = "";
|
||||||
|
if (newWhiteListField.isEmpty) {
|
||||||
|
// pass
|
||||||
|
} else {
|
||||||
|
final ips =
|
||||||
|
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||||
|
// test ip
|
||||||
|
final ipMatch = RegExp(
|
||||||
|
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||||
|
final ipv6Match = RegExp(
|
||||||
|
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
||||||
|
for (final ip in ips) {
|
||||||
|
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
||||||
|
msg = "${translate("Invalid IP")} $ip";
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newWhiteList = ips.join(',');
|
||||||
|
}
|
||||||
|
if (newWhiteList.trim().isEmpty) {
|
||||||
|
newWhiteList = defaultOptionWhitelist;
|
||||||
|
}
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionWhitelist, value: newWhiteList);
|
||||||
|
callback?.call();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onCancel: close,
|
onCancel: close,
|
||||||
);
|
);
|
||||||
@@ -298,7 +311,7 @@ Future<String> changeDirectAccessPort(
|
|||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("OK", onPressed: () async {
|
dialogButton("OK", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'direct-access-port', value: controller.text);
|
key: kOptionDirectAccessPort, value: controller.text);
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -345,7 +358,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
|
|||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("OK", onPressed: () async {
|
dialogButton("OK", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'auto-disconnect-timeout', value: controller.text);
|
key: kOptionAutoDisconnectTimeout, value: controller.text);
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -1753,6 +1766,66 @@ void renameDialog(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void changeBot({Function()? callback}) async {
|
||||||
|
if (bind.mainHasValidBotSync()) {
|
||||||
|
await bind.mainSetOption(key: "bot", value: "");
|
||||||
|
callback?.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String errorText = '';
|
||||||
|
bool loading = false;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
onVerify() async {
|
||||||
|
final token = controller.text.trim();
|
||||||
|
if (token == "") return;
|
||||||
|
loading = true;
|
||||||
|
errorText = '';
|
||||||
|
setState(() {});
|
||||||
|
final error = await bind.mainVerifyBot(token: token);
|
||||||
|
if (error == "") {
|
||||||
|
callback?.call();
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
errorText = translate(error);
|
||||||
|
loading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final codeField = TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: translate('Token'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Telegram bot")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SelectableText(translate("enable-bot-desc"),
|
||||||
|
style: TextStyle(fontSize: 12))
|
||||||
|
.marginOnly(bottom: 12),
|
||||||
|
Row(children: [Expanded(child: codeField)]),
|
||||||
|
if (errorText != '')
|
||||||
|
Text(errorText, style: TextStyle(color: Colors.red))
|
||||||
|
.marginOnly(top: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
|
loading
|
||||||
|
? CircularProgressIndicator()
|
||||||
|
: dialogButton("OK", onPressed: onVerify),
|
||||||
|
],
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void change2fa({Function()? callback}) async {
|
void change2fa({Function()? callback}) async {
|
||||||
if (bind.mainHasValid2FaSync()) {
|
if (bind.mainHasValid2FaSync()) {
|
||||||
await bind.mainSetOption(key: "2fa", value: "");
|
await bind.mainSetOption(key: "2fa", value: "");
|
||||||
@@ -2115,3 +2188,31 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
||||||
|
VoidCallback onConfirm) {
|
||||||
|
dialogManager.show((setState, close, context) {
|
||||||
|
submit() {
|
||||||
|
close();
|
||||||
|
onConfirm.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(content,
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
textAlign: TextAlign.start),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 12),
|
||||||
|
actions: [
|
||||||
|
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||||
|
dialogButton(translate("OK"), onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: This debounce logic is not working properly.
|
||||||
|
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
|
||||||
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
||||||
start(ScaleUpdateDetails d) {
|
start(ScaleUpdateDetails d) {
|
||||||
_currentState = GestureState.oneFingerPan;
|
_currentState = GestureState.oneFingerPan;
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ Future<bool?> loginDialog() async {
|
|||||||
}
|
}
|
||||||
if (isEmailVerification != null) {
|
if (isEmailVerification != null) {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (close != null) close(false);
|
if (close != null) close(null);
|
||||||
verificationCodeDialog(
|
verificationCodeDialog(
|
||||||
resp.user, resp.secret, isEmailVerification);
|
resp.user, resp.secret, isEmailVerification);
|
||||||
} else {
|
} else {
|
||||||
@@ -712,6 +712,11 @@ Future<bool?> verificationCodeDialog(
|
|||||||
dialogButton("Verify", onPressed: getOnSubmit()),
|
dialogButton("Verify", onPressed: getOnSubmit()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
|
||||||
|
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
|
||||||
|
if (isMobile && res == true) {
|
||||||
|
await UserModel.updateOtherModels();
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (draggablePositions.chatWindow.isInvalid()) {
|
||||||
|
draggablePositions.chatWindow.update(position);
|
||||||
|
}
|
||||||
return isIOS
|
return isIOS
|
||||||
? IOSDraggable(
|
? IOSDraggable(
|
||||||
position: position,
|
position: draggablePositions.chatWindow,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Draggable(
|
: Draggable(
|
||||||
checkKeyboard: true,
|
checkKeyboard: true,
|
||||||
position: position,
|
position: draggablePositions.chatWindow,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
@@ -166,15 +171,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// floating buttons of back/home/recent actions for android
|
/// floating buttons of back/home/recent actions for android
|
||||||
class DraggableMobileActions extends StatelessWidget {
|
class DraggableMobileActions extends StatelessWidget {
|
||||||
DraggableMobileActions(
|
DraggableMobileActions(
|
||||||
{this.position = Offset.zero,
|
{this.onBackPressed,
|
||||||
this.onBackPressed,
|
|
||||||
this.onRecentPressed,
|
this.onRecentPressed,
|
||||||
this.onHomePressed,
|
this.onHomePressed,
|
||||||
this.onHidePressed,
|
this.onHidePressed,
|
||||||
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height});
|
required this.height,
|
||||||
|
required this.scale});
|
||||||
|
|
||||||
final Offset position;
|
final double scale;
|
||||||
|
final DraggableKeyPosition position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
@@ -186,8 +193,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Draggable(
|
return Draggable(
|
||||||
position: position,
|
position: position,
|
||||||
width: width,
|
width: scale * width,
|
||||||
height: height,
|
height: scale * height,
|
||||||
builder: (_, onPanUpdate) {
|
builder: (_, onPanUpdate) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onPanUpdate: onPanUpdate,
|
onPanUpdate: onPanUpdate,
|
||||||
@@ -197,7 +204,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.accent.withOpacity(0.4),
|
color: MyTheme.accent.withOpacity(0.4),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(15))),
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(15 * scale))),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
@@ -205,17 +213,20 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.arrow_back)),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
iconSize: 24 * scale),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onHomePressed,
|
onPressed: onHomePressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.home)),
|
icon: const Icon(Icons.home),
|
||||||
|
iconSize: 24 * scale),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onRecentPressed,
|
onPressed: onRecentPressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.more_horiz)),
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
iconSize: 24 * scale),
|
||||||
const VerticalDivider(
|
const VerticalDivider(
|
||||||
width: 0,
|
width: 0,
|
||||||
thickness: 2,
|
thickness: 2,
|
||||||
@@ -226,7 +237,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onHidePressed,
|
onPressed: onHidePressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.keyboard_arrow_down)),
|
icon: const Icon(Icons.keyboard_arrow_down),
|
||||||
|
iconSize: 24 * scale),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
@@ -234,12 +246,98 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DraggableKeyPosition {
|
||||||
|
final String key;
|
||||||
|
Offset _pos;
|
||||||
|
late Debouncer<int> _debouncerStore;
|
||||||
|
DraggableKeyPosition(this.key)
|
||||||
|
: _pos = DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
|
||||||
|
get pos => _pos;
|
||||||
|
|
||||||
|
_loadPosition(String k) {
|
||||||
|
final value = bind.getLocalFlutterOption(k: k);
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
final parts = value.split(',');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
return Offset(double.parse(parts[0]), double.parse(parts[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
_pos = _loadPosition(key);
|
||||||
|
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
|
||||||
|
onChanged: (v) => _store(), initialValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(Offset pos) {
|
||||||
|
_pos = pos;
|
||||||
|
_triggerStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust position to keep it in the screen
|
||||||
|
// Only used for desktop and web desktop
|
||||||
|
tryAdjust(double w, double h, double scale) {
|
||||||
|
final size = MediaQuery.of(Get.context!).size;
|
||||||
|
w = w * scale;
|
||||||
|
h = h * scale;
|
||||||
|
double x = _pos.dx;
|
||||||
|
double y = _pos.dy;
|
||||||
|
if (x + w > size.width) {
|
||||||
|
x = size.width - w;
|
||||||
|
}
|
||||||
|
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
|
||||||
|
if (y + h > (size.height - tabBarHeight)) {
|
||||||
|
y = size.height - tabBarHeight - h;
|
||||||
|
}
|
||||||
|
if (x < 0) {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
if (x != _pos.dx || y != _pos.dy) {
|
||||||
|
update(Offset(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInvalid() {
|
||||||
|
return _pos == DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
|
||||||
|
_store() {
|
||||||
|
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraggablePositions {
|
||||||
|
static const kChatWindow = 'draggablePositionChat';
|
||||||
|
static const kMobileActions = 'draggablePositionMobile';
|
||||||
|
static const kIOSDraggable = 'draggablePositionIOS';
|
||||||
|
|
||||||
|
static const kInvalidDraggablePosition = Offset(-999999, -999999);
|
||||||
|
final chatWindow = DraggableKeyPosition(kChatWindow);
|
||||||
|
final mobileActions = DraggableKeyPosition(kMobileActions);
|
||||||
|
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
|
||||||
|
|
||||||
|
load() {
|
||||||
|
chatWindow.load();
|
||||||
|
mobileActions.load();
|
||||||
|
iOSDraggable.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DraggablePositions draggablePositions = DraggablePositions();
|
||||||
|
|
||||||
class Draggable extends StatefulWidget {
|
class Draggable extends StatefulWidget {
|
||||||
const Draggable(
|
Draggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.checkKeyboard = false,
|
this.checkKeyboard = false,
|
||||||
this.checkScreenSize = false,
|
this.checkScreenSize = false,
|
||||||
this.position = Offset.zero,
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
this.chatModel,
|
this.chatModel,
|
||||||
@@ -248,7 +346,7 @@ class Draggable extends StatefulWidget {
|
|||||||
|
|
||||||
final bool checkKeyboard;
|
final bool checkKeyboard;
|
||||||
final bool checkScreenSize;
|
final bool checkScreenSize;
|
||||||
final Offset position;
|
final DraggableKeyPosition position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
@@ -259,7 +357,6 @@ class Draggable extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableState extends State<Draggable> {
|
class _DraggableState extends State<Draggable> {
|
||||||
late Offset _position;
|
|
||||||
late ChatModel? _chatModel;
|
late ChatModel? _chatModel;
|
||||||
bool _keyboardVisible = false;
|
bool _keyboardVisible = false;
|
||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
@@ -268,35 +365,36 @@ class _DraggableState extends State<Draggable> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_position = widget.position;
|
|
||||||
_chatModel = widget.chatModel;
|
_chatModel = widget.chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get position => widget.position.pos;
|
||||||
|
|
||||||
void onPanUpdate(DragUpdateDetails d) {
|
void onPanUpdate(DragUpdateDetails d) {
|
||||||
final offset = d.delta;
|
final offset = d.delta;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
double x = 0;
|
double x = 0;
|
||||||
double y = 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;
|
x = size.width - widget.width;
|
||||||
} else if (_position.dx + offset.dx < 0) {
|
} else if (position.dx + offset.dx < 0) {
|
||||||
x = 0;
|
x = 0;
|
||||||
} else {
|
} 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;
|
y = size.height - widget.height;
|
||||||
} else if (_position.dy + offset.dy < 0) {
|
} else if (position.dy + offset.dy < 0) {
|
||||||
y = 0;
|
y = 0;
|
||||||
} else {
|
} else {
|
||||||
y = _position.dy + offset.dy;
|
y = position.dy + offset.dy;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(x, y);
|
widget.position.update(Offset(x, y));
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(_position);
|
_chatModel?.setChatWindowPosition(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkScreenSize() {}
|
checkScreenSize() {}
|
||||||
@@ -307,13 +405,13 @@ class _DraggableState extends State<Draggable> {
|
|||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = _position.dy;
|
_saveHeight = position.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, _saveHeight);
|
widget.position.update(Offset(position.dx, _saveHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,10 +419,10 @@ class _DraggableState extends State<Draggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + widget.height;
|
final sumHeight = bottomHeight + widget.height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + _position.dy > contextHeight) {
|
if (sumHeight + position.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, y);
|
widget.position.update(Offset(position.dx, y));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,8 +441,8 @@ class _DraggableState extends State<Draggable> {
|
|||||||
}
|
}
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
top: _position.dy,
|
top: position.dy,
|
||||||
left: _position.dx,
|
left: position.dx,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
child: widget.builder(context, onPanUpdate))
|
child: widget.builder(context, onPanUpdate))
|
||||||
@@ -355,14 +453,14 @@ class _DraggableState extends State<Draggable> {
|
|||||||
class IOSDraggable extends StatefulWidget {
|
class IOSDraggable extends StatefulWidget {
|
||||||
const IOSDraggable(
|
const IOSDraggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.position = Offset.zero,
|
|
||||||
this.chatModel,
|
this.chatModel,
|
||||||
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
required this.builder})
|
required this.builder})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final Offset position;
|
final DraggableKeyPosition position;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
@@ -373,7 +471,6 @@ class IOSDraggable extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class IOSDraggableState extends State<IOSDraggable> {
|
class IOSDraggableState extends State<IOSDraggable> {
|
||||||
late Offset _position;
|
|
||||||
late ChatModel? _chatModel;
|
late ChatModel? _chatModel;
|
||||||
late double _width;
|
late double _width;
|
||||||
late double _height;
|
late double _height;
|
||||||
@@ -384,25 +481,26 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_position = widget.position;
|
|
||||||
_chatModel = widget.chatModel;
|
_chatModel = widget.chatModel;
|
||||||
_width = widget.width;
|
_width = widget.width;
|
||||||
_height = widget.height;
|
_height = widget.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DraggableKeyPosition get position => widget.position;
|
||||||
|
|
||||||
checkKeyboard() {
|
checkKeyboard() {
|
||||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
final currentVisible = bottomHeight != 0;
|
final currentVisible = bottomHeight != 0;
|
||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = _position.dy;
|
_saveHeight = position.pos.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, _saveHeight);
|
position.update(Offset(position.pos.dx, _saveHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,10 +508,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + _height;
|
final sumHeight = bottomHeight + _height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + _position.dy > contextHeight) {
|
if (sumHeight + position.pos.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, y);
|
position.update(Offset(position.pos.dx, y));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
left: _position.dx,
|
left: position.pos.dx,
|
||||||
top: _position.dy,
|
top: position.pos.dy,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position += details.delta;
|
position.update(position.pos + details.delta);
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(_position);
|
_chatModel?.setChatWindowPosition(position.pos);
|
||||||
},
|
},
|
||||||
child: Material(
|
child: Material(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -491,8 +589,10 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
||||||
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
||||||
|
// let delay be 0 if fps is 0
|
||||||
_row(
|
_row(
|
||||||
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
|
"Delay",
|
||||||
|
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
|
||||||
rightColor: Colors.green),
|
rightColor: Colors.green),
|
||||||
_row("Target Bitrate",
|
_row("Target Bitrate",
|
||||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ enum PeerUiType { grid, tile, list }
|
|||||||
|
|
||||||
final peerCardUiType = PeerUiType.grid.obs;
|
final peerCardUiType = PeerUiType.grid.obs;
|
||||||
|
|
||||||
|
bool? hideUsernameOnCard;
|
||||||
|
|
||||||
class _PeerCard extends StatefulWidget {
|
class _PeerCard extends StatefulWidget {
|
||||||
final Peer peer;
|
final Peer peer;
|
||||||
final PeerTabIndex tab;
|
final PeerTabIndex tab;
|
||||||
@@ -130,8 +132,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
Widget _buildPeerTile(
|
Widget _buildPeerTile(
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||||
final name =
|
hideUsernameOnCard ??=
|
||||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
|
final name = hideUsernameOnCard == true
|
||||||
|
? peer.hostname
|
||||||
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final greyStyle = TextStyle(
|
final greyStyle = TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||||
@@ -239,8 +244,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
Widget _buildPeerCard(
|
Widget _buildPeerCard(
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||||
final name =
|
hideUsernameOnCard ??=
|
||||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
|
final name = hideUsernameOnCard == true
|
||||||
|
? peer.hostname
|
||||||
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final child = Card(
|
final child = Card(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -887,7 +895,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
menuItems.add(_createShortCutAction(peer.id));
|
menuItems.add(_createShortCutAction(peer.id));
|
||||||
}
|
}
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||||
@@ -943,7 +951,7 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
menuItems.add(_createShortCutAction(peer.id));
|
menuItems.add(_createShortCutAction(peer.id));
|
||||||
}
|
}
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||||
@@ -1048,7 +1056,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
}
|
}
|
||||||
if (gFFI.abModel.current.canWrite()) {
|
if (gFFI.abModel.current.canWrite()) {
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
];
|
];
|
||||||
RelativeRect? mobileTabContextMenuPos;
|
RelativeRect? mobileTabContextMenuPos;
|
||||||
|
|
||||||
|
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||||
if (uiType != '') {
|
if (uiType != '') {
|
||||||
peerCardUiType.value = int.parse(uiType) == 0
|
peerCardUiType.value = int.parse(uiType) == 0
|
||||||
? PeerUiType.grid
|
? PeerUiType.grid
|
||||||
@@ -85,7 +87,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
: PeerUiType.list;
|
: PeerUiType.list;
|
||||||
}
|
}
|
||||||
hideAbTagsPanel.value =
|
hideAbTagsPanel.value =
|
||||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +175,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
child: Icon(model.tabIcon(t), color: color)
|
child: Icon(model.tabIcon(t), color: color)
|
||||||
.paddingSymmetric(horizontal: 4),
|
.paddingSymmetric(horizontal: 4),
|
||||||
).paddingSymmetric(horizontal: 4),
|
).paddingSymmetric(horizontal: 4),
|
||||||
onTap: () async {
|
onTap: isOptionFixed(kOptionPeerTabIndex)
|
||||||
await handleTabSelection(t);
|
? null
|
||||||
await bind.setLocalFlutterOption(
|
: () async {
|
||||||
k: PeerTabModel.kPeerTabIndex, v: t.toString());
|
await handleTabSelection(t);
|
||||||
},
|
await bind.setLocalFlutterOption(
|
||||||
|
k: kOptionPeerTabIndex, v: t.toString());
|
||||||
|
},
|
||||||
onHover: (value) => hover.value = value,
|
onHover: (value) => hover.value = value,
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
@@ -265,17 +269,22 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
if (!model.isEnabled[i]) continue;
|
if (!model.isEnabled[i]) continue;
|
||||||
items.add(PopupMenuItem(
|
items.add(PopupMenuItem(
|
||||||
height: kMinInteractiveDimension * 0.8,
|
height: kMinInteractiveDimension * 0.8,
|
||||||
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
onTap: isOptVisiableFixed
|
||||||
|
? null
|
||||||
|
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||||
|
enabled: !isOptVisiableFixed,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: model.isVisibleEnabled[i],
|
value: model.isVisibleEnabled[i],
|
||||||
onChanged: (_) {
|
onChanged: isOptVisiableFixed
|
||||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
? null
|
||||||
if (Navigator.canPop(context)) {
|
: (_) {
|
||||||
Navigator.pop(context);
|
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||||
}
|
if (Navigator.canPop(context)) {
|
||||||
}),
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}),
|
||||||
Expanded(child: Text(model.tabTooltip(i))),
|
Expanded(child: Text(model.tabTooltip(i))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -332,8 +341,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
currentValue: model.isVisibleEnabled[tabIndex],
|
currentValue: model.isVisibleEnabled[tabIndex],
|
||||||
setter: (show) async {
|
setter: (show) async {
|
||||||
model.setTabVisible(tabIndex, show);
|
model.setTabVisible(tabIndex, show);
|
||||||
cancelFunc();
|
// Do not hide the current menu (checkbox)
|
||||||
}));
|
// cancelFunc();
|
||||||
|
},
|
||||||
|
enabled: (!isOptVisiableFixed).obs));
|
||||||
}
|
}
|
||||||
return mod_menu.PopupMenu(
|
return mod_menu.PopupMenu(
|
||||||
items: menu
|
items: menu
|
||||||
@@ -388,9 +399,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
final peers = model.selectedPeers;
|
final peers = model.selectedPeers;
|
||||||
switch (model.currentTab) {
|
switch (model.currentTab) {
|
||||||
case 0:
|
case 0:
|
||||||
peers.map((p) async {
|
for (var p in peers) {
|
||||||
await bind.mainRemovePeer(id: p.id);
|
await bind.mainRemovePeer(id: p.id);
|
||||||
}).toList();
|
}
|
||||||
await bind.mainLoadRecentPeers();
|
await bind.mainLoadRecentPeers();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
@@ -402,9 +413,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
await bind.mainLoadFavPeers();
|
await bind.mainLoadFavPeers();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
peers.map((p) async {
|
for (var p in peers) {
|
||||||
await bind.mainRemoveDiscovered(id: p.id);
|
await bind.mainRemoveDiscovered(id: p.id);
|
||||||
}).toList();
|
}
|
||||||
await bind.mainLoadLanPeers();
|
await bind.mainLoadLanPeers();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
@@ -529,7 +540,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Widget _toggleTags() {
|
Widget _toggleTags() {
|
||||||
return _hoverAction(
|
return _hoverAction(
|
||||||
context: context,
|
context: context,
|
||||||
toolTip: translate('Toggle tags'),
|
toolTip: translate('Toggle Tags'),
|
||||||
hoverableWhenfalse: hideAbTagsPanel,
|
hoverableWhenfalse: hideAbTagsPanel,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.tag_rounded,
|
Icons.tag_rounded,
|
||||||
@@ -537,7 +548,8 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
|
key: kOptionHideAbTagsPanel,
|
||||||
|
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
|
||||||
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -790,25 +802,38 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 36,
|
height: 36,
|
||||||
child: getRadio<PeerUiType>(
|
child: getRadio<PeerUiType>(
|
||||||
Text(
|
Tooltip(
|
||||||
translate(types.indexOf(e) == 0
|
message: translate(types.indexOf(e) == 0
|
||||||
? 'Big tiles'
|
? 'Big tiles'
|
||||||
: types.indexOf(e) == 1
|
: types.indexOf(e) == 1
|
||||||
? 'Small tiles'
|
? 'Small tiles'
|
||||||
: 'List'),
|
: 'List'),
|
||||||
style: style),
|
child: Icon(
|
||||||
|
e == PeerUiType.grid
|
||||||
|
? Icons.grid_view_rounded
|
||||||
|
: e == PeerUiType.list
|
||||||
|
? Icons.view_list_rounded
|
||||||
|
: Icons.view_agenda_rounded,
|
||||||
|
size: 18,
|
||||||
|
)),
|
||||||
e,
|
e,
|
||||||
peerCardUiType.value,
|
peerCardUiType.value,
|
||||||
dense: true, (PeerUiType? v) async {
|
dense: true,
|
||||||
if (v != null) {
|
isOptionFixed(kOptionPeerCardUiType)
|
||||||
peerCardUiType.value = v;
|
? null
|
||||||
setState(() {});
|
: (PeerUiType? v) async {
|
||||||
await bind.setLocalFlutterOption(
|
if (v != null) {
|
||||||
k: "peer-card-ui-type",
|
peerCardUiType.value = v;
|
||||||
v: peerCardUiType.value.index.toString(),
|
setState(() {});
|
||||||
);
|
await bind.setLocalFlutterOption(
|
||||||
}
|
k: kOptionPeerCardUiType,
|
||||||
}),
|
v: peerCardUiType.value.index.toString(),
|
||||||
|
);
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
@@ -820,7 +845,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
peerCardUiType.value == PeerUiType.grid
|
peerCardUiType.value == PeerUiType.grid
|
||||||
? Icons.grid_view_rounded
|
? Icons.grid_view_rounded
|
||||||
: peerCardUiType.value == PeerUiType.tile
|
: peerCardUiType.value == PeerUiType.list
|
||||||
? Icons.view_list_rounded
|
? Icons.view_list_rounded
|
||||||
: Icons.view_agenda_rounded,
|
: Icons.view_agenda_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
@@ -852,7 +877,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
|||||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||||
peerSort.value = PeerSortType.remoteId;
|
peerSort.value = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -882,7 +907,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
|||||||
if (v != null) {
|
if (v != null) {
|
||||||
peerSort.value = v;
|
peerSort.value = v;
|
||||||
await bind.setLocalFlutterOption(
|
await bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import 'dart:collection';
|
|||||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../models/peer_model.dart';
|
import '../../models/peer_model.dart';
|
||||||
@@ -45,7 +45,7 @@ class LoadEvent {
|
|||||||
final peerSearchText = "".obs;
|
final peerSearchText = "".obs;
|
||||||
|
|
||||||
/// for peer sort, global obs value
|
/// for peer sort, global obs value
|
||||||
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
|
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||||
|
|
||||||
// list for listener
|
// list for listener
|
||||||
final obslist = [peerSearchText, peerSort].obs;
|
final obslist = [peerSearchText, peerSort].obs;
|
||||||
@@ -82,7 +82,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
final _curPeers = <String>{};
|
final _curPeers = <String>{};
|
||||||
var _lastChangeTime = DateTime.now();
|
var _lastChangeTime = DateTime.now();
|
||||||
var _lastQueryPeers = <String>{};
|
var _lastQueryPeers = <String>{};
|
||||||
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
|
var _lastQueryTime = DateTime.now();
|
||||||
var _queryCount = 0;
|
var _queryCount = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
|
|
||||||
@@ -253,10 +253,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
final _queryInterval = const Duration(seconds: 20);
|
var _queryInterval = const Duration(seconds: 20);
|
||||||
|
|
||||||
void _startCheckOnlines() {
|
void _startCheckOnlines() {
|
||||||
() async {
|
() async {
|
||||||
|
final p = await bind.mainIsUsingPublicServer();
|
||||||
|
if (!p) {
|
||||||
|
_queryInterval = const Duration(seconds: 6);
|
||||||
|
}
|
||||||
while (!_exit) {
|
while (!_exit) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
||||||
@@ -264,7 +268,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryOnlines(false);
|
_queryOnlines(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_queryCount < _maxQueryCount) {
|
if (_queryCount < _maxQueryCount || !p) {
|
||||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
@@ -302,7 +306,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
if (!PeerSortType.values.contains(sortedBy)) {
|
if (!PeerSortType.values.contains(sortedBy)) {
|
||||||
sortedBy = PeerSortType.remoteId;
|
sortedBy = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: sortedBy,
|
v: sortedBy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
|
|||||||
class _RawTouchGestureDetectorRegionState
|
class _RawTouchGestureDetectorRegionState
|
||||||
extends State<RawTouchGestureDetectorRegion> {
|
extends State<RawTouchGestureDetectorRegion> {
|
||||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||||
|
// Timestamp of the last long press event.
|
||||||
|
int _cacheLongPressPositionTs = 0;
|
||||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||||
double _scale = 1;
|
double _scale = 1;
|
||||||
|
|
||||||
@@ -95,8 +97,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
// Desktop or mobile "Touch mode"
|
// Desktop or mobile "Touch mode"
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
inputModel.tapDown(MouseButtons.left);
|
inputModel.tapDown(MouseButtons.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +108,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
inputModel.tapUp(MouseButtons.left);
|
inputModel.tapUp(MouseButtons.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +138,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
inputModel.tap(MouseButtons.left);
|
inputModel.tap(MouseButtons.left);
|
||||||
inputModel.tap(MouseButtons.left);
|
inputModel.tap(MouseButtons.left);
|
||||||
}
|
}
|
||||||
@@ -146,6 +153,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
_cacheLongPressPosition = d.localPosition;
|
_cacheLongPressPosition = d.localPosition;
|
||||||
|
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,9 +230,22 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
|
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||||
|
// If the time interval between the start event and the first pan event is less than 500ms,
|
||||||
|
// we consider to use the long press position as the start position.
|
||||||
|
//
|
||||||
|
// TODO: We should find a better way to send the first pan event as soon as possible.
|
||||||
|
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
|
||||||
|
500) {
|
||||||
|
ffi.cursorModel
|
||||||
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||||
|
}
|
||||||
inputModel.sendMouse('down', MouseButtons.left);
|
inputModel.sendMouse('down', MouseButtons.left);
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +265,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +305,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// mobile
|
// mobile
|
||||||
ffi.canvasModel.updateScale(d.scale / _scale);
|
ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
|
||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
||||||
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/consts.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:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
customImageQualityWidget(
|
customImageQualityWidget(
|
||||||
{required double initQuality,
|
{required double initQuality,
|
||||||
required double initFps,
|
required double initFps,
|
||||||
required Function(double) setQuality,
|
required Function(double)? setQuality,
|
||||||
required Function(double) setFps,
|
required Function(double)? setFps,
|
||||||
required bool showFps,
|
required bool showFps,
|
||||||
required bool showMoreQuality}) {
|
required bool showMoreQuality}) {
|
||||||
if (initQuality < kMinQuality ||
|
if (initQuality < kMinQuality ||
|
||||||
@@ -27,16 +26,12 @@ customImageQualityWidget(
|
|||||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
||||||
final debouncerQuality = Debouncer<double>(
|
final debouncerQuality = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: setQuality,
|
||||||
setQuality(v);
|
|
||||||
},
|
|
||||||
initialValue: qualityValue.value,
|
initialValue: qualityValue.value,
|
||||||
);
|
);
|
||||||
final debouncerFps = Debouncer<double>(
|
final debouncerFps = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: setFps,
|
||||||
setFps(v);
|
|
||||||
},
|
|
||||||
initialValue: fpsValue.value,
|
initialValue: fpsValue.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,10 +57,12 @@ customImageQualityWidget(
|
|||||||
divisions: moreQualityChecked.value
|
divisions: moreQualityChecked.value
|
||||||
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
||||||
: ((kMaxQuality - kMinQuality) / 5).round(),
|
: ((kMaxQuality - kMinQuality) / 5).round(),
|
||||||
onChanged: (double value) async {
|
onChanged: setQuality == null
|
||||||
qualityValue.value = value;
|
? null
|
||||||
debouncerQuality.value = value;
|
: (double value) async {
|
||||||
},
|
qualityValue.value = value;
|
||||||
|
debouncerQuality.value = value;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -124,10 +121,12 @@ customImageQualityWidget(
|
|||||||
min: kMinFps,
|
min: kMinFps,
|
||||||
max: kMaxFps,
|
max: kMaxFps,
|
||||||
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
||||||
onChanged: (double value) async {
|
onChanged: setFps == null
|
||||||
fpsValue.value = value;
|
? null
|
||||||
debouncerFps.value = value;
|
: (double value) async {
|
||||||
},
|
fpsValue.value = value;
|
||||||
|
debouncerFps.value = value;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -152,21 +151,29 @@ customImageQualitySetting() {
|
|||||||
final qualityKey = 'custom_image_quality';
|
final qualityKey = 'custom_image_quality';
|
||||||
final fpsKey = 'custom-fps';
|
final fpsKey = 'custom-fps';
|
||||||
|
|
||||||
var initQuality =
|
final initQuality =
|
||||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||||
kDefaultQuality);
|
kDefaultQuality);
|
||||||
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
final isQuanlityFixed = isOptionFixed(qualityKey);
|
||||||
kDefaultFps);
|
final initFps =
|
||||||
|
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||||
|
kDefaultFps);
|
||||||
|
final isFpsFixed = isOptionFixed(fpsKey);
|
||||||
|
|
||||||
return customImageQualityWidget(
|
return customImageQualityWidget(
|
||||||
initQuality: initQuality,
|
initQuality: initQuality,
|
||||||
initFps: initFps,
|
initFps: initFps,
|
||||||
setQuality: (v) {
|
setQuality: isQuanlityFixed
|
||||||
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
|
? null
|
||||||
},
|
: (v) {
|
||||||
setFps: (v) {
|
bind.mainSetUserDefaultOption(
|
||||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
key: qualityKey, value: v.toString());
|
||||||
},
|
},
|
||||||
|
setFps: isFpsFixed
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
|
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||||
|
},
|
||||||
showFps: true,
|
showFps: true,
|
||||||
showMoreQuality: true);
|
showMoreQuality: true);
|
||||||
}
|
}
|
||||||
@@ -208,29 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
|
|||||||
|
|
||||||
List<(String, String)> otherDefaultSettings() {
|
List<(String, String)> otherDefaultSettings() {
|
||||||
List<(String, String)> v = [
|
List<(String, String)> v = [
|
||||||
('View Mode', 'view_only'),
|
('View Mode', kOptionViewOnly),
|
||||||
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
if ((isDesktop || isWebDesktop))
|
||||||
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
|
('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||||
('Show remote cursor', 'show_remote_cursor'),
|
if ((isDesktop || isWebDesktop))
|
||||||
('Follow remote cursor', 'follow_remote_cursor'),
|
('Collapse toolbar', kOptionCollapseToolbar),
|
||||||
('Follow remote window focus', 'follow_remote_window'),
|
('Show remote cursor', kOptionShowRemoteCursor),
|
||||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
|
('Follow remote cursor', kOptionFollowRemoteCursor),
|
||||||
('Show quality monitor', 'show_quality_monitor'),
|
('Follow remote window focus', kOptionFollowRemoteWindow),
|
||||||
('Mute', 'disable_audio'),
|
if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
|
||||||
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
|
('Show quality monitor', kOptionShowQualityMonitor),
|
||||||
('Disable clipboard', 'disable_clipboard'),
|
('Mute', kOptionDisableAudio),
|
||||||
('Lock after session end', 'lock_after_session_end'),
|
if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
|
||||||
('Privacy mode', 'privacy_mode'),
|
('Disable clipboard', kOptionDisableClipboard),
|
||||||
if (isMobile) ('Touch mode', 'touch-mode'),
|
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||||
('True color (4:4:4)', 'i444'),
|
('Privacy mode', kOptionPrivacyMode),
|
||||||
|
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||||
|
('True color (4:4:4)', kOptionI444),
|
||||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||||
if (isDesktop && useTextureRender)
|
if (isDesktop)
|
||||||
(
|
(
|
||||||
'Show displays as individual windows',
|
'Show displays as individual windows',
|
||||||
kKeyShowDisplaysAsIndividualWindows
|
kKeyShowDisplaysAsIndividualWindows
|
||||||
),
|
),
|
||||||
if (isDesktop && useTextureRender)
|
if (isDesktop)
|
||||||
(
|
(
|
||||||
'Use all my displays for the remote session',
|
'Use all my displays for the remote session',
|
||||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import 'package:flutter_hbb/common.dart';
|
|||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
bool isEditOsPassword = false;
|
bool isEditOsPassword = false;
|
||||||
@@ -23,6 +23,20 @@ class TTextMenu {
|
|||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.divider = false});
|
this.divider = false});
|
||||||
|
|
||||||
|
Widget getChild() {
|
||||||
|
if (trailingIcon != null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
trailingIcon!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TRadioMenu<T> {
|
class TRadioMenu<T> {
|
||||||
@@ -116,7 +130,10 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// paste
|
// paste
|
||||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
if (isMobile &&
|
||||||
|
pi.platform != kPeerPlatformAndroid &&
|
||||||
|
perms['keyboard'] != false &&
|
||||||
|
perms['clipboard'] != false) {
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Paste')),
|
child: Text(translate('Paste')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -327,7 +344,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
final alternativeCodecs =
|
final alternativeCodecs =
|
||||||
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
||||||
final groupValue = await bind.sessionGetOption(
|
final groupValue = await bind.sessionGetOption(
|
||||||
sessionId: sessionId, arg: 'codec-preference') ??
|
sessionId: sessionId, arg: kOptionCodecPreference) ??
|
||||||
'';
|
'';
|
||||||
final List<bool> codecs = [];
|
final List<bool> codecs = [];
|
||||||
try {
|
try {
|
||||||
@@ -349,7 +366,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
onChanged(String? value) async {
|
onChanged(String? value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionPeerOption(
|
await bind.sessionPeerOption(
|
||||||
sessionId: sessionId, name: 'codec-preference', value: value);
|
sessionId: sessionId, name: kOptionCodecPreference, value: value);
|
||||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +379,8 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var autoLabel = translate('Auto');
|
var autoLabel = translate('Auto');
|
||||||
if (groupValue == 'auto') {
|
if (groupValue == 'auto' &&
|
||||||
|
ffi.qualityMonitorModel.data.codecFormat != null) {
|
||||||
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@@ -380,7 +398,6 @@ Future<List<TToggleMenu>> toolbarCursor(
|
|||||||
List<TToggleMenu> v = [];
|
List<TToggleMenu> v = [];
|
||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final perms = ffiModel.permissions;
|
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
|
||||||
// show remote cursor
|
// show remote cursor
|
||||||
@@ -534,15 +551,15 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
perms['file'] != false &&
|
perms['file'] != false &&
|
||||||
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'enable-file-transfer';
|
final value = bind.sessionGetToggleOptionSync(
|
||||||
final value =
|
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
||||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
||||||
v.add(TToggleMenu(
|
v.add(TToggleMenu(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(translate('Enable file copy and paste'))));
|
child: Text(translate('Enable file copy and paste'))));
|
||||||
@@ -565,7 +582,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Disable clipboard'))));
|
child: Text(translate('Disable clipboard'))));
|
||||||
}
|
}
|
||||||
// lock after session end
|
// lock after session end
|
||||||
if (ffiModel.keyboard) {
|
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'lock-after-session-end';
|
final option = 'lock-after-session-end';
|
||||||
final value =
|
final value =
|
||||||
@@ -581,8 +598,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Lock after session end'))));
|
child: Text(translate('Lock after session end'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useTextureRender &&
|
if (pi.isSupportMultiDisplay &&
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
PrivacyModeState.find(id).isEmpty &&
|
PrivacyModeState.find(id).isEmpty &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||||
@@ -594,13 +610,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionSetDisplaysAsIndividualWindows(
|
bind.sessionSetDisplaysAsIndividualWindows(
|
||||||
sessionId: sessionId, value: value ? 'Y' : '');
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
child: Text(translate('Show displays as individual windows'))));
|
child: Text(translate('Show displays as individual windows'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||||
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
|
if (pi.isSupportMultiDisplay && isMultiScreens) {
|
||||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
@@ -609,7 +625,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: sessionId, value: value ? 'Y' : '');
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
child: Text(translate('Use all my displays for the remote session'))));
|
child: Text(translate('Use all my displays for the remote session'))));
|
||||||
}
|
}
|
||||||
@@ -635,6 +651,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
v.addAll(toolbarKeyboardToggles(ffi));
|
v.addAll(toolbarKeyboardToggles(ffi));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// view mode (mobile only, desktop is in keyboard menu)
|
||||||
|
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: ffiModel.viewOnly,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
|
ffiModel.setViewOnly(id, value);
|
||||||
|
},
|
||||||
|
child: Text(translate('View Mode'))));
|
||||||
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,3 +803,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool showVirtualDisplayMenu(FFI ffi) {
|
||||||
|
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ffi.ffiModel.pi.isInstalled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> getVirtualDisplayMenuChildren(
|
||||||
|
FFI ffi, String id, VoidCallback? clickCallBack) {
|
||||||
|
if (!showVirtualDisplayMenu(ffi)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final pi = ffi.ffiModel.pi;
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
|
if (pi.isRustDeskIdd) {
|
||||||
|
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||||
|
final children = <Widget>[];
|
||||||
|
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||||
|
children.add(Obx(() => CkbMenuButton(
|
||||||
|
value: virtualDisplays.contains(i + 1),
|
||||||
|
onChanged: privacyModeState.isNotEmpty
|
||||||
|
? null
|
||||||
|
: (bool? value) async {
|
||||||
|
if (value != null) {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: i + 1, on: value);
|
||||||
|
clickCallBack?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||||
|
ffi: ffi,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
children.add(Divider());
|
||||||
|
children.add(Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)));
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (pi.isAmyuniIdd) {
|
||||||
|
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||||
|
final children = <Widget>[
|
||||||
|
Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: 0, on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
child: Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
Text(count.toString()),
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: 0, on: true);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Divider(),
|
||||||
|
Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,16 +63,97 @@ const String kWindowEventActiveDisplaySession = "active_display_session";
|
|||||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||||
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
||||||
|
const String kWindowEventSetFullscreen = "set_fullscreen";
|
||||||
|
|
||||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
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 kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||||
|
const String kOptionTextureRender = "use-texture-render";
|
||||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||||
const String kOptionForceAlwaysRelay = "force-always-relay";
|
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 kOptionStopService = "stop-service";
|
||||||
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
|
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||||
|
|
||||||
|
// buildin opitons
|
||||||
|
const String kOptionHideServerSetting = "hide-server-settings";
|
||||||
|
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||||
|
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||||
|
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||||
|
const String kOptionRemovePresetPasswordWarning =
|
||||||
|
"remove-preset-password-warning";
|
||||||
|
const kHideUsernameOnCard = "hide-username-on-card";
|
||||||
|
const String kOptionHideHelpCards = "hide-help-cards";
|
||||||
|
|
||||||
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
|
|
||||||
|
const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||||
|
|
||||||
|
const String kOptionKeepScreenOn = "keep-screen-on";
|
||||||
|
|
||||||
|
const String kOptionShowMobileAction = "showMobileActions";
|
||||||
|
|
||||||
const String kUrlActionClose = "close";
|
const String kUrlActionClose = "close";
|
||||||
|
|
||||||
@@ -85,6 +166,8 @@ const int kWindowMainId = 0;
|
|||||||
const String kPointerEventKindTouch = "touch";
|
const String kPointerEventKindTouch = "touch";
|
||||||
const String kPointerEventKindMouse = "mouse";
|
const String kPointerEventKindMouse = "mouse";
|
||||||
|
|
||||||
|
const String kKeyFlutterKey = "flutter_key";
|
||||||
|
|
||||||
const String kKeyShowDisplaysAsIndividualWindows =
|
const String kKeyShowDisplaysAsIndividualWindows =
|
||||||
'displays_as_individual_windows';
|
'displays_as_individual_windows';
|
||||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||||
@@ -92,10 +175,13 @@ const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
|||||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||||
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
||||||
|
|
||||||
|
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
|
||||||
|
|
||||||
// the executable name of the portable version
|
// the executable name of the portable version
|
||||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||||
|
|
||||||
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
||||||
|
const Color kColorCanvas = Colors.black;
|
||||||
|
|
||||||
const int kMobileDefaultDisplayWidth = 720;
|
const int kMobileDefaultDisplayWidth = 720;
|
||||||
const int kMobileDefaultDisplayHeight = 1280;
|
const int kMobileDefaultDisplayHeight = 1280;
|
||||||
@@ -156,7 +242,7 @@ const kFullScreenEdgeSize = 0.0;
|
|||||||
const kMaximizeEdgeSize = 0.0;
|
const kMaximizeEdgeSize = 0.0;
|
||||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||||
final kWindowBorderWidth = isLinux ? 1.0 : 0.0;
|
final kWindowBorderWidth = 1.0;
|
||||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||||
const kFrameBorderRadius = 12.0;
|
const kFrameBorderRadius = 12.0;
|
||||||
const kFrameClipRRectBorderRadius = 12.0;
|
const kFrameClipRRectBorderRadius = 12.0;
|
||||||
@@ -208,12 +294,6 @@ const kRemoteImageQualityLow = 'low';
|
|||||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||||
const kRemoteImageQualityCustom = 'custom';
|
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;
|
const kIgnoreDpi = true;
|
||||||
|
|
||||||
// ================================ mobile ================================
|
// ================================ mobile ================================
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
?.merge(TextStyle(height: 1)),
|
?.merge(TextStyle(height: 1)),
|
||||||
).marginOnly(right: 4),
|
).marginOnly(right: 4),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
waitDuration: Duration(milliseconds: 0),
|
waitDuration: Duration(milliseconds: 300),
|
||||||
message: translate("id_input_tip"),
|
message: translate("id_input_tip"),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.help_outline_outlined,
|
Icons.help_outline_outlined,
|
||||||
|
|||||||
@@ -443,14 +443,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (isMacOS) {
|
} else if (isMacOS) {
|
||||||
if (!(bind.isOutgoingOnly() ||
|
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||||
bind.mainIsCanScreenRecording(prompt: false))) {
|
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
|
||||||
return buildInstallCard("Permissions", "config_screen", "Configure",
|
return buildInstallCard("Permissions", "config_screen", "Configure",
|
||||||
() async {
|
() async {
|
||||||
bind.mainIsCanScreenRecording(prompt: true);
|
bind.mainIsCanScreenRecording(prompt: true);
|
||||||
watchIsCanScreenRecording = true;
|
watchIsCanScreenRecording = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||||
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
|
} else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
|
||||||
return buildInstallCard("Permissions", "config_acc", "Configure",
|
return buildInstallCard("Permissions", "config_acc", "Configure",
|
||||||
() async {
|
() async {
|
||||||
bind.mainIsProcessTrusted(prompt: true);
|
bind.mainIsProcessTrusted(prompt: true);
|
||||||
@@ -462,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
bind.mainIsCanInputMonitoring(prompt: true);
|
bind.mainIsCanInputMonitoring(prompt: true);
|
||||||
watchIsInputMonitoring = true;
|
watchIsInputMonitoring = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||||
} else if (!svcStopped.value &&
|
} else if (!isOutgoingOnly &&
|
||||||
|
!svcStopped.value &&
|
||||||
bind.mainIsInstalled() &&
|
bind.mainIsInstalled() &&
|
||||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||||
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
||||||
@@ -545,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
String? link,
|
String? link,
|
||||||
bool? closeButton,
|
bool? closeButton,
|
||||||
String? closeOption}) {
|
String? closeOption}) {
|
||||||
|
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
|
||||||
|
content != 'install_daemon_tip') {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
void closeCard() async {
|
void closeCard() async {
|
||||||
if (closeOption != null) {
|
if (closeOption != null) {
|
||||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||||
@@ -658,10 +663,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
Timer(const Duration(seconds: 1), () async {
|
if (!bind.isCustomClient()) {
|
||||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
Timer(const Duration(seconds: 1), () async {
|
||||||
if (updateUrl.isNotEmpty) setState(() {});
|
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
});
|
if (updateUrl.isNotEmpty) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||||
await gFFI.serverModel.fetchID();
|
await gFFI.serverModel.fetchID();
|
||||||
final error = await bind.mainGetError();
|
final error = await bind.mainGetError();
|
||||||
@@ -669,7 +676,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
systemError = error;
|
systemError = error;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
final v = await bind.mainGetOption(key: "stop-service") == "Y";
|
final v = await mainGetBoolOption(kOptionStopService);
|
||||||
if (v != svcStopped.value) {
|
if (v != svcStopped.value) {
|
||||||
svcStopped.value = v;
|
svcStopped.value = v;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -836,7 +843,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPasswordDialog() async {
|
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||||
final pw = await bind.mainGetPermanentPassword();
|
final pw = await bind.mainGetPermanentPassword();
|
||||||
final p0 = TextEditingController(text: pw);
|
final p0 = TextEditingController(text: pw);
|
||||||
final p1 = TextEditingController(text: pw);
|
final p1 = TextEditingController(text: pw);
|
||||||
@@ -876,6 +883,9 @@ void setPasswordDialog() async {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.mainSetPermanentPassword(password: pass);
|
bind.mainSetPermanentPassword(password: pass);
|
||||||
|
if (pass.isNotEmpty) {
|
||||||
|
notEmptyCallback?.call();
|
||||||
|
}
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
|||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
// import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
static void onAddSetting(
|
static void onAddSetting(
|
||||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||||
try {
|
try {
|
||||||
DesktopTabController tabController = Get.find();
|
DesktopTabController tabController = Get.find<DesktopTabController>();
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: kTabLabelSettingPage,
|
key: kTabLabelSettingPage,
|
||||||
label: kTabLabelSettingPage,
|
label: kTabLabelSettingPage,
|
||||||
@@ -36,12 +37,26 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
class _DesktopTabPageState extends State<DesktopTabPage>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
|
final RxBool _block = false.obs;
|
||||||
|
// bool mouseIn = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
} else if (state == AppLifecycleState.inactive) {}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
Get.put<DesktopTabController>(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
@@ -66,10 +81,23 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
bool _handleKeyEvent(KeyEvent event) {
|
||||||
|
if (!mouseIn && event is KeyDownEvent) {
|
||||||
|
print('key down: ${event.logicalKey}');
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
}
|
||||||
|
return false; // allow it to propagate
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
Get.delete<DesktopTabController>();
|
Get.delete<DesktopTabController>();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,6 +116,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
isClose: false,
|
isClose: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blockTab: _block,
|
||||||
)));
|
)));
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? tabWidget
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
_ffi.dialogManager
|
_ffi.dialogManager
|
||||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
});
|
});
|
||||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
|
||||||
if (!isLinux) {
|
if (!isLinux) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
tabController.clear();
|
tabController.clear();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
forceRelay: widget.forceRelay,
|
forceRelay: widget.forceRelay,
|
||||||
isRdp: widget.isRDP);
|
isRdp: widget.isRDP);
|
||||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||||
debugPrint("Port forward page init success with id ${widget.id}");
|
debugPrint("Port forward page init success with id ${widget.id}");
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController.onSelected?.call(widget.id);
|
||||||
}
|
}
|
||||||
@@ -141,8 +141,9 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context)
|
data: Theme.of(context).copyWith(
|
||||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
colorScheme: Theme.of(context).colorScheme,
|
||||||
|
),
|
||||||
child: Obx(() => ListView.builder(
|
child: Obx(() => ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: pfs.length + 2,
|
itemCount: pfs.length + 2,
|
||||||
@@ -289,7 +290,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
).marginOnly(left: _kTextLeftMargin));
|
).marginOnly(left: _kTextLeftMargin));
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context)
|
data: Theme.of(context)
|
||||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
.copyWith(colorScheme: Theme.of(context).colorScheme),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: 2,
|
itemCount: 2,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import '../../common.dart';
|
|||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
import '../../common/widgets/toolbar.dart';
|
import '../../common/widgets/toolbar.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/desktop_render_texture.dart';
|
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
@@ -86,6 +85,9 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||||
|
|
||||||
|
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
||||||
|
// to identify the toolbar instance and its callback function.
|
||||||
|
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
||||||
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
@@ -94,7 +96,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
void _initStates(String id) {
|
void _initStates(String id) {
|
||||||
initSharedStates(id);
|
initSharedStates(id);
|
||||||
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||||
@@ -105,7 +107,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_initStates(widget.id);
|
_initStates(widget.id);
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -132,11 +134,12 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
// Session option should be set after models.dart/FFI.start
|
// Session option should be set after models.dart/FFI.start
|
||||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'zoom-cursor');
|
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
// if (!_isCustomCursorInited) {
|
// if (!_isCustomCursorInited) {
|
||||||
// customCursorController.registerNeedUpdateCursorCallback(
|
// customCursorController.registerNeedUpdateCursorCallback(
|
||||||
@@ -209,6 +212,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
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
||||||
@@ -221,6 +240,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
DesktopMultiWindow.removeListener(this);
|
DesktopMultiWindow.removeListener(this);
|
||||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||||
|
_ffi.imageModel.disposeImage();
|
||||||
|
_ffi.cursorModel.disposeImages();
|
||||||
_ffi.recordingModel.onClose();
|
_ffi.recordingModel.onClose();
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
await _ffi.close(closeSession: closeSession);
|
await _ffi.close(closeSession: closeSession);
|
||||||
@@ -251,9 +272,18 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
id: widget.id,
|
id: widget.id,
|
||||||
ffi: _ffi,
|
ffi: _ffi,
|
||||||
state: widget.toolbarState,
|
state: widget.toolbarState,
|
||||||
onEnterOrLeaveImageSetter: (func) =>
|
onEnterOrLeaveImageSetter: (id, func) {
|
||||||
_onEnterOrLeaveImage4Toolbar = func,
|
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
|
||||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
|
_onEnterOrLeaveImage4Toolbar = func;
|
||||||
|
},
|
||||||
|
onEnterOrLeaveImageCleaner: (id) {
|
||||||
|
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
|
||||||
|
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
|
||||||
|
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
|
||||||
|
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
|
||||||
|
_onEnterOrLeaveImage4Toolbar = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
setRemoteState: setState,
|
setRemoteState: setState,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -261,7 +291,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black,
|
color: kColorCanvas,
|
||||||
child: RawKeyFocusScope(
|
child: RawKeyFocusScope(
|
||||||
focusNode: _rawKeyFocusNode,
|
focusNode: _rawKeyFocusNode,
|
||||||
onFocusChange: (bool imageFocused) {
|
onFocusChange: (bool imageFocused) {
|
||||||
@@ -290,8 +320,21 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||||
? emptyOverlay()
|
? emptyOverlay()
|
||||||
: () {
|
: () {
|
||||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
|
} else {
|
||||||
|
return Obx(() => Offstage(
|
||||||
|
offstage: _ffi.dialogManager
|
||||||
|
.mobileActionsOverlayVisible.isFalse,
|
||||||
|
child: Overlay(initialEntries: [
|
||||||
|
makeMobileActionsOverlayEntry(
|
||||||
|
() => _ffi.dialogManager
|
||||||
|
.setMobileActionsOverlayVisible(false),
|
||||||
|
ffi: _ffi,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
}(),
|
}(),
|
||||||
// Use Overlay to enable rebuild every time on menu button click.
|
// Use Overlay to enable rebuild every time on menu button click.
|
||||||
_ffi.ffiModel.pi.isSet.isTrue
|
_ffi.ffiModel.pi.isSet.isTrue
|
||||||
@@ -438,14 +481,14 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}, onExit: (evt) {
|
}, onExit: (evt) {
|
||||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||||
Future.delayed(Duration.zero, () {
|
final c = Provider.of<CanvasModel>(context, listen: false);
|
||||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
||||||
});
|
|
||||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||||
return Obx(
|
return Obx(
|
||||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||||
? Container(color: Colors.transparent)
|
? Container(color: Colors.transparent)
|
||||||
: Obx(() {
|
: Obx(() {
|
||||||
|
widget.toolbarState.initShow(sessionId);
|
||||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||||
return ImagePaint(
|
return ImagePaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
@@ -463,12 +506,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||||
paints.add(Obx(() => Offstage(
|
paints
|
||||||
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
|
||||||
child: CursorPaint(
|
? Offstage()
|
||||||
id: widget.id,
|
: CursorPaint(
|
||||||
zoomCursor: _zoomCursor,
|
id: widget.id,
|
||||||
))));
|
zoomCursor: _zoomCursor,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
paints.add(
|
paints.add(
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -575,15 +619,15 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
onHover: (evt) {},
|
onHover: (evt) {},
|
||||||
child: child);
|
child: child);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||||
final paintWidth = c.getDisplayWidth() * s;
|
final paintWidth = c.getDisplayWidth() * s;
|
||||||
final paintHeight = c.getDisplayHeight() * s;
|
final paintHeight = c.getDisplayHeight() * s;
|
||||||
final paintSize = Size(paintWidth, paintHeight);
|
final paintSize = Size(paintWidth, paintHeight);
|
||||||
final paintWidget = useTextureRender
|
final paintWidget =
|
||||||
? _BuildPaintTextureRender(
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
? _BuildPaintTextureRender(
|
||||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||||
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
c.updateScrollPercent();
|
c.updateScrollPercent();
|
||||||
@@ -601,17 +645,18 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
if (c.size.width > 0 && c.size.height > 0) {
|
if (c.size.width > 0 && c.size.height > 0) {
|
||||||
final paintWidget = useTextureRender
|
final paintWidget =
|
||||||
? _BuildPaintTextureRender(
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||||
c,
|
? _BuildPaintTextureRender(
|
||||||
s,
|
c,
|
||||||
Offset(
|
s,
|
||||||
isLinux ? c.x.toInt().toDouble() : c.x,
|
Offset(
|
||||||
isLinux ? c.y.toInt().toDouble() : c.y,
|
isLinux ? c.x.toInt().toDouble() : c.x,
|
||||||
),
|
isLinux ? c.y.toInt().toDouble() : c.y,
|
||||||
c.size,
|
),
|
||||||
isViewOriginal())
|
c.size,
|
||||||
: _buildScrollAuthNonTextureRender(m, c, s);
|
isViewOriginal())
|
||||||
|
: _buildScrollAutoNonTextureRender(m, c, s);
|
||||||
return mouseRegion(child: _buildListener(paintWidget));
|
return mouseRegion(child: _buildListener(paintWidget));
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -627,7 +672,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScrollAuthNonTextureRender(
|
Widget _buildScrollAutoNonTextureRender(
|
||||||
ImageModel m, CanvasModel c, double s) {
|
ImageModel m, CanvasModel c, double s) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
size: Size(c.size.width, c.size.height),
|
size: Size(c.size.width, c.size.height),
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
|
|
||||||
late ToolbarState _toolbarState;
|
|
||||||
String? peerId;
|
String? peerId;
|
||||||
bool _isScreenRectSet = false;
|
bool _isScreenRectSet = false;
|
||||||
int? _display;
|
int? _display;
|
||||||
@@ -54,7 +53,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||||
|
|
||||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||||
_toolbarState = ToolbarState();
|
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
peerId = params['id'];
|
peerId = params['id'];
|
||||||
final sessionId = params['session_id'];
|
final sessionId = params['session_id'];
|
||||||
@@ -91,7 +89,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
display: display,
|
display: display,
|
||||||
displays: displays?.cast<int>(),
|
displays: displays?.cast<int>(),
|
||||||
password: params['password'],
|
password: params['password'],
|
||||||
toolbarState: _toolbarState,
|
toolbarState: ToolbarState(),
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
switchUuid: params['switch_uuid'],
|
switchUuid: params['switch_uuid'],
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
@@ -126,7 +124,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_toolbarState.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -251,15 +248,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final pi = ffi.ffiModel.pi;
|
final pi = ffi.ffiModel.pi;
|
||||||
final perms = ffi.ffiModel.permissions;
|
final perms = ffi.ffiModel.permissions;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
final toolbarState = remotePage.toolbarState;
|
||||||
menu.addAll([
|
menu.addAll([
|
||||||
MenuEntryButton<String>(
|
MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||||
translate(
|
translate(
|
||||||
_toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
style: style,
|
style: style,
|
||||||
)),
|
)),
|
||||||
proc: () {
|
proc: () {
|
||||||
_toolbarState.switchShow();
|
toolbarState.switchShow(sessionId);
|
||||||
cancelFunc();
|
cancelFunc();
|
||||||
},
|
},
|
||||||
padding: padding,
|
padding: padding,
|
||||||
@@ -350,7 +348,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
void onRemoveId(String id) async {
|
void onRemoveId(String id) async {
|
||||||
if (tabController.state.value.tabs.isEmpty) {
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
|
||||||
// Keep calling until the window status is hidden.
|
// Keep calling until the window status is hidden.
|
||||||
//
|
//
|
||||||
// Workaround for Windows:
|
// Workaround for Windows:
|
||||||
@@ -384,9 +381,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
tabController.clear();
|
tabController.clear();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
@@ -416,19 +413,17 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final display = args['display'];
|
final display = args['display'];
|
||||||
final displays = args['displays'];
|
final displays = args['displays'];
|
||||||
final screenRect = parseParamScreenRect(args);
|
final screenRect = parseParamScreenRect(args);
|
||||||
windowOnTop(windowId());
|
Future.delayed(Duration.zero, () async {
|
||||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
if (stateGlobal.fullscreen.isTrue) {
|
||||||
if (tabController.length == 0) {
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||||
// Show the hidden window.
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
|
|
||||||
stateGlobal.setFullscreen(true);
|
|
||||||
}
|
}
|
||||||
// Reset the state
|
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
|
||||||
stateGlobal.closeOnFullscreen = null;
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||||
}
|
await windowOnTop(windowId());
|
||||||
|
});
|
||||||
|
});
|
||||||
ConnectionTypeState.init(id);
|
ConnectionTypeState.init(id);
|
||||||
_toolbarState.setShow(
|
|
||||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: id,
|
key: id,
|
||||||
label: id,
|
label: id,
|
||||||
@@ -443,7 +438,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
display: display,
|
display: display,
|
||||||
displays: displays?.cast<int>(),
|
displays: displays?.cast<int>(),
|
||||||
password: args['password'],
|
password: args['password'],
|
||||||
toolbarState: _toolbarState,
|
toolbarState: ToolbarState(),
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
switchUuid: switchUuid,
|
switchUuid: switchUuid,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
@@ -522,6 +517,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
returnValue = jsonEncode(coords.toJson());
|
returnValue = jsonEncode(coords.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (call.method == kWindowEventSetFullscreen) {
|
||||||
|
stateGlobal.setFullscreen(call.arguments == 'true');
|
||||||
}
|
}
|
||||||
_update_remote_count();
|
_update_remote_count();
|
||||||
return returnValue;
|
return returnValue;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
void initState() {
|
void initState() {
|
||||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
Get.put(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
tabController.onRemoved = (_, id) {
|
tabController.onRemoved = (_, id) {
|
||||||
onRemoveId(id);
|
onRemoveId(id);
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,20 @@ class ConnectionManager extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => ConnectionManagerState();
|
State<StatefulWidget> createState() => ConnectionManagerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConnectionManagerState extends State<ConnectionManager> {
|
class ConnectionManagerState extends State<ConnectionManager>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
final RxBool _block = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
if (!allowRemoteCMModification()) {
|
||||||
|
shouldBeBlocked(_block, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
gFFI.serverModel.updateClientState();
|
gFFI.serverModel.updateClientState();
|
||||||
@@ -127,9 +140,16 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
gFFI.chatModel.isConnManager = true;
|
gFFI.chatModel.isConnManager = true;
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
@@ -165,6 +185,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
maxLabelWidth: 100,
|
maxLabelWidth: 100,
|
||||||
tail: null, //buildScrollJumper(),
|
tail: null, //buildScrollJumper(),
|
||||||
|
blockTab: allowRemoteCMModification() ? null : _block,
|
||||||
selectedTabBackgroundColor:
|
selectedTabBackgroundColor:
|
||||||
Theme.of(context).hintColor.withOpacity(0),
|
Theme.of(context).hintColor.withOpacity(0),
|
||||||
tabBuilder: (key, icon, label, themeConf) {
|
tabBuilder: (key, icon, label, themeConf) {
|
||||||
@@ -207,15 +228,12 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
Consumer<ChatModel>(
|
Consumer<ChatModel>(
|
||||||
builder: (_, model, child) => SizedBox(
|
builder: (_, model, child) => SizedBox(
|
||||||
width: realChatPageWidth,
|
width: realChatPageWidth,
|
||||||
child: buildRemoteBlock(
|
child: allowRemoteCMModification()
|
||||||
child: Container(
|
? buildSidePage()
|
||||||
decoration: BoxDecoration(
|
: buildRemoteBlock(
|
||||||
border: Border(
|
child: buildSidePage(),
|
||||||
right: BorderSide(
|
block: _block,
|
||||||
color: Theme.of(context)
|
mask: true),
|
||||||
.dividerColor))),
|
|
||||||
child: buildSidePage()),
|
|
||||||
),
|
|
||||||
)),
|
)),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: realClosedWidth,
|
width: realClosedWidth,
|
||||||
@@ -289,9 +307,9 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
windowManager.close();
|
windowManager.close();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
@@ -714,7 +732,7 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
child: buildButton(context,
|
child: buildButton(context,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
onClick: null, onTapDown: (details) async {
|
onClick: null, onTapDown: (details) async {
|
||||||
final devicesInfo = await AudioInput.getDevicesInfo();
|
final devicesInfo = await AudioInput.getDevicesInfo(true, true);
|
||||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
msgBox(
|
msgBox(
|
||||||
@@ -740,13 +758,13 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
value: d,
|
value: d,
|
||||||
height: 18,
|
height: 18,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () => AudioInput.setDevice(d),
|
onTap: () => AudioInput.setDevice(d, true, true),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: RadioMenuButton(
|
child: RadioMenuButton(
|
||||||
value: d,
|
value: d,
|
||||||
groupValue: currentDevice,
|
groupValue: currentDevice,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v != null) AudioInput.setDevice(v);
|
if (v != null) AudioInput.setDevice(v, true, true);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1043,6 +1061,10 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void checkClickTime(int id, Function() callback) async {
|
void checkClickTime(int id, Function() callback) async {
|
||||||
|
if (allowRemoteCMModification()) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
await bind.cmCheckClickTime(connId: id);
|
await bind.cmCheckClickTime(connId: id);
|
||||||
Timer(const Duration(milliseconds: 120), () async {
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
@@ -1051,6 +1073,11 @@ void checkClickTime(int id, Function() callback) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool allowRemoteCMModification() {
|
||||||
|
return option2bool(kOptionAllowRemoteCmModification,
|
||||||
|
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
|
||||||
|
}
|
||||||
|
|
||||||
class _FileTransferLogPage extends StatefulWidget {
|
class _FileTransferLogPage extends StatefulWidget {
|
||||||
_FileTransferLogPage({Key? key}) : super(key: key);
|
_FileTransferLogPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
|||||||
/// The text style of the popup menu item.
|
/// The text style of the popup menu item.
|
||||||
///
|
///
|
||||||
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
||||||
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1]
|
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
|
||||||
/// of [ThemeData.textTheme] is used.
|
/// of [ThemeData.textTheme] is used.
|
||||||
final TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
@@ -341,8 +341,9 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
@protected
|
@protected
|
||||||
void handleTap() {
|
void handleTap() {
|
||||||
widget.onTap?.call();
|
widget.onTap?.call();
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
Navigator.pop<T>(context, widget.value);
|
Navigator.pop<T>(context, widget.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -351,7 +352,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
TextStyle style = widget.textStyle ??
|
TextStyle style = widget.textStyle ??
|
||||||
popupMenuTheme.textStyle ??
|
popupMenuTheme.textStyle ??
|
||||||
theme.textTheme.subtitle1!;
|
theme.textTheme.titleMedium!;
|
||||||
|
|
||||||
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
|||||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
TextStyle style = widget.textStyle ??
|
TextStyle style = widget.textStyle ??
|
||||||
popupMenuTheme.textStyle ??
|
popupMenuTheme.textStyle ??
|
||||||
theme.textTheme.subtitle1!;
|
theme.textTheme.titleMedium!;
|
||||||
return Obx(() => mod_menu.PopupMenuButton<T>(
|
return Obx(() => mod_menu.PopupMenuButton<T>(
|
||||||
enabled: enabled.value,
|
enabled: enabled.value,
|
||||||
position: widget.position,
|
position: widget.position,
|
||||||
@@ -445,9 +445,18 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
dismissCallback: dismissCallback,
|
dismissCallback: dismissCallback,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bool get isEnabled => enabled?.value ?? true;
|
||||||
|
|
||||||
RxBool get curOption;
|
RxBool get curOption;
|
||||||
Future<void> setOption(bool? option);
|
Future<void> setOption(bool? option);
|
||||||
|
|
||||||
|
tryPop(BuildContext context) {
|
||||||
|
if (dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
super.dismissCallback?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<mod_menu.PopupMenuEntry<T>> build(
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
BuildContext context, MenuConfig conf) {
|
BuildContext context, MenuConfig conf) {
|
||||||
@@ -481,44 +490,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
if (switchType == SwitchType.sswitch) {
|
if (switchType == SwitchType.sswitch) {
|
||||||
return Switch(
|
return Switch(
|
||||||
value: curOption.value,
|
value: curOption.value,
|
||||||
onChanged: (v) {
|
onChanged: isEnabled
|
||||||
if (super.dismissOnClicked &&
|
? (v) {
|
||||||
Navigator.canPop(context)) {
|
tryPop(context);
|
||||||
Navigator.pop(context);
|
setOption(v);
|
||||||
if (super.dismissCallback != null) {
|
}
|
||||||
super.dismissCallback!();
|
: null,
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(v);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Checkbox(
|
return Checkbox(
|
||||||
value: curOption.value,
|
value: curOption.value,
|
||||||
onChanged: (v) {
|
onChanged: isEnabled
|
||||||
if (super.dismissOnClicked &&
|
? (v) {
|
||||||
Navigator.canPop(context)) {
|
tryPop(context);
|
||||||
Navigator.pop(context);
|
setOption(v);
|
||||||
if (super.dismissCallback != null) {
|
}
|
||||||
super.dismissCallback!();
|
: null,
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(v);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
])),
|
])),
|
||||||
onPressed: () {
|
onPressed: isEnabled
|
||||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
? () {
|
||||||
Navigator.pop(context);
|
tryPop(context);
|
||||||
if (super.dismissCallback != null) {
|
setOption(!curOption.value);
|
||||||
super.dismissCallback!();
|
}
|
||||||
}
|
: null,
|
||||||
}
|
|
||||||
setOption(!curOption.value);
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
|||||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||||
@@ -27,46 +26,42 @@ import './popup_menu.dart';
|
|||||||
import './kb_layout_type_chooser.dart';
|
import './kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
class ToolbarState {
|
class ToolbarState {
|
||||||
final kStoreKey = 'remoteMenubarState';
|
|
||||||
late RxBool show;
|
|
||||||
late RxBool _pin;
|
late RxBool _pin;
|
||||||
|
|
||||||
|
bool isShowInited = false;
|
||||||
|
RxBool show = false.obs;
|
||||||
|
|
||||||
ToolbarState() {
|
ToolbarState() {
|
||||||
final s = bind.getLocalFlutterOption(k: kStoreKey);
|
_pin = RxBool(false);
|
||||||
|
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||||
if (s.isEmpty) {
|
if (s.isEmpty) {
|
||||||
_initSet(false, false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final m = jsonDecode(s);
|
final m = jsonDecode(s);
|
||||||
if (m == null) {
|
if (m != null) {
|
||||||
_initSet(false, false);
|
_pin = RxBool(m['pin'] ?? false);
|
||||||
} else {
|
|
||||||
_initSet(m['pin'] ?? false, m['pin'] ?? false);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
||||||
_initSet(false, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initSet(bool s, bool p) {
|
|
||||||
// Show remubar when connection is established.
|
|
||||||
show =
|
|
||||||
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
|
||||||
_pin = RxBool(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get pin => _pin.value;
|
bool get pin => _pin.value;
|
||||||
|
|
||||||
switchShow() async {
|
switchShow(SessionID sessionId) async {
|
||||||
|
bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: kOptionCollapseToolbar);
|
||||||
show.value = !show.value;
|
show.value = !show.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShow(bool v) async {
|
initShow(SessionID sessionId) async {
|
||||||
if (show.value != v) {
|
if (!isShowInited) {
|
||||||
show.value = v;
|
show.value = !(await bind.sessionGetToggleOption(
|
||||||
|
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
|
||||||
|
false);
|
||||||
|
isShowInited = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,11 +81,7 @@ class ToolbarState {
|
|||||||
|
|
||||||
_savePin() async {
|
_savePin() async {
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
|
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||||
}
|
|
||||||
|
|
||||||
save() async {
|
|
||||||
await _savePin();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,8 +325,8 @@ class RemoteToolbar extends StatefulWidget {
|
|||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final ToolbarState state;
|
final ToolbarState state;
|
||||||
final Function(Function(bool)) onEnterOrLeaveImageSetter;
|
final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
|
||||||
final VoidCallback onEnterOrLeaveImageCleaner;
|
final Function(int) onEnterOrLeaveImageCleaner;
|
||||||
final Function(VoidCallback) setRemoteState;
|
final Function(VoidCallback) setRemoteState;
|
||||||
|
|
||||||
RemoteToolbar({
|
RemoteToolbar({
|
||||||
@@ -395,7 +386,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
widget.onEnterOrLeaveImageSetter((enter) {
|
widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
|
||||||
if (enter) {
|
if (enter) {
|
||||||
triggerAutoHide();
|
triggerAutoHide();
|
||||||
_isCursorOverImage = true;
|
_isCursorOverImage = true;
|
||||||
@@ -415,12 +406,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
dispose() {
|
dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
widget.onEnterOrLeaveImageCleaner();
|
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// No need to use future builder here.
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Obx(() => show.value
|
child: Obx(() => show.value
|
||||||
@@ -449,7 +439,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fractionX: _fractionX,
|
fractionX: _fractionX,
|
||||||
show: show,
|
toolbarState: widget.state,
|
||||||
setFullscreen: _setFullscreen,
|
setFullscreen: _setFullscreen,
|
||||||
setMinimize: _minimize,
|
setMinimize: _minimize,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
@@ -589,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
return Obx(() => _IconMenuButton(
|
return Obx(() => _IconMenuButton(
|
||||||
assetName: 'assets/actions_mobile.svg',
|
assetName: 'assets/actions_mobile.svg',
|
||||||
tooltip: 'Mobile Actions',
|
tooltip: 'Mobile Actions',
|
||||||
onPressed: () =>
|
onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
|
||||||
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
|
!ffi.dialogManager.mobileActionsOverlayVisible.value),
|
||||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||||
? _ToolbarTheme.blueColor
|
? _ToolbarTheme.blueColor
|
||||||
: _ToolbarTheme.inactiveColor,
|
: _ToolbarTheme.inactiveColor,
|
||||||
@@ -616,14 +606,14 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
||||||
|
|
||||||
bool get supportIndividualWindows =>
|
bool get supportIndividualWindows =>
|
||||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => showMonitorsToolbar
|
Widget build(BuildContext context) => showMonitorsToolbar
|
||||||
? buildMultiMonitorMenu()
|
? buildMultiMonitorMenu(context)
|
||||||
: Obx(() => buildMonitorMenu());
|
: Obx(() => buildMonitorMenu(context));
|
||||||
|
|
||||||
Widget buildMonitorMenu() {
|
Widget buildMonitorMenu(BuildContext context) {
|
||||||
final width = SimpleWrapper<double>(0);
|
final width = SimpleWrapper<double>(0);
|
||||||
final monitorsIcon =
|
final monitorsIcon =
|
||||||
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
||||||
@@ -637,18 +627,18 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
menuStyle: MenuStyle(
|
menuStyle: MenuStyle(
|
||||||
padding:
|
padding:
|
||||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
|
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMultiMonitorMenu() {
|
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||||
return Row(children: buildMonitorList(true));
|
return Row(children: buildMonitorList(context, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMonitorSubmenuWidget() {
|
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(children: buildMonitorList(false)),
|
Row(children: buildMonitorList(context, false)),
|
||||||
supportIndividualWindows ? Divider() : Offstage(),
|
supportIndividualWindows ? Divider() : Offstage(),
|
||||||
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
||||||
],
|
],
|
||||||
@@ -664,7 +654,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionSetDisplaysAsIndividualWindows(
|
await bind.sessionSetDisplaysAsIndividualWindows(
|
||||||
sessionId: ffi.sessionId, value: value ? 'Y' : '');
|
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
child: Text(translate('Show displays as individual windows')));
|
child: Text(translate('Show displays as individual windows')));
|
||||||
@@ -681,7 +671,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> buildMonitorList(bool isMulti) {
|
List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
|
||||||
final List<Widget> monitorList = [];
|
final List<Widget> monitorList = [];
|
||||||
final pi = ffi.ffiModel.pi;
|
final pi = ffi.ffiModel.pi;
|
||||||
|
|
||||||
@@ -819,7 +809,11 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
RxInt display = CurrentDisplayState.find(id);
|
RxInt display = CurrentDisplayState.find(id);
|
||||||
if (display.value != i) {
|
if (display.value != i) {
|
||||||
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
|
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||||
|
bind.sessionGetDisplaysAsIndividualWindows(
|
||||||
|
sessionId: ffi.sessionId) ==
|
||||||
|
'Y';
|
||||||
|
if (isChooseDisplayToOpenInNewWindow) {
|
||||||
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
||||||
} else {
|
} else {
|
||||||
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
||||||
@@ -1058,17 +1052,12 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
screenAdjustor: _screenAdjustor,
|
screenAdjustor: _screenAdjustor,
|
||||||
),
|
),
|
||||||
if (pi.isRustDeskIdd)
|
if (showVirtualDisplayMenu(ffi))
|
||||||
_RustDeskVirtualDisplayMenu(
|
_SubmenuButton(
|
||||||
id: widget.id,
|
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
|
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
),
|
),
|
||||||
if (pi.isAmyuniIdd)
|
|
||||||
_AmyuniVirtualDisplayMenu(
|
|
||||||
id: widget.id,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
cursorToggles(),
|
cursorToggles(),
|
||||||
Divider(),
|
Divider(),
|
||||||
toggles(),
|
toggles(),
|
||||||
@@ -1220,14 +1209,16 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
final v = data as List<TToggleMenu>;
|
final v = data as List<TToggleMenu>;
|
||||||
if (v.isEmpty) return Offstage();
|
if (v.isEmpty) return Offstage();
|
||||||
return Column(
|
return Column(children: [
|
||||||
children: v
|
Divider(),
|
||||||
.map((e) => CkbMenuButton(
|
...v
|
||||||
value: e.value,
|
.map((e) => CkbMenuButton(
|
||||||
onChanged: e.onChanged,
|
value: e.value,
|
||||||
child: e.child,
|
onChanged: e.onChanged,
|
||||||
ffi: ffi))
|
child: e.child,
|
||||||
.toList());
|
ffi: ffi))
|
||||||
|
.toList(),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1564,155 +1555,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
|
|
||||||
final String id;
|
|
||||||
final FFI ffi;
|
|
||||||
|
|
||||||
_RustDeskVirtualDisplayMenu({
|
|
||||||
Key? key,
|
|
||||||
required this.id,
|
|
||||||
required this.ffi,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_RustDeskVirtualDisplayMenu> createState() =>
|
|
||||||
_RustDeskVirtualDisplayMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RustDeskVirtualDisplayMenuState
|
|
||||||
extends State<_RustDeskVirtualDisplayMenu> {
|
|
||||||
@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 virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
|
||||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
|
||||||
|
|
||||||
final children = <Widget>[];
|
|
||||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
|
||||||
children.add(Obx(() => CkbMenuButton(
|
|
||||||
value: virtualDisplays.contains(i + 1),
|
|
||||||
onChanged: privacyModeState.isNotEmpty
|
|
||||||
? null
|
|
||||||
: (bool? value) async {
|
|
||||||
if (value != null) {
|
|
||||||
bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId,
|
|
||||||
index: i + 1,
|
|
||||||
on: value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
|
||||||
ffi: widget.ffi,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
children.add(Divider());
|
|
||||||
children.add(Obx(() => MenuButton(
|
|
||||||
onPressed: privacyModeState.isNotEmpty
|
|
||||||
? 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 _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 {
|
class _KeyboardMenu extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
@@ -1746,6 +1588,7 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
viewMode(),
|
viewMode(),
|
||||||
Divider(),
|
Divider(),
|
||||||
...toolbarToggles(),
|
...toolbarToggles(),
|
||||||
|
...mobileActions(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1875,13 +1718,46 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
? (value) async {
|
? (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
sessionId: ffi.sessionId, value: kOptionViewOnly);
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
ffiModel.setViewOnly(id, value);
|
ffiModel.setViewOnly(id, value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
child: Text(translate('View Mode')));
|
child: Text(translate('View Mode')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mobileActions() {
|
||||||
|
if (pi.platform != kPeerPlatformAndroid) return [];
|
||||||
|
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
|
||||||
|
if (!enabled) return [];
|
||||||
|
return [
|
||||||
|
Divider(),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Back')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileBack(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Home')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileHome(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Apps')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileApps(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Volume up')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Volume down')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Power')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobilePower(),
|
||||||
|
ffi: ffi),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatMenu extends StatefulWidget {
|
class _ChatMenu extends StatefulWidget {
|
||||||
@@ -1955,28 +1831,31 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
menuChildrenGetter() {
|
menuChildrenGetter() {
|
||||||
final audioInput =
|
final audioInput = AudioInput(
|
||||||
AudioInput(builder: (devices, currentDevice, setDevice) {
|
builder: (devices, currentDevice, setDevice) {
|
||||||
return Column(
|
return Column(
|
||||||
children: devices
|
children: devices
|
||||||
.map((d) => RdoMenuButton<String>(
|
.map((d) => RdoMenuButton<String>(
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
d,
|
d,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxWidth: 250),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints(maxWidth: 250),
|
value: d,
|
||||||
),
|
groupValue: currentDevice,
|
||||||
value: d,
|
onChanged: (v) {
|
||||||
groupValue: currentDevice,
|
if (v != null) setDevice(v);
|
||||||
onChanged: (v) {
|
},
|
||||||
if (v != null) setDevice(v);
|
ffi: ffi,
|
||||||
},
|
))
|
||||||
ffi: ffi,
|
.toList(),
|
||||||
))
|
);
|
||||||
.toList(),
|
},
|
||||||
);
|
isCm: false,
|
||||||
});
|
isVoiceCall: true,
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
audioInput,
|
audioInput,
|
||||||
Divider(),
|
Divider(),
|
||||||
@@ -2019,6 +1898,7 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecordMenu extends StatelessWidget {
|
class _RecordMenu extends StatelessWidget {
|
||||||
const _RecordMenu({Key? key}) : super(key: key);
|
const _RecordMenu({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -2340,7 +2220,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fractionX;
|
final RxDouble fractionX;
|
||||||
final RxBool dragging;
|
final RxBool dragging;
|
||||||
final RxBool show;
|
final ToolbarState toolbarState;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
|
|
||||||
final Function(bool) setFullscreen;
|
final Function(bool) setFullscreen;
|
||||||
@@ -2351,7 +2231,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fractionX,
|
required this.fractionX,
|
||||||
required this.dragging,
|
required this.dragging,
|
||||||
required this.show,
|
required this.toolbarState,
|
||||||
required this.setFullscreen,
|
required this.setFullscreen,
|
||||||
required this.setMinimize,
|
required this.setMinimize,
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
@@ -2367,23 +2247,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
double left = 0.0;
|
double left = 0.0;
|
||||||
double right = 1.0;
|
double right = 1.0;
|
||||||
|
|
||||||
|
RxBool get show => widget.toolbarState.show;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
final confLeft = double.tryParse(
|
final confLeft = double.tryParse(
|
||||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-left'));
|
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
|
||||||
if (confLeft == null) {
|
if (confLeft == null) {
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: 'remote-menubar-drag-left', value: left.toString());
|
key: kOptionRemoteMenubarDragLeft, value: left.toString());
|
||||||
} else {
|
} else {
|
||||||
left = confLeft;
|
left = confLeft;
|
||||||
}
|
}
|
||||||
final confRight = double.tryParse(
|
final confRight = double.tryParse(
|
||||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-right'));
|
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
|
||||||
if (confRight == null) {
|
if (confRight == null) {
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: 'remote-menubar-drag-right', value: right.toString());
|
key: kOptionRemoteMenubarDragRight, value: right.toString());
|
||||||
} else {
|
} else {
|
||||||
right = confRight;
|
right = confRight;
|
||||||
}
|
}
|
||||||
@@ -2454,28 +2336,29 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
Obx(() => Offstage(
|
if (!isMacOS)
|
||||||
offstage: isFullscreen.isFalse,
|
Obx(() => Offstage(
|
||||||
child: TextButton(
|
offstage: isFullscreen.isFalse,
|
||||||
onPressed: () => widget.setMinimize(),
|
child: TextButton(
|
||||||
child: Tooltip(
|
onPressed: () => widget.setMinimize(),
|
||||||
message: translate('Minimize'),
|
child: Tooltip(
|
||||||
child: Icon(
|
message: translate('Minimize'),
|
||||||
Icons.remove,
|
child: Icon(
|
||||||
size: iconSize,
|
Icons.remove,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)),
|
||||||
)),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
widget.show.value = !widget.show.value;
|
widget.toolbarState.switchShow(widget.sessionId);
|
||||||
}),
|
}),
|
||||||
child: Obx((() => Tooltip(
|
child: Obx((() => Tooltip(
|
||||||
message: translate(
|
message:
|
||||||
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
|
|||||||
@@ -230,8 +230,7 @@ typedef LabelGetter = Rx<String> Function(String key);
|
|||||||
int _lastClickTime =
|
int _lastClickTime =
|
||||||
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
class DesktopTab extends StatefulWidget {
|
||||||
class DesktopTab extends StatelessWidget {
|
|
||||||
final bool showLogo;
|
final bool showLogo;
|
||||||
final bool showTitle;
|
final bool showTitle;
|
||||||
final bool showMinimize;
|
final bool showMinimize;
|
||||||
@@ -248,15 +247,12 @@ class DesktopTab extends StatelessWidget {
|
|||||||
final Color? selectedTabBackgroundColor;
|
final Color? selectedTabBackgroundColor;
|
||||||
final Color? unSelectedTabBackgroundColor;
|
final Color? unSelectedTabBackgroundColor;
|
||||||
final Color? selectedBorderColor;
|
final Color? selectedBorderColor;
|
||||||
|
final RxBool? blockTab;
|
||||||
|
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
|
|
||||||
Rx<DesktopTabState> get state => controller.state;
|
|
||||||
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
||||||
|
|
||||||
late final DesktopTabType tabType;
|
|
||||||
late final bool isMainWindow;
|
|
||||||
|
|
||||||
final RxList<String> invisibleTabKeys = RxList.empty();
|
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||||
|
|
||||||
DesktopTab({
|
DesktopTab({
|
||||||
@@ -277,12 +273,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
this.selectedTabBackgroundColor,
|
this.selectedTabBackgroundColor,
|
||||||
this.unSelectedTabBackgroundColor,
|
this.unSelectedTabBackgroundColor,
|
||||||
this.selectedBorderColor,
|
this.selectedBorderColor,
|
||||||
}) : super(key: key) {
|
this.blockTab,
|
||||||
tabType = controller.tabType;
|
}) : super(key: key);
|
||||||
isMainWindow = tabType == DesktopTabType.main ||
|
|
||||||
tabType == DesktopTabType.cm ||
|
|
||||||
tabType == DesktopTabType.install;
|
|
||||||
}
|
|
||||||
|
|
||||||
static RxString tablabelGetter(String peerId) {
|
static RxString tablabelGetter(String peerId) {
|
||||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
@@ -290,223 +282,56 @@ class DesktopTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<DesktopTab> createState() {
|
||||||
return Column(children: [
|
return _DesktopTabState();
|
||||||
Obx(() => Offstage(
|
|
||||||
offstage: !stateGlobal.showTabBar.isTrue ||
|
|
||||||
(kUseCompatibleUiMode && isHideSingleItem()),
|
|
||||||
child: SizedBox(
|
|
||||||
height: _kTabBarHeight,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: _kTabBarHeight - 1,
|
|
||||||
child: _buildBar(),
|
|
||||||
),
|
|
||||||
const Divider(
|
|
||||||
height: 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
))),
|
|
||||||
Expanded(
|
|
||||||
child: pageViewBuilder != null
|
|
||||||
? pageViewBuilder!(_buildPageView())
|
|
||||||
: _buildPageView())
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBlock({required Widget child}) {
|
|
||||||
if (tabType != DesktopTabType.main) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
return buildRemoteBlock(
|
|
||||||
child: child,
|
|
||||||
use: () async {
|
|
||||||
var access_mode = await bind.mainGetOption(key: 'access-mode');
|
|
||||||
var option = option2bool(
|
|
||||||
'allow-remote-config-modification',
|
|
||||||
await bind.mainGetOption(
|
|
||||||
key: 'allow-remote-config-modification'));
|
|
||||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
|
||||||
Widget _buildPageView() {
|
|
||||||
return _buildBlock(
|
|
||||||
child: Obx(() => PageView(
|
|
||||||
controller: state.value.pageController,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
children: () {
|
|
||||||
/// to-do refactor, separate connection state and UI state for remote session.
|
|
||||||
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
|
||||||
final tabLen = state.value.tabs.length;
|
|
||||||
if (tabLen == _tabWidgets.length) {
|
|
||||||
return _tabWidgets;
|
|
||||||
} else if (_tabWidgets.isNotEmpty &&
|
|
||||||
tabLen == _tabWidgets.length + 1) {
|
|
||||||
/// On add. Use the previous list(pointer) to prevent item's state init twice.
|
|
||||||
/// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built.
|
|
||||||
_tabWidgets.add(state.value.tabs.last.page);
|
|
||||||
return _tabWidgets;
|
|
||||||
} else {
|
|
||||||
/// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal.
|
|
||||||
/// the Widgets in list must enable [AutomaticKeepAliveClientMixin]
|
|
||||||
final newList = state.value.tabs.map((v) => v.page).toList();
|
|
||||||
_tabWidgets = newList;
|
|
||||||
return newList;
|
|
||||||
}
|
|
||||||
}())));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether to show ListView
|
|
||||||
///
|
|
||||||
/// Conditions:
|
|
||||||
/// - hide single item when only has one item (home) on [DesktopTabPage].
|
|
||||||
bool isHideSingleItem() {
|
|
||||||
return state.value.tabs.length == 1 &&
|
|
||||||
(controller.tabType == DesktopTabType.main ||
|
|
||||||
controller.tabType == DesktopTabType.install);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBar() {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
// custom double tap handler
|
|
||||||
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
|
|
||||||
showMaximize
|
|
||||||
? () {
|
|
||||||
final current = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
final elapsed = current - _lastClickTime;
|
|
||||||
_lastClickTime = current;
|
|
||||||
if (elapsed < bind.getDoubleClickTime()) {
|
|
||||||
// onDoubleTap
|
|
||||||
toggleMaximize(isMainWindow)
|
|
||||||
.then((value) => stateGlobal.setMaximized(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
onPanStart: (_) => startDragging(isMainWindow),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Offstage(
|
|
||||||
offstage: !isMacOS,
|
|
||||||
child: const SizedBox(
|
|
||||||
width: 78,
|
|
||||||
)),
|
|
||||||
Offstage(
|
|
||||||
offstage: kUseCompatibleUiMode || isMacOS,
|
|
||||||
child: Row(children: [
|
|
||||||
Offstage(
|
|
||||||
offstage: !showLogo,
|
|
||||||
child: loadIcon(16),
|
|
||||||
),
|
|
||||||
Offstage(
|
|
||||||
offstage: !showTitle,
|
|
||||||
child: const Text(
|
|
||||||
"RustDesk",
|
|
||||||
style: TextStyle(fontSize: 13),
|
|
||||||
).marginOnly(left: 2))
|
|
||||||
]).marginOnly(
|
|
||||||
left: 5,
|
|
||||||
right: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Listener(
|
|
||||||
// handle mouse wheel
|
|
||||||
onPointerSignal: (e) {
|
|
||||||
if (e is PointerScrollEvent) {
|
|
||||||
final sc =
|
|
||||||
controller.state.value.scrollController;
|
|
||||||
if (!sc.canScroll) return;
|
|
||||||
_scrollDebounce.call(() {
|
|
||||||
sc.animateTo(sc.offset + e.scrollDelta.dy,
|
|
||||||
duration: Duration(milliseconds: 200),
|
|
||||||
curve: Curves.ease);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: _ListView(
|
|
||||||
controller: controller,
|
|
||||||
invisibleTabKeys: invisibleTabKeys,
|
|
||||||
tabBuilder: tabBuilder,
|
|
||||||
tabMenuBuilder: tabMenuBuilder,
|
|
||||||
labelGetter: labelGetter,
|
|
||||||
maxLabelWidth: maxLabelWidth,
|
|
||||||
selectedTabBackgroundColor:
|
|
||||||
selectedTabBackgroundColor,
|
|
||||||
unSelectedTabBackgroundColor:
|
|
||||||
unSelectedTabBackgroundColor,
|
|
||||||
selectedBorderColor: selectedBorderColor,
|
|
||||||
))),
|
|
||||||
],
|
|
||||||
))),
|
|
||||||
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
|
||||||
WindowActionPanel(
|
|
||||||
isMainWindow: isMainWindow,
|
|
||||||
tabType: tabType,
|
|
||||||
state: state,
|
|
||||||
tabController: controller,
|
|
||||||
invisibleTabKeys: invisibleTabKeys,
|
|
||||||
tail: tail,
|
|
||||||
showMinimize: showMinimize,
|
|
||||||
showMaximize: showMaximize,
|
|
||||||
showClose: showClose,
|
|
||||||
onClose: onWindowCloseButton,
|
|
||||||
labelGetter: labelGetter,
|
|
||||||
).paddingOnly(left: 10)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WindowActionPanel extends StatefulWidget {
|
// ignore: must_be_immutable
|
||||||
final bool isMainWindow;
|
class _DesktopTabState extends State<DesktopTab>
|
||||||
final DesktopTabType tabType;
|
|
||||||
final Rx<DesktopTabState> state;
|
|
||||||
final DesktopTabController tabController;
|
|
||||||
|
|
||||||
final bool showMinimize;
|
|
||||||
final bool showMaximize;
|
|
||||||
final bool showClose;
|
|
||||||
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.labelGetter})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return WindowActionPanelState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WindowActionPanelState extends State<WindowActionPanel>
|
|
||||||
with MultiWindowListener, WindowListener {
|
with MultiWindowListener, WindowListener {
|
||||||
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||||
Timer? _macOSCheckRestoreTimer;
|
Timer? _macOSCheckRestoreTimer;
|
||||||
int _macOSCheckRestoreCounter = 0;
|
int _macOSCheckRestoreCounter = 0;
|
||||||
|
|
||||||
|
bool get showLogo => widget.showLogo;
|
||||||
|
bool get showTitle => widget.showTitle;
|
||||||
|
bool get showMinimize => widget.showMinimize;
|
||||||
|
bool get showMaximize => widget.showMaximize;
|
||||||
|
bool get showClose => widget.showClose;
|
||||||
|
Widget Function(Widget pageView)? get pageViewBuilder =>
|
||||||
|
widget.pageViewBuilder;
|
||||||
|
TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
|
||||||
|
Widget? get tail => widget.tail;
|
||||||
|
Future<bool> Function()? get onWindowCloseButton =>
|
||||||
|
widget.onWindowCloseButton;
|
||||||
|
TabBuilder? get tabBuilder => widget.tabBuilder;
|
||||||
|
LabelGetter? get labelGetter => widget.labelGetter;
|
||||||
|
double? get maxLabelWidth => widget.maxLabelWidth;
|
||||||
|
Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
|
||||||
|
Color? get unSelectedTabBackgroundColor =>
|
||||||
|
widget.unSelectedTabBackgroundColor;
|
||||||
|
Color? get selectedBorderColor => widget.selectedBorderColor;
|
||||||
|
RxBool? get blockTab => widget.blockTab;
|
||||||
|
DesktopTabController get controller => widget.controller;
|
||||||
|
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
|
||||||
|
Debouncer get _scrollDebounce => widget._scrollDebounce;
|
||||||
|
|
||||||
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
|
DesktopTabType get tabType => controller.tabType;
|
||||||
|
bool get isMainWindow =>
|
||||||
|
tabType == DesktopTabType.main ||
|
||||||
|
tabType == DesktopTabType.cm ||
|
||||||
|
tabType == DesktopTabType.install;
|
||||||
|
|
||||||
|
_DesktopTabState() : super();
|
||||||
|
|
||||||
|
static RxString tablabelGetter(String peerId) {
|
||||||
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
|
return RxString(getDesktopTabLabel(peerId, alias));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -514,7 +339,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
|
|
||||||
Future.delayed(Duration(milliseconds: 500), () {
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
if (widget.isMainWindow) {
|
if (isMainWindow) {
|
||||||
windowManager.isMaximized().then((maximized) {
|
windowManager.isMaximized().then((maximized) {
|
||||||
if (stateGlobal.isMaximized.value != maximized) {
|
if (stateGlobal.isMaximized.value != maximized) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
@@ -580,7 +405,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_saveFrame() async {
|
_saveFrame() async {
|
||||||
if (widget.tabType == DesktopTabType.main) {
|
if (tabType == DesktopTabType.main) {
|
||||||
await saveWindowPosition(WindowType.Main);
|
await saveWindowPosition(WindowType.Main);
|
||||||
} else if (kWindowType != null && kWindowId != null) {
|
} else if (kWindowType != null && kWindowId != null) {
|
||||||
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
||||||
@@ -602,18 +427,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
@override
|
@override
|
||||||
void onWindowClose() async {
|
void onWindowClose() async {
|
||||||
mainWindowClose() async => await windowManager.hide();
|
mainWindowClose() async => await windowManager.hide();
|
||||||
notMainWindowClose(WindowController controller) async {
|
notMainWindowClose(WindowController windowController) async {
|
||||||
if (widget.tabController.length != 0) {
|
if (controller.length != 0) {
|
||||||
debugPrint("close not empty multiwindow from taskbar");
|
debugPrint("close not empty multiwindow from taskbar");
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
await controller.show();
|
await windowController.show();
|
||||||
await controller.focus();
|
await windowController.focus();
|
||||||
final res = await widget.onClose?.call() ?? true;
|
final res = await onWindowCloseButton?.call() ?? true;
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
}
|
}
|
||||||
widget.tabController.clear();
|
controller.clear();
|
||||||
}
|
}
|
||||||
await controller.hide();
|
await windowController.hide();
|
||||||
await rustDeskWinManager
|
await rustDeskWinManager
|
||||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
||||||
}
|
}
|
||||||
@@ -635,20 +460,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hide window on close
|
// hide window on close
|
||||||
if (widget.isMainWindow) {
|
if (isMainWindow) {
|
||||||
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
||||||
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
||||||
}
|
}
|
||||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||||
if (isMacOS && await windowManager.isFullScreen()) {
|
if (isMacOS && await windowManager.isFullScreen()) {
|
||||||
stateGlobal.closeOnFullscreen ??= true;
|
|
||||||
await windowManager.setFullScreen(false);
|
await windowManager.setFullScreen(false);
|
||||||
await macOSWindowClose(
|
await macOSWindowClose(
|
||||||
() async => await windowManager.isFullScreen(),
|
() async => await windowManager.isFullScreen(),
|
||||||
mainWindowClose,
|
mainWindowClose,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.closeOnFullscreen ??= false;
|
|
||||||
await mainWindowClose();
|
await mainWindowClose();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -658,9 +481,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
|
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
|
||||||
// use ??= to make sure the value is set on first call.
|
// use ??= to make sure the value is set on first call.
|
||||||
|
|
||||||
if (await widget.onClose?.call() ?? true) {
|
if (await onWindowCloseButton?.call() ?? true) {
|
||||||
if (await controller.isFullScreen()) {
|
if (await controller.isFullScreen()) {
|
||||||
stateGlobal.closeOnFullscreen ??= true;
|
|
||||||
await controller.setFullscreen(false);
|
await controller.setFullscreen(false);
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
await macOSWindowClose(
|
await macOSWindowClose(
|
||||||
@@ -668,7 +490,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
() async => await notMainWindowClose(controller),
|
() async => await notMainWindowClose(controller),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.closeOnFullscreen ??= false;
|
|
||||||
await notMainWindowClose(controller);
|
await notMainWindowClose(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,6 +500,243 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
super.onWindowClose();
|
super.onWindowClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(children: [
|
||||||
|
Obx(() {
|
||||||
|
if (stateGlobal.showTabBar.isTrue &&
|
||||||
|
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
||||||
|
return SizedBox(
|
||||||
|
height: _kTabBarHeight,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: _kTabBarHeight - 1,
|
||||||
|
child: _buildBar(),
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Expanded(
|
||||||
|
child: pageViewBuilder != null
|
||||||
|
? pageViewBuilder!(_buildPageView())
|
||||||
|
: _buildPageView())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBlock({required Widget child}) {
|
||||||
|
if (blockTab != null) {
|
||||||
|
return buildRemoteBlock(
|
||||||
|
child: child,
|
||||||
|
block: blockTab!,
|
||||||
|
use: canBeBlocked,
|
||||||
|
mask: tabType == DesktopTabType.main);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _tabWidgets = [];
|
||||||
|
Widget _buildPageView() {
|
||||||
|
final child = _buildBlock(
|
||||||
|
child: Obx(() => PageView(
|
||||||
|
controller: state.value.pageController,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
children: () {
|
||||||
|
/// to-do refactor, separate connection state and UI state for remote session.
|
||||||
|
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
||||||
|
final tabLen = state.value.tabs.length;
|
||||||
|
if (tabLen == _tabWidgets.length) {
|
||||||
|
return _tabWidgets;
|
||||||
|
} else if (_tabWidgets.isNotEmpty &&
|
||||||
|
tabLen == _tabWidgets.length + 1) {
|
||||||
|
/// On add. Use the previous list(pointer) to prevent item's state init twice.
|
||||||
|
/// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built.
|
||||||
|
_tabWidgets.add(state.value.tabs.last.page);
|
||||||
|
return _tabWidgets;
|
||||||
|
} else {
|
||||||
|
/// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal.
|
||||||
|
/// the Widgets in list must enable [AutomaticKeepAliveClientMixin]
|
||||||
|
final newList = state.value.tabs.map((v) => v.page).toList();
|
||||||
|
_tabWidgets = newList;
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
|
}())));
|
||||||
|
if (tabType == DesktopTabType.remoteScreen) {
|
||||||
|
return Container(color: kColorCanvas, child: child);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether to show ListView
|
||||||
|
///
|
||||||
|
/// Conditions:
|
||||||
|
/// - hide single item when only has one item (home) on [DesktopTabPage].
|
||||||
|
bool isHideSingleItem() {
|
||||||
|
return state.value.tabs.length == 1 &&
|
||||||
|
(controller.tabType == DesktopTabType.main ||
|
||||||
|
controller.tabType == DesktopTabType.install);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBar() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
// custom double tap handler
|
||||||
|
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
|
||||||
|
showMaximize
|
||||||
|
? () {
|
||||||
|
final current = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final elapsed = current - _lastClickTime;
|
||||||
|
_lastClickTime = current;
|
||||||
|
if (elapsed < bind.getDoubleClickTime()) {
|
||||||
|
// onDoubleTap
|
||||||
|
toggleMaximize(isMainWindow)
|
||||||
|
.then((value) => stateGlobal.setMaximized(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
onPanStart: (_) => startDragging(isMainWindow),
|
||||||
|
onPanCancel: () {
|
||||||
|
// We want to disable dragging of the tab area in the tab bar.
|
||||||
|
// Disable dragging is needed because macOS handles dragging by default.
|
||||||
|
if (isMacOS) {
|
||||||
|
setMovable(isMainWindow, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
if (isMacOS) {
|
||||||
|
setMovable(isMainWindow, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !isMacOS,
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 78,
|
||||||
|
)),
|
||||||
|
Offstage(
|
||||||
|
offstage: kUseCompatibleUiMode || isMacOS,
|
||||||
|
child: Row(children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !showLogo,
|
||||||
|
child: loadIcon(16),
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !showTitle,
|
||||||
|
child: const Text(
|
||||||
|
"RustDesk",
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
).marginOnly(left: 2))
|
||||||
|
]).marginOnly(
|
||||||
|
left: 5,
|
||||||
|
right: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Listener(
|
||||||
|
// handle mouse wheel
|
||||||
|
onPointerSignal: (e) {
|
||||||
|
if (e is PointerScrollEvent) {
|
||||||
|
final sc =
|
||||||
|
controller.state.value.scrollController;
|
||||||
|
if (!sc.canScroll) return;
|
||||||
|
_scrollDebounce.call(() {
|
||||||
|
sc.animateTo(sc.offset + e.scrollDelta.dy,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: _ListView(
|
||||||
|
controller: controller,
|
||||||
|
invisibleTabKeys: invisibleTabKeys,
|
||||||
|
tabBuilder: tabBuilder,
|
||||||
|
tabMenuBuilder: tabMenuBuilder,
|
||||||
|
labelGetter: labelGetter,
|
||||||
|
maxLabelWidth: maxLabelWidth,
|
||||||
|
selectedTabBackgroundColor:
|
||||||
|
selectedTabBackgroundColor,
|
||||||
|
unSelectedTabBackgroundColor:
|
||||||
|
unSelectedTabBackgroundColor,
|
||||||
|
selectedBorderColor: selectedBorderColor,
|
||||||
|
))),
|
||||||
|
],
|
||||||
|
))),
|
||||||
|
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
||||||
|
WindowActionPanel(
|
||||||
|
isMainWindow: isMainWindow,
|
||||||
|
state: state,
|
||||||
|
tabController: controller,
|
||||||
|
invisibleTabKeys: invisibleTabKeys,
|
||||||
|
tail: tail,
|
||||||
|
showMinimize: showMinimize,
|
||||||
|
showMaximize: showMaximize,
|
||||||
|
showClose: showClose,
|
||||||
|
onClose: onWindowCloseButton,
|
||||||
|
labelGetter: labelGetter,
|
||||||
|
).paddingOnly(left: 10)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowActionPanel extends StatefulWidget {
|
||||||
|
final bool isMainWindow;
|
||||||
|
final Rx<DesktopTabState> state;
|
||||||
|
final DesktopTabController tabController;
|
||||||
|
|
||||||
|
final bool showMinimize;
|
||||||
|
final bool showMaximize;
|
||||||
|
final bool showClose;
|
||||||
|
final Widget? tail;
|
||||||
|
final Future<bool> Function()? onClose;
|
||||||
|
|
||||||
|
final RxList<String> invisibleTabKeys;
|
||||||
|
final LabelGetter? labelGetter;
|
||||||
|
|
||||||
|
const WindowActionPanel(
|
||||||
|
{Key? key,
|
||||||
|
required this.isMainWindow,
|
||||||
|
required this.state,
|
||||||
|
required this.tabController,
|
||||||
|
required this.invisibleTabKeys,
|
||||||
|
this.tail,
|
||||||
|
this.showMinimize = true,
|
||||||
|
this.showMaximize = true,
|
||||||
|
this.showClose = true,
|
||||||
|
this.onClose,
|
||||||
|
this.labelGetter})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return WindowActionPanelState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowActionPanelState extends State<WindowActionPanel> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
bool showTabDowndown() {
|
bool showTabDowndown() {
|
||||||
return widget.tabController.state.value.tabs.length > 1 &&
|
return widget.tabController.state.value.tabs.length > 1 &&
|
||||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||||
@@ -699,72 +757,69 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Obx(() => Offstage(
|
Obx(() {
|
||||||
offstage:
|
if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
|
||||||
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
return _TabDropDownButton(
|
||||||
child: _TabDropDownButton(
|
controller: widget.tabController,
|
||||||
controller: widget.tabController,
|
labelGetter: widget.labelGetter,
|
||||||
labelGetter: widget.labelGetter,
|
tabkeys: existingInvisibleTab());
|
||||||
tabkeys: existingInvisibleTab()),
|
} else {
|
||||||
)),
|
return Offstage();
|
||||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
}
|
||||||
Offstage(
|
}),
|
||||||
offstage: kUseCompatibleUiMode,
|
if (widget.tail != null) widget.tail!,
|
||||||
child: Row(
|
if (!kUseCompatibleUiMode)
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
if (widget.showMinimize && !isMacOS)
|
||||||
offstage: !widget.showMinimize || isMacOS,
|
ActionIcon(
|
||||||
child: ActionIcon(
|
message: 'Minimize',
|
||||||
message: 'Minimize',
|
icon: IconFont.min,
|
||||||
icon: IconFont.min,
|
onTap: () {
|
||||||
onTap: () {
|
if (widget.isMainWindow) {
|
||||||
if (widget.isMainWindow) {
|
windowManager.minimize();
|
||||||
windowManager.minimize();
|
} else {
|
||||||
} else {
|
WindowController.fromWindowId(kWindowId!).minimize();
|
||||||
WindowController.fromWindowId(kWindowId!).minimize();
|
}
|
||||||
}
|
},
|
||||||
},
|
isClose: false,
|
||||||
isClose: false,
|
),
|
||||||
)),
|
if (widget.showMaximize && !isMacOS)
|
||||||
Offstage(
|
Obx(() => ActionIcon(
|
||||||
offstage: !widget.showMaximize || isMacOS,
|
message: stateGlobal.isMaximized.isTrue
|
||||||
child: Obx(() => ActionIcon(
|
? 'Restore'
|
||||||
message: stateGlobal.isMaximized.isTrue
|
: 'Maximize',
|
||||||
? 'Restore'
|
icon: stateGlobal.isMaximized.isTrue
|
||||||
: 'Maximize',
|
? IconFont.restore
|
||||||
icon: stateGlobal.isMaximized.isTrue
|
: IconFont.max,
|
||||||
? IconFont.restore
|
onTap: bind.isIncomingOnly() && isInHomePage()
|
||||||
: IconFont.max,
|
? null
|
||||||
onTap: bind.isIncomingOnly() && isInHomePage()
|
: _toggleMaximize,
|
||||||
? null
|
isClose: false,
|
||||||
: _toggleMaximize,
|
)),
|
||||||
isClose: false,
|
if (widget.showClose && !isMacOS)
|
||||||
))),
|
ActionIcon(
|
||||||
Offstage(
|
message: 'Close',
|
||||||
offstage: !widget.showClose || isMacOS,
|
icon: IconFont.close,
|
||||||
child: ActionIcon(
|
onTap: () async {
|
||||||
message: 'Close',
|
final res = await widget.onClose?.call() ?? true;
|
||||||
icon: IconFont.close,
|
if (res) {
|
||||||
onTap: () async {
|
// hide for all window
|
||||||
final res = await widget.onClose?.call() ?? true;
|
// note: the main window can be restored by tray icon
|
||||||
if (res) {
|
Future.delayed(Duration.zero, () async {
|
||||||
// hide for all window
|
if (widget.isMainWindow) {
|
||||||
// note: the main window can be restored by tray icon
|
await windowManager.close();
|
||||||
Future.delayed(Duration.zero, () async {
|
} else {
|
||||||
if (widget.isMainWindow) {
|
await WindowController.fromWindowId(kWindowId!)
|
||||||
await windowManager.close();
|
.close();
|
||||||
} else {
|
}
|
||||||
await WindowController.fromWindowId(kWindowId!)
|
});
|
||||||
.close();
|
}
|
||||||
}
|
},
|
||||||
});
|
isClose: true,
|
||||||
}
|
)
|
||||||
},
|
|
||||||
isClose: true,
|
|
||||||
))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -785,6 +840,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 true -> window will be maximize
|
||||||
/// return false -> window will be unmaximize
|
/// return false -> window will be unmaximize
|
||||||
Future<bool> toggleMaximize(bool isMainWindow) async {
|
Future<bool> toggleMaximize(bool isMainWindow) async {
|
||||||
@@ -812,9 +875,9 @@ Future<bool> closeConfirmDialog() async {
|
|||||||
var confirm = true;
|
var confirm = true;
|
||||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||||
submit() {
|
submit() {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
String value = bool2option(kOptionEnableConfirmClosingTabs, confirm);
|
||||||
String value = bool2option(opt, confirm);
|
bind.mainSetLocalOption(
|
||||||
bind.mainSetLocalOption(key: opt, value: value);
|
key: kOptionEnableConfirmClosingTabs, value: value);
|
||||||
close(true);
|
close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +983,7 @@ class _ListView extends StatelessWidget {
|
|||||||
final label = labelGetter == null
|
final label = labelGetter == null
|
||||||
? Rx<String>(tab.label)
|
? Rx<String>(tab.label)
|
||||||
: labelGetter!(tab.label);
|
: labelGetter!(tab.label);
|
||||||
return VisibilityDetector(
|
final child = VisibilityDetector(
|
||||||
key: ValueKey(tab.key),
|
key: ValueKey(tab.key),
|
||||||
onVisibilityChanged: onVisibilityChanged,
|
onVisibilityChanged: onVisibilityChanged,
|
||||||
child: _Tab(
|
child: _Tab(
|
||||||
@@ -953,6 +1016,10 @@ class _ListView extends StatelessWidget {
|
|||||||
selectedBorderColor: selectedBorderColor,
|
selectedBorderColor: selectedBorderColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return GestureDetector(
|
||||||
|
onPanStart: (e) {},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}).toList()));
|
}).toList()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1156,22 +1223,26 @@ class _CloseButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _kIconSize,
|
width: _kIconSize,
|
||||||
child: Offstage(
|
child: () {
|
||||||
offstage: !visible,
|
if (visible) {
|
||||||
child: InkWell(
|
return InkWell(
|
||||||
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
onTap: () => onClose(),
|
onTap: () => onClose(),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
size: _kIconSize,
|
size: _kIconSize,
|
||||||
color: tabSelected
|
color: tabSelected
|
||||||
? MyTheme.tabbar(context).selectedIconColor
|
? MyTheme.tabbar(context).selectedIconColor
|
||||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
)).paddingOnly(left: 10);
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
}())
|
||||||
|
.paddingOnly(left: 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1325,27 +1396,30 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
|||||||
child: InkWell(child: Text(label)),
|
child: InkWell(child: Text(label)),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => Offstage(
|
() {
|
||||||
offstage: !(tabInfo?.onTabCloseButton != null &&
|
if (tabInfo?.onTabCloseButton != null &&
|
||||||
menuHover.value),
|
menuHover.value) {
|
||||||
child: InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
tabInfo?.onTabCloseButton?.call();
|
tabInfo?.onTabCloseButton?.call();
|
||||||
if (Navigator.of(context).canPop()) {
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
onHover: (event) =>
|
onHover: (event) =>
|
||||||
setState(() => btnHover.value = true),
|
setState(() => btnHover.value = true),
|
||||||
onExit: (event) =>
|
onExit: (event) =>
|
||||||
setState(() => btnHover.value = false),
|
setState(() => btnHover.value = false),
|
||||||
child: Icon(Icons.close,
|
child: Icon(Icons.close,
|
||||||
color:
|
color:
|
||||||
btnHover.value ? Colors.red : null))),
|
btnHover.value ? Colors.red : null)));
|
||||||
),
|
} else {
|
||||||
)
|
return Offstage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart';
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/overlay.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/install_page.dart';
|
import 'package:flutter_hbb/desktop/pages/install_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
@@ -95,6 +96,9 @@ Future<void> main(List<String> args) async {
|
|||||||
desktopType = DesktopType.main;
|
desktopType = DesktopType.main;
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
windowManager.setPreventClose(true);
|
windowManager.setPreventClose(true);
|
||||||
|
if (isMacOS) {
|
||||||
|
disableWindowMovable(kWindowId);
|
||||||
|
}
|
||||||
runMainApp(true);
|
runMainApp(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +157,7 @@ void runMobileApp() async {
|
|||||||
await initEnv(kAppTypeMain);
|
await initEnv(kAppTypeMain);
|
||||||
if (isAndroid) androidChannelInit();
|
if (isAndroid) androidChannelInit();
|
||||||
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||||
|
draggablePositions.load();
|
||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
runApp(App());
|
runApp(App());
|
||||||
@@ -167,9 +172,13 @@ void runMultiWindow(
|
|||||||
final title = getWindowName();
|
final title = getWindowName();
|
||||||
// set prevent close to true, we handle close event manually
|
// set prevent close to true, we handle close event manually
|
||||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||||
|
if (isMacOS) {
|
||||||
|
disableWindowMovable(kWindowId);
|
||||||
|
}
|
||||||
late Widget widget;
|
late Widget widget;
|
||||||
switch (appType) {
|
switch (appType) {
|
||||||
case kAppTypeDesktopRemote:
|
case kAppTypeDesktopRemote:
|
||||||
|
draggablePositions.load();
|
||||||
widget = DesktopRemoteScreen(
|
widget = DesktopRemoteScreen(
|
||||||
params: argument,
|
params: argument,
|
||||||
);
|
);
|
||||||
@@ -338,7 +347,6 @@ void runInstallPage() async {
|
|||||||
windowManager.focus();
|
windowManager.focus();
|
||||||
windowManager.setOpacity(1);
|
windowManager.setOpacity(1);
|
||||||
windowManager.setAlignment(Alignment.center); // ensure
|
windowManager.setAlignment(Alignment.center); // ensure
|
||||||
windowManager.setTitle(getWindowName());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
Timer(const Duration(seconds: 1), () async {
|
if (!bind.isCustomClient()) {
|
||||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
Timer(const Duration(seconds: 1), () async {
|
||||||
if (_updateUrl.isNotEmpty) setState(() {});
|
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
});
|
if (_updateUrl.isNotEmpty) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_idController.addListener(() {
|
_idController.addListener(() {
|
||||||
@@ -393,7 +395,7 @@ class _WebMenuState extends State<WebMenu> {
|
|||||||
[
|
[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: "about",
|
value: "about",
|
||||||
child: Text('${translate('About')} RustDesk'),
|
child: Text(translate('About RustDesk')),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -54,6 +55,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
SessionID get sessionId => gFFI.sessionId;
|
SessionID get sessionId => gFFI.sessionId;
|
||||||
|
|
||||||
|
final TextEditingController _textController =
|
||||||
|
TextEditingController(text: initText);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -79,16 +83,19 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
initSharedStates(widget.id);
|
initSharedStates(widget.id);
|
||||||
gFFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
|
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
|
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay();
|
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
||||||
gFFI.inputModel.listenToMouse(false);
|
gFFI.inputModel.listenToMouse(false);
|
||||||
|
gFFI.imageModel.disposeImage();
|
||||||
|
gFFI.cursorModel.disposeImages();
|
||||||
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
_mobileFocusNode.dispose();
|
_mobileFocusNode.dispose();
|
||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
@@ -102,6 +109,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
await keyboardSubscription.cancel();
|
await keyboardSubscription.cancel();
|
||||||
removeSharedStates(widget.id);
|
removeSharedStates(widget.id);
|
||||||
|
// `on_voice_call_closed` should be called when the connection is ended.
|
||||||
|
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
|
||||||
|
// Only one client is considered here for now.
|
||||||
|
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||||
}
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
@@ -137,37 +148,59 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle mobile virtual keyboard
|
void _handleIOSSoftKeyboardInput(String newValue) {
|
||||||
void handleSoftKeyboardInput(String newValue) {
|
|
||||||
var oldValue = _value;
|
var oldValue = _value;
|
||||||
_value = newValue;
|
_value = newValue;
|
||||||
if (isIOS) {
|
var i = newValue.length - 1;
|
||||||
var i = newValue.length - 1;
|
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
var j = oldValue.length - 1;
|
||||||
var j = oldValue.length - 1;
|
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
if (i < j) j = i;
|
||||||
if (i < j) j = i;
|
var subNewValue = newValue.substring(j + 1);
|
||||||
newValue = newValue.substring(j + 1);
|
var subOldValue = oldValue.substring(j + 1);
|
||||||
oldValue = oldValue.substring(j + 1);
|
|
||||||
var common = 0;
|
// get common prefix of subNewValue and subOldValue
|
||||||
for (;
|
var common = 0;
|
||||||
common < oldValue.length &&
|
for (;
|
||||||
common < newValue.length &&
|
common < subOldValue.length &&
|
||||||
newValue[common] == oldValue[common];
|
common < subNewValue.length &&
|
||||||
++common) {}
|
subNewValue[common] == subOldValue[common];
|
||||||
for (i = 0; i < oldValue.length - common; ++i) {
|
++common) {}
|
||||||
inputModel.inputKey('VK_BACK');
|
|
||||||
}
|
// get newStr from subNewValue
|
||||||
if (newValue.length > common) {
|
var newStr = "";
|
||||||
var s = newValue.substring(common);
|
if (subNewValue.length > common) {
|
||||||
if (s.length > 1) {
|
newStr = subNewValue.substring(common);
|
||||||
bind.sessionInputString(sessionId: sessionId, value: s);
|
|
||||||
} else {
|
|
||||||
inputChar(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the value to the old value and early return if is still composing. (1 && 2)
|
||||||
|
// 1. The composing range is valid
|
||||||
|
// 2. The new string is shorter than the composing range.
|
||||||
|
if (_textController.value.isComposingRangeValid) {
|
||||||
|
final composingLength = _textController.value.composing.end -
|
||||||
|
_textController.value.composing.start;
|
||||||
|
if (composingLength > newStr.length) {
|
||||||
|
_value = oldValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the different part in the old value.
|
||||||
|
for (i = 0; i < subOldValue.length - common; ++i) {
|
||||||
|
inputModel.inputKey('VK_BACK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input the new string.
|
||||||
|
if (newStr.length > 1) {
|
||||||
|
bind.sessionInputString(sessionId: sessionId, value: newStr);
|
||||||
|
} else {
|
||||||
|
inputChar(newStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
||||||
|
var oldValue = _value;
|
||||||
|
_value = newValue;
|
||||||
if (oldValue.isNotEmpty &&
|
if (oldValue.isNotEmpty &&
|
||||||
newValue.isNotEmpty &&
|
newValue.isNotEmpty &&
|
||||||
oldValue[0] == '\1' &&
|
oldValue[0] == '\1' &&
|
||||||
@@ -206,6 +239,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle mobile virtual keyboard
|
||||||
|
void handleSoftKeyboardInput(String newValue) {
|
||||||
|
if (isIOS) {
|
||||||
|
_handleIOSSoftKeyboardInput(newValue);
|
||||||
|
} else {
|
||||||
|
_handleNonIOSSoftKeyboardInput(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void inputChar(String char) {
|
void inputChar(String char) {
|
||||||
if (char == '\n') {
|
if (char == '\n') {
|
||||||
char = 'VK_RETURN';
|
char = 'VK_RETURN';
|
||||||
@@ -219,6 +261,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
// destroy first, so that our _value trick can work
|
// destroy first, so that our _value trick can work
|
||||||
_value = initText;
|
_value = initText;
|
||||||
|
_textController.text = _value;
|
||||||
setState(() => _showEdit = false);
|
setState(() => _showEdit = false);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||||
@@ -234,12 +277,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
|
|
||||||
|
|
||||||
Widget _bottomWidget() => _showGestureHelp
|
Widget _bottomWidget() => _showGestureHelp
|
||||||
? getGestureHelp()
|
? getGestureHelp()
|
||||||
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
||||||
? getBottomAppBar(keyboard)
|
? getBottomAppBar()
|
||||||
: Offstage());
|
: Offstage());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -304,9 +345,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black,
|
color: kColorCanvas,
|
||||||
child: isWebDesktop
|
child: isWebDesktop
|
||||||
? getBodyForDesktopWithListener(keyboard)
|
? getBodyForDesktopWithListener()
|
||||||
: SafeArea(
|
: SafeArea(
|
||||||
child:
|
child:
|
||||||
OrientationBuilder(builder: (ctx, orientation) {
|
OrientationBuilder(builder: (ctx, orientation) {
|
||||||
@@ -338,9 +379,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget getRawPointerAndKeyBody(Widget child) {
|
Widget getRawPointerAndKeyBody(Widget child) {
|
||||||
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
return RawPointerMouseRegion(
|
return RawPointerMouseRegion(
|
||||||
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
||||||
inputModel: inputModel,
|
inputModel: inputModel,
|
||||||
// Disable RawKeyFocusScope before the connecting is established.
|
// Disable RawKeyFocusScope before the connecting is established.
|
||||||
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
||||||
@@ -353,7 +394,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBottomAppBar(bool keyboard) {
|
Widget getBottomAppBar() {
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
elevation: 10,
|
elevation: 10,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
@@ -369,9 +411,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
clientClose(sessionId, gFFI.dialogManager);
|
clientClose(sessionId, gFFI.dialogManager);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
] +
|
|
||||||
<Widget>[
|
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.tv),
|
icon: Icon(Icons.tv),
|
||||||
@@ -381,7 +421,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
(isWebDesktop
|
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
|
||||||
? []
|
? []
|
||||||
: gFFI.ffiModel.isPeerAndroid
|
: gFFI.ffiModel.isPeerAndroid
|
||||||
? [
|
? [
|
||||||
@@ -413,15 +453,21 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
(isWeb
|
(isWeb
|
||||||
? []
|
? []
|
||||||
: <Widget>[
|
: <Widget>[
|
||||||
IconButton(
|
futureBuilder(
|
||||||
color: Colors.white,
|
future: gFFI.invokeMethod(
|
||||||
icon: Icon(Icons.message),
|
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
|
||||||
onPressed: () {
|
hasData: (isSupportVoiceCall) => IconButton(
|
||||||
gFFI.chatModel.changeCurrentKey(MessageKey(
|
color: Colors.white,
|
||||||
widget.id, ChatModel.clientModeID));
|
icon: isAndroid && isSupportVoiceCall
|
||||||
gFFI.chatModel.toggleChatOverlay();
|
? SvgPicture.asset('assets/chat.svg',
|
||||||
},
|
colorFilter: ColorFilter.mode(
|
||||||
)
|
Colors.white, BlendMode.srcIn))
|
||||||
|
: Icon(Icons.message),
|
||||||
|
onPressed: () =>
|
||||||
|
isAndroid && isSupportVoiceCall
|
||||||
|
? showChatOptions(widget.id)
|
||||||
|
: onPressedTextChat(widget.id),
|
||||||
|
))
|
||||||
]) +
|
]) +
|
||||||
[
|
[
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -471,11 +517,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
: TextFormField(
|
: TextFormField(
|
||||||
textInputAction: TextInputAction.newline,
|
textInputAction: TextInputAction.newline,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
// Flutter 3.16.9 Android.
|
||||||
|
// `enableSuggestions` causes secure keyboard to be shown.
|
||||||
|
// https://github.com/flutter/flutter/issues/139143
|
||||||
|
// https://github.com/flutter/flutter/issues/146540
|
||||||
|
// enableSuggestions: false,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
focusNode: _mobileFocusNode,
|
focusNode: _mobileFocusNode,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
initialValue: _value,
|
controller: _textController,
|
||||||
// trick way to make backspace work always
|
// trick way to make backspace work always
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
onChanged: handleSoftKeyboardInput,
|
onChanged: handleSoftKeyboardInput,
|
||||||
@@ -483,48 +533,84 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint(widget.id));
|
||||||
}
|
}
|
||||||
return paints;
|
return paints;
|
||||||
}()));
|
}()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
Widget getBodyForDesktopWithListener() {
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
var paints = <Widget>[ImagePaint()];
|
var paints = <Widget>[ImagePaint()];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
final cursor = bind.sessionGetToggleOptionSync(
|
final cursor = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
if (keyboard || cursor) {
|
if (ffiModel.keyboard || cursor) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint(widget.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor, child: Stack(children: paints));
|
color: MyTheme.canvasColor, child: Stack(children: paints));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<TTextMenu> _getMobileActionMenus() {
|
||||||
|
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
|
||||||
|
!gFFI.ffiModel.keyboard) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
|
||||||
|
if (!enabled) return [];
|
||||||
|
return [
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Back')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileBack(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Home')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileHome(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Apps')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileApps(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Volume up')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Volume down')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Power')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobilePower(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void showActions(String id) async {
|
void showActions(String id) async {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final x = 120.0;
|
final x = 120.0;
|
||||||
final y = size.height;
|
final y = size.height;
|
||||||
|
final mobileActionMenus = _getMobileActionMenus();
|
||||||
final menus = toolbarControls(context, id, gFFI);
|
final menus = toolbarControls(context, id, gFFI);
|
||||||
getChild(TTextMenu menu) {
|
|
||||||
if (menu.trailingIcon != null) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
menu.child,
|
|
||||||
menu.trailingIcon!,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return menu.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final more = menus
|
final List<PopupMenuEntry<int>> more = [
|
||||||
.asMap()
|
...mobileActionMenus
|
||||||
.entries
|
.asMap()
|
||||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
.entries
|
||||||
.toList();
|
.map((e) =>
|
||||||
|
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||||
|
.toList(),
|
||||||
|
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
|
||||||
|
...menus
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PopupMenuItem<int>(
|
||||||
|
child: e.value.getChild(),
|
||||||
|
value: e.key + mobileActionMenus.length))
|
||||||
|
.toList(),
|
||||||
|
];
|
||||||
() async {
|
() async {
|
||||||
var index = await showMenu(
|
var index = await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -532,10 +618,86 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
items: more,
|
items: more,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
|
if (index != null) {
|
||||||
|
if (index < mobileActionMenus.length) {
|
||||||
|
mobileActionMenus[index].onPressed.call();
|
||||||
|
} else if (index < mobileActionMenus.length + more.length) {
|
||||||
|
menus[index - mobileActionMenus.length].onPressed.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
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: IgnorePointer(
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: null,
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
|
||||||
|
final menuItems = menus
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), 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) {
|
if (index != null && index < menus.length) {
|
||||||
menus[index].onPressed.call();
|
menus[index].onPressed.call();
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// aka changeTouchMode
|
/// aka changeTouchMode
|
||||||
@@ -550,7 +712,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.ffiModel.toggleTouchMode();
|
gFFI.ffiModel.toggleTouchMode();
|
||||||
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||||
bind.sessionPeerOption(
|
bind.sessionPeerOption(
|
||||||
sessionId: sessionId, name: "touch-mode", value: v);
|
sessionId: sessionId, name: kOptionTouchMode, value: v);
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,6 +751,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
var _fn = false;
|
var _fn = false;
|
||||||
var _pin = false;
|
var _pin = false;
|
||||||
final _keyboardVisibilityController = KeyboardVisibilityController();
|
final _keyboardVisibilityController = KeyboardVisibilityController();
|
||||||
|
final _key = GlobalKey();
|
||||||
|
|
||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
|
|
||||||
@@ -613,6 +776,24 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
onPressed: onPressed);
|
onPressed: onPressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRect() {
|
||||||
|
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
||||||
|
if (renderObject == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (renderObject is RenderBox) {
|
||||||
|
final size = renderObject.size;
|
||||||
|
Offset pos = renderObject.localToGlobal(Offset.zero);
|
||||||
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
|
||||||
|
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasModifierOn = inputModel.ctrl ||
|
final hasModifierOn = inputModel.ctrl ||
|
||||||
@@ -621,6 +802,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
inputModel.command;
|
inputModel.command;
|
||||||
|
|
||||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||||
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -731,7 +913,12 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
final space = size.width > 320 ? 4.0 : 2.0;
|
final space = size.width > 320 ? 4.0 : 2.0;
|
||||||
|
// 500 ms is long enough for this widget to be built!
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
|
_updateRect();
|
||||||
|
});
|
||||||
return Container(
|
return Container(
|
||||||
|
key: _key,
|
||||||
color: Color(0xAA000000),
|
color: Color(0xAA000000),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
|
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
|
||||||
@@ -762,26 +949,52 @@ class ImagePaint extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CursorPaint extends StatelessWidget {
|
class CursorPaint extends StatelessWidget {
|
||||||
|
late final String id;
|
||||||
|
CursorPaint(this.id);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<CursorModel>(context);
|
final m = Provider.of<CursorModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||||
var s = c.scale;
|
final s = c.scale;
|
||||||
double hotx = m.hotx;
|
double hotx = m.hotx;
|
||||||
double hoty = m.hoty;
|
double hoty = m.hoty;
|
||||||
if (m.image == null) {
|
var image = m.image;
|
||||||
|
if (image == null) {
|
||||||
if (preDefaultCursor.image != null) {
|
if (preDefaultCursor.image != null) {
|
||||||
|
image = preDefaultCursor.image;
|
||||||
hotx = preDefaultCursor.image!.width / 2;
|
hotx = preDefaultCursor.image!.width / 2;
|
||||||
hoty = preDefaultCursor.image!.height / 2;
|
hoty = preDefaultCursor.image!.height / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (preForbiddenCursor.image != null &&
|
||||||
|
!ffiModel.viewOnly &&
|
||||||
|
!ffiModel.keyboard &&
|
||||||
|
!ShowRemoteCursorState.find(id).value) {
|
||||||
|
image = preForbiddenCursor.image;
|
||||||
|
hotx = preForbiddenCursor.image!.width / 2;
|
||||||
|
hoty = preForbiddenCursor.image!.height / 2;
|
||||||
|
}
|
||||||
|
if (image == null) {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final minSize = 12.0;
|
||||||
|
double mins =
|
||||||
|
minSize / (image.width > image.height ? image.width : image.height);
|
||||||
|
double factor = 1.0;
|
||||||
|
if (s < mins) {
|
||||||
|
factor = s / mins;
|
||||||
|
}
|
||||||
|
final s2 = s < mins ? mins : s;
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: m.image ?? preDefaultCursor.image,
|
image: image,
|
||||||
x: m.x * s - hotx + c.x,
|
x: (m.x - hotx) * factor + c.x / s2,
|
||||||
y: m.y * s - hoty + c.y - adjust,
|
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
|
||||||
scale: 1),
|
scale: s2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,7 +1024,7 @@ void showOptions(
|
|||||||
border: Border.all(color: Theme.of(context).hintColor),
|
border: Border.all(color: Theme.of(context).hintColor),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
color: i == cur
|
color: i == cur
|
||||||
? Theme.of(context).toggleableActiveColor.withOpacity(0.6)
|
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||||
: null),
|
: null),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text((i + 1).toString(),
|
child: Text((i + 1).toString(),
|
||||||
@@ -859,22 +1072,40 @@ void showOptions(
|
|||||||
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
||||||
final radios = [
|
final radios = [
|
||||||
for (var e in viewStyleRadios)
|
for (var e in viewStyleRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
e.onChanged?.call(v);
|
e.child,
|
||||||
if (v != null) viewStyle.value = v;
|
e.value,
|
||||||
})),
|
viewStyle.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) viewStyle.value = v;
|
||||||
|
}
|
||||||
|
: null)),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
for (var e in imageQualityRadios)
|
for (var e in imageQualityRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
e.onChanged?.call(v);
|
e.child,
|
||||||
if (v != null) imageQuality.value = v;
|
e.value,
|
||||||
})),
|
imageQuality.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) imageQuality.value = v;
|
||||||
|
}
|
||||||
|
: null)),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
for (var e in codecRadios)
|
for (var e in codecRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
e.onChanged?.call(v);
|
e.child,
|
||||||
if (v != null) codec.value = v;
|
e.value,
|
||||||
})),
|
codec.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) codec.value = v;
|
||||||
|
}
|
||||||
|
: null)),
|
||||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||||
];
|
];
|
||||||
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||||
@@ -885,10 +1116,12 @@ void showOptions(
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
value: rxCursorToggleValues[e.key].value,
|
value: rxCursorToggleValues[e.key].value,
|
||||||
onChanged: (v) {
|
onChanged: e.value.onChanged != null
|
||||||
e.value.onChanged?.call(v);
|
? (v) {
|
||||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
e.value.onChanged?.call(v);
|
||||||
},
|
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -900,10 +1133,12 @@ void showOptions(
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
value: rxToggleValues[e.key].value,
|
value: rxToggleValues[e.key].value,
|
||||||
onChanged: (v) {
|
onChanged: e.value.onChanged != null
|
||||||
e.value.onChanged?.call(v);
|
? (v) {
|
||||||
if (v != null) rxToggleValues[e.key].value = v;
|
e.value.onChanged?.call(v);
|
||||||
},
|
if (v != null) rxToggleValues[e.key].value = v;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
final toggles = [
|
final toggles = [
|
||||||
@@ -923,14 +1158,110 @@ void showOptions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var popupDialogMenus = List<Widget>.empty(growable: true);
|
||||||
|
final resolution = getResolutionMenu(gFFI, id);
|
||||||
|
if (resolution != null) {
|
||||||
|
popupDialogMenus.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: resolution.child,
|
||||||
|
onTap: () {
|
||||||
|
close();
|
||||||
|
resolution.onPressed();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
|
||||||
|
if (virtualDisplayMenu != null) {
|
||||||
|
popupDialogMenus.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: virtualDisplayMenu.child,
|
||||||
|
onTap: () {
|
||||||
|
close();
|
||||||
|
virtualDisplayMenu.onPressed();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (popupDialogMenus.isNotEmpty) {
|
||||||
|
popupDialogMenus.add(const Divider(color: MyTheme.border));
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: displays + radios + toggles + [privacyModeWidget]),
|
children: displays +
|
||||||
|
radios +
|
||||||
|
popupDialogMenus +
|
||||||
|
toggles +
|
||||||
|
[privacyModeWidget]),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||||
|
if (!showVirtualDisplayMenu(ffi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TTextMenu(
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
|
onPressed: () {
|
||||||
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
|
final children = getVirtualDisplayMenuChildren(ffi, id, close);
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Virtual display')),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
||||||
|
final ffiModel = ffi.ffiModel;
|
||||||
|
final pi = ffiModel.pi;
|
||||||
|
final resolutions = pi.resolutions;
|
||||||
|
final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
|
||||||
|
|
||||||
|
final visible =
|
||||||
|
ffiModel.keyboard && (resolutions.length > 1) && display != null;
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return TTextMenu(
|
||||||
|
child: Text(translate("Resolution")),
|
||||||
|
onPressed: () {
|
||||||
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
|
final children = resolutions
|
||||||
|
.map((e) => getRadio<String>(
|
||||||
|
Text('${e.width}x${e.height}'),
|
||||||
|
'${e.width}x${e.height}',
|
||||||
|
'${display.width}x${display.height}',
|
||||||
|
(value) {
|
||||||
|
close();
|
||||||
|
bind.sessionChangeResolution(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
display: pi.currentDisplay,
|
||||||
|
width: e.width,
|
||||||
|
height: e.height,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Resolution')),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void sendPrompt(bool isMac, String key) {
|
void sendPrompt(bool isMac, String key) {
|
||||||
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
final icon = const Icon(Icons.mobile_screen_share);
|
final icon = const Icon(Icons.mobile_screen_share);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final appBarActions = [
|
final appBarActions = (!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||||
|
? [_DropDownAction()]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
ServerPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ServerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DropDownAction extends StatelessWidget {
|
||||||
|
_DropDownAction();
|
||||||
|
|
||||||
|
// should only have one action
|
||||||
|
final actions = [
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
tooltip: "",
|
tooltip: "",
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
@@ -39,6 +55,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
final approveMode = gFFI.serverModel.approveMode;
|
final approveMode = gFFI.serverModel.approveMode;
|
||||||
final verificationMethod = gFFI.serverModel.verificationMethod;
|
final verificationMethod = gFFI.serverModel.verificationMethod;
|
||||||
final showPasswordOption = approveMode != 'click';
|
final showPasswordOption = approveMode != 'click';
|
||||||
|
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: gFFI.serverModel.connectStatus > 0,
|
enabled: gFFI.serverModel.connectStatus > 0,
|
||||||
@@ -50,16 +67,19 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
value: 'AcceptSessionsViaPassword',
|
value: 'AcceptSessionsViaPassword',
|
||||||
child: listTile(
|
child: listTile(
|
||||||
'Accept sessions via password', approveMode == 'password'),
|
'Accept sessions via password', approveMode == 'password'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'AcceptSessionsViaClick',
|
value: 'AcceptSessionsViaClick',
|
||||||
child:
|
child:
|
||||||
listTile('Accept sessions via click', approveMode == 'click'),
|
listTile('Accept sessions via click', approveMode == 'click'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: "AcceptSessionsViaBoth",
|
value: "AcceptSessionsViaBoth",
|
||||||
child: listTile("Accept sessions via both",
|
child: listTile("Accept sessions via both",
|
||||||
approveMode != 'password' && approveMode != 'click'),
|
approveMode != 'password' && approveMode != 'click'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
if (showPasswordOption) const PopupMenuDivider(),
|
if (showPasswordOption) const PopupMenuDivider(),
|
||||||
if (showPasswordOption &&
|
if (showPasswordOption &&
|
||||||
@@ -97,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onSelected: (value) {
|
onSelected: (value) async {
|
||||||
if (value == "changeID") {
|
if (value == "changeID") {
|
||||||
changeIdDialog();
|
changeIdDialog();
|
||||||
} else if (value == "setPermanentPassword") {
|
} else if (value == "setPermanentPassword") {
|
||||||
setPermanentPasswordDialog(gFFI.dialogManager);
|
setPasswordDialog();
|
||||||
} else if (value == "setTemporaryPasswordLength") {
|
} else if (value == "setTemporaryPasswordLength") {
|
||||||
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
||||||
} else if (value == kUsePermanentPassword ||
|
} else if (value == kUsePermanentPassword ||
|
||||||
value == kUseTemporaryPassword ||
|
value == kUseTemporaryPassword ||
|
||||||
value == kUseBothPasswords) {
|
value == kUseBothPasswords) {
|
||||||
bind.mainSetOption(key: "verification-method", value: value);
|
callback() {
|
||||||
gFFI.serverModel.updatePasswordModel();
|
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||||
|
gFFI.serverModel.updatePasswordModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == kUsePermanentPassword &&
|
||||||
|
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||||
|
setPasswordDialog(notEmptyCallback: callback);
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||||
value = value.substring("AcceptSessionsVia".length);
|
value = value.substring("AcceptSessionsVia".length);
|
||||||
if (value == "Password") {
|
if (value == "Password") {
|
||||||
@@ -116,16 +145,16 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
} else if (value == "Click") {
|
} else if (value == "Click") {
|
||||||
gFFI.serverModel.setApproveMode('click');
|
gFFI.serverModel.setApproveMode('click');
|
||||||
} else {
|
} else {
|
||||||
gFFI.serverModel.setApproveMode('');
|
gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
ServerPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _ServerPageState();
|
Widget build(BuildContext context) {
|
||||||
|
return actions[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage> {
|
class _ServerPageState extends State<ServerPage> {
|
||||||
@@ -637,40 +666,94 @@ class ConnectionManager extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
).marginOnly(bottom: 5),
|
).marginOnly(bottom: 5),
|
||||||
client.authorized
|
client.authorized
|
||||||
? Container(
|
? _buildDisconnectButton(client)
|
||||||
alignment: Alignment.centerRight,
|
: _buildNewConnectionHint(serverModel, client),
|
||||||
child: ElevatedButton.icon(
|
if (client.incomingVoiceCall && !client.inVoiceCall)
|
||||||
style: ButtonStyle(
|
..._buildNewVoiceCallHint(context, serverModel, client),
|
||||||
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);
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
])))
|
])))
|
||||||
.toList());
|
.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 {
|
class PaddingCard extends StatelessWidget {
|
||||||
@@ -787,6 +870,24 @@ void androidChannelInit() {
|
|||||||
gFFI.serverModel.stopService();
|
gFFI.serverModel.stopService();
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
case "stop_service":
|
||||||
|
{
|
||||||
|
print(
|
||||||
|
"stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
|
||||||
|
if (gFFI.serverModel.isStart) {
|
||||||
|
gFFI.serverModel.stopService();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: "MethodCallHandler err:$e");
|
debugPrintStack(label: "MethodCallHandler err:$e");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
@@ -35,15 +36,47 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
|||||||
|
|
||||||
const url = 'https://rustdesk.com/';
|
const url = 'https://rustdesk.com/';
|
||||||
|
|
||||||
|
enum KeepScreenOn {
|
||||||
|
never,
|
||||||
|
duringControlled,
|
||||||
|
serviceOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
String _keepScreenOnToOption(KeepScreenOn value) {
|
||||||
|
switch (value) {
|
||||||
|
case KeepScreenOn.never:
|
||||||
|
return 'never';
|
||||||
|
case KeepScreenOn.duringControlled:
|
||||||
|
return 'during-controlled';
|
||||||
|
case KeepScreenOn.serviceOn:
|
||||||
|
return 'service-on';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeepScreenOn optionToKeepScreenOn(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'never':
|
||||||
|
return KeepScreenOn.never;
|
||||||
|
case 'service-on':
|
||||||
|
return KeepScreenOn.serviceOn;
|
||||||
|
default:
|
||||||
|
return KeepScreenOn.duringControlled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||||
final _hasIgnoreBattery = androidVersion >= 26;
|
final _hasIgnoreBattery =
|
||||||
|
false; //androidVersion >= 26; // remove because not work on every device
|
||||||
var _ignoreBatteryOpt = false;
|
var _ignoreBatteryOpt = false;
|
||||||
var _enableStartOnBoot = false;
|
var _enableStartOnBoot = false;
|
||||||
|
var _floatingWindowDisabled = false;
|
||||||
|
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
||||||
var _enableAbr = false;
|
var _enableAbr = false;
|
||||||
var _denyLANDiscovery = false;
|
var _denyLANDiscovery = false;
|
||||||
var _onlyWhiteList = false;
|
var _onlyWhiteList = false;
|
||||||
var _enableDirectIPAccess = false;
|
var _enableDirectIPAccess = false;
|
||||||
var _enableRecordSession = false;
|
var _enableRecordSession = false;
|
||||||
|
var _enableHardwareCodec = false;
|
||||||
var _autoRecordIncomingSession = false;
|
var _autoRecordIncomingSession = false;
|
||||||
var _allowAutoDisconnect = false;
|
var _allowAutoDisconnect = false;
|
||||||
var _localIP = "";
|
var _localIP = "";
|
||||||
@@ -51,12 +84,40 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _fingerprint = "";
|
var _fingerprint = "";
|
||||||
var _buildDate = "";
|
var _buildDate = "";
|
||||||
var _autoDisconnectTimeout = "";
|
var _autoDisconnectTimeout = "";
|
||||||
|
var _hideServer = false;
|
||||||
|
var _hideProxy = false;
|
||||||
|
var _hideNetwork = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
_enableAbr = option2bool(
|
||||||
|
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||||
|
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
||||||
|
_onlyWhiteList = whitelistNotEmpty();
|
||||||
|
_enableDirectIPAccess = option2bool(
|
||||||
|
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
||||||
|
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
|
||||||
|
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||||
|
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||||
|
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||||
|
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||||
|
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||||
|
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
|
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
||||||
|
_autoDisconnectTimeout =
|
||||||
|
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
||||||
|
_hideServer =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||||
|
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
|
_hideNetwork =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
var update = false;
|
var update = false;
|
||||||
|
|
||||||
@@ -85,60 +146,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_enableStartOnBoot = enableStartOnBoot;
|
_enableStartOnBoot = enableStartOnBoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableAbrRes = option2bool(
|
var floatingWindowDisabled =
|
||||||
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
if (enableAbrRes != _enableAbr) {
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
|
if (floatingWindowDisabled != _floatingWindowDisabled) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableAbr = enableAbrRes;
|
_floatingWindowDisabled = floatingWindowDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
final denyLanDiscovery = !option2bool('enable-lan-discovery',
|
final keepScreenOn = _floatingWindowDisabled
|
||||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
? KeepScreenOn.never
|
||||||
if (denyLanDiscovery != _denyLANDiscovery) {
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
||||||
|
if (keepScreenOn != _keepScreenOn) {
|
||||||
update = true;
|
update = true;
|
||||||
_denyLANDiscovery = denyLanDiscovery;
|
_keepScreenOn = keepScreenOn;
|
||||||
}
|
|
||||||
|
|
||||||
final onlyWhiteList =
|
|
||||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
|
||||||
update = true;
|
|
||||||
_onlyWhiteList = onlyWhiteList;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableDirectIPAccess = option2bool(
|
|
||||||
'direct-server', await bind.mainGetOption(key: 'direct-server'));
|
|
||||||
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
|
||||||
update = true;
|
|
||||||
_enableDirectIPAccess = enableDirectIPAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableRecordSession = option2bool('enable-record-session',
|
|
||||||
await bind.mainGetOption(key: 'enable-record-session'));
|
|
||||||
if (enableRecordSession != _enableRecordSession) {
|
|
||||||
update = true;
|
|
||||||
_enableRecordSession = enableRecordSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
final autoRecordIncomingSession = option2bool(
|
|
||||||
'allow-auto-record-incoming',
|
|
||||||
await bind.mainGetOption(key: 'allow-auto-record-incoming'));
|
|
||||||
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
|
||||||
update = true;
|
|
||||||
_autoRecordIncomingSession = autoRecordIncomingSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
final localIP = await bind.mainGetOption(key: 'local-ip-addr');
|
|
||||||
if (localIP != _localIP) {
|
|
||||||
update = true;
|
|
||||||
_localIP = localIP;
|
|
||||||
}
|
|
||||||
|
|
||||||
final directAccessPort =
|
|
||||||
await bind.mainGetOption(key: 'direct-access-port');
|
|
||||||
if (directAccessPort != _directAccessPort) {
|
|
||||||
update = true;
|
|
||||||
_directAccessPort = directAccessPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final fingerprint = await bind.mainGetFingerprint();
|
final fingerprint = await bind.mainGetFingerprint();
|
||||||
@@ -152,21 +174,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
update = true;
|
update = true;
|
||||||
_buildDate = buildDate;
|
_buildDate = buildDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
|
|
||||||
await bind.mainGetOption(key: 'allow-auto-disconnect'));
|
|
||||||
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
|
||||||
update = true;
|
|
||||||
_allowAutoDisconnect = allowAutoDisconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
final autoDisconnectTimeout =
|
|
||||||
await bind.mainGetOption(key: 'auto-disconnect-timeout');
|
|
||||||
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
|
||||||
update = true;
|
|
||||||
_autoDisconnectTimeout = autoDisconnectTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
@@ -249,16 +256,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Deny LAN discovery')),
|
title: Text(translate('Deny LAN discovery')),
|
||||||
initialValue: _denyLANDiscovery,
|
initialValue: _denyLANDiscovery,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableLanDiscovery)
|
||||||
await bind.mainSetOption(
|
? null
|
||||||
key: "enable-lan-discovery",
|
: (v) async {
|
||||||
value: bool2option("enable-lan-discovery", !v));
|
await bind.mainSetOption(
|
||||||
final newValue = !option2bool('enable-lan-discovery',
|
key: kOptionEnableLanDiscovery,
|
||||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
value: bool2option(kOptionEnableLanDiscovery, !v));
|
||||||
setState(() {
|
final newValue = !option2bool(kOptionEnableLanDiscovery,
|
||||||
_denyLANDiscovery = newValue;
|
await bind.mainGetOption(key: kOptionEnableLanDiscovery));
|
||||||
});
|
setState(() {
|
||||||
},
|
_denyLANDiscovery = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
@@ -272,8 +281,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
initialValue: _onlyWhiteList,
|
initialValue: _onlyWhiteList,
|
||||||
onToggle: (_) async {
|
onToggle: (_) async {
|
||||||
update() async {
|
update() async {
|
||||||
final onlyWhiteList =
|
final onlyWhiteList = whitelistNotEmpty();
|
||||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
@@ -287,26 +295,29 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
||||||
initialValue: _enableAbr,
|
initialValue: _enableAbr,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||||
await bind.mainSetOption(key: "enable-abr", value: v ? "" : "N");
|
? null
|
||||||
final newValue = await bind.mainGetOption(key: "enable-abr") != "N";
|
: (v) async {
|
||||||
setState(() {
|
await mainSetBoolOption(kOptionEnableAbr, v);
|
||||||
_enableAbr = newValue;
|
final newValue = await mainGetBoolOption(kOptionEnableAbr);
|
||||||
});
|
setState(() {
|
||||||
},
|
_enableAbr = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Enable recording session')),
|
title: Text(translate('Enable recording session')),
|
||||||
initialValue: _enableRecordSession,
|
initialValue: _enableRecordSession,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
||||||
await bind.mainSetOption(
|
? null
|
||||||
key: "enable-record-session", value: v ? "" : "N");
|
: (v) async {
|
||||||
final newValue =
|
await mainSetBoolOption(kOptionEnableRecordSession, v);
|
||||||
await bind.mainGetOption(key: "enable-record-session") != "N";
|
final newValue =
|
||||||
setState(() {
|
await mainGetBoolOption(kOptionEnableRecordSession);
|
||||||
_enableRecordSession = newValue;
|
setState(() {
|
||||||
});
|
_enableRecordSession = newValue;
|
||||||
},
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -333,21 +344,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionDirectAccessPort)
|
||||||
final port = await changeDirectAccessPort(
|
? null
|
||||||
_localIP, _directAccessPort);
|
: () async {
|
||||||
setState(() {
|
final port = await changeDirectAccessPort(
|
||||||
_directAccessPort = port;
|
_localIP, _directAccessPort);
|
||||||
});
|
setState(() {
|
||||||
}))
|
_directAccessPort = port;
|
||||||
|
});
|
||||||
|
}))
|
||||||
]),
|
]),
|
||||||
initialValue: _enableDirectIPAccess,
|
initialValue: _enableDirectIPAccess,
|
||||||
onToggle: (_) async {
|
onToggle: isOptionFixed(kOptionDirectServer)
|
||||||
_enableDirectIPAccess = !_enableDirectIPAccess;
|
? null
|
||||||
String value = bool2option('direct-server', _enableDirectIPAccess);
|
: (_) async {
|
||||||
await bind.mainSetOption(key: 'direct-server', value: value);
|
_enableDirectIPAccess = !_enableDirectIPAccess;
|
||||||
setState(() {});
|
String value =
|
||||||
},
|
bool2option(kOptionDirectServer, _enableDirectIPAccess);
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionDirectServer, value: value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -374,22 +391,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
|
||||||
final timeout = await changeAutoDisconnectTimeout(
|
? null
|
||||||
_autoDisconnectTimeout);
|
: () async {
|
||||||
setState(() {
|
final timeout = await changeAutoDisconnectTimeout(
|
||||||
_autoDisconnectTimeout = timeout;
|
_autoDisconnectTimeout);
|
||||||
});
|
setState(() {
|
||||||
}))
|
_autoDisconnectTimeout = timeout;
|
||||||
|
});
|
||||||
|
}))
|
||||||
]),
|
]),
|
||||||
initialValue: _allowAutoDisconnect,
|
initialValue: _allowAutoDisconnect,
|
||||||
onToggle: (_) async {
|
onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
|
||||||
_allowAutoDisconnect = !_allowAutoDisconnect;
|
? null
|
||||||
String value =
|
: (_) async {
|
||||||
bool2option('allow-auto-disconnect', _allowAutoDisconnect);
|
_allowAutoDisconnect = !_allowAutoDisconnect;
|
||||||
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value);
|
String value = bool2option(
|
||||||
setState(() {});
|
kOptionAllowAutoDisconnect, _allowAutoDisconnect);
|
||||||
},
|
await bind.mainSetOption(
|
||||||
|
key: kOptionAllowAutoDisconnect, value: value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
if (_hasIgnoreBattery) {
|
if (_hasIgnoreBattery) {
|
||||||
@@ -463,7 +485,59 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
onFloatingWindowChanged(bool toValue) async {
|
||||||
|
if (toValue) {
|
||||||
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
|
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final disable = !toValue;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionDisableFloatingWindow,
|
||||||
|
value: disable ? 'Y' : defaultOptionNo);
|
||||||
|
setState(() => _floatingWindowDisabled = disable);
|
||||||
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancementsTiles.add(SettingsTile.switchTile(
|
||||||
|
initialValue: !_floatingWindowDisabled,
|
||||||
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(translate('Floating window')),
|
||||||
|
Text('* ${translate('floating_window_tip')}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
]),
|
||||||
|
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
|
||||||
|
? null
|
||||||
|
: onFloatingWindowChanged));
|
||||||
|
|
||||||
|
enhancementsTiles.add(_getPopupDialogRadioEntry(
|
||||||
|
title: 'Keep screen on',
|
||||||
|
list: [
|
||||||
|
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
|
||||||
|
_RadioEntry('During controlled',
|
||||||
|
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
|
||||||
|
_RadioEntry('During service is on',
|
||||||
|
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
|
||||||
|
],
|
||||||
|
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
|
||||||
|
? KeepScreenOn.never
|
||||||
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
|
||||||
|
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionKeepScreenOn, value: value);
|
||||||
|
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
|
||||||
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
final disabledSettings = bind.isDisableSettings();
|
final disabledSettings = bind.isDisableSettings();
|
||||||
|
final hideSecuritySettings =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
|
||||||
final settings = SettingsList(
|
final settings = SettingsList(
|
||||||
sections: [
|
sections: [
|
||||||
customClientSection,
|
customClientSection,
|
||||||
@@ -487,13 +561,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||||
if (!disabledSettings)
|
if (!disabledSettings && !_hideNetwork && !_hideServer)
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('ID/Relay Server')),
|
title: Text(translate('ID/Relay Server')),
|
||||||
leading: Icon(Icons.cloud),
|
leading: Icon(Icons.cloud),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showServerSettings(gFFI.dialogManager);
|
showServerSettings(gFFI.dialogManager);
|
||||||
}),
|
}),
|
||||||
|
if (!isIOS && !_hideNetwork && !_hideProxy)
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||||
|
leading: Icon(Icons.network_ping),
|
||||||
|
onPressed: (context) {
|
||||||
|
changeSocks5Proxy();
|
||||||
|
}),
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('Language')),
|
title: Text(translate('Language')),
|
||||||
leading: Icon(Icons.translate),
|
leading: Icon(Icons.translate),
|
||||||
@@ -503,8 +584,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate(
|
title: Text(translate(
|
||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).brightness == Brightness.light
|
||||||
? 'Dark Theme'
|
? 'Light Theme'
|
||||||
: 'Light Theme')),
|
: 'Dark Theme')),
|
||||||
leading: Icon(Theme.of(context).brightness == Brightness.light
|
leading: Icon(Theme.of(context).brightness == Brightness.light
|
||||||
? Icons.dark_mode
|
? Icons.dark_mode
|
||||||
: Icons.light_mode),
|
: Icons.light_mode),
|
||||||
@@ -513,6 +594,23 @@ 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 mainSetBoolOption(kOptionEnableHwcodec, v);
|
||||||
|
final newValue =
|
||||||
|
await mainGetBoolOption(kOptionEnableHwcodec);
|
||||||
|
setState(() {
|
||||||
|
_enableHardwareCodec = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
if (isAndroid && !outgoingOnly)
|
if (isAndroid && !outgoingOnly)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Recording")),
|
title: Text(translate("Recording")),
|
||||||
@@ -521,34 +619,40 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
title:
|
title:
|
||||||
Text(translate('Automatically record incoming sessions')),
|
Text(translate('Automatically record incoming sessions')),
|
||||||
leading: Icon(Icons.videocam),
|
leading: Icon(Icons.videocam),
|
||||||
description: FutureBuilder(
|
description: Text(
|
||||||
builder: (ctx, data) => Offstage(
|
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
||||||
offstage: !data.hasData,
|
|
||||||
child: Text("${translate("Directory")}: ${data.data}")),
|
|
||||||
future: bind.mainVideoSaveDirectory(root: false)),
|
|
||||||
initialValue: _autoRecordIncomingSession,
|
initialValue: _autoRecordIncomingSession,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
await bind.mainSetOption(
|
? null
|
||||||
key: "allow-auto-record-incoming",
|
: (v) async {
|
||||||
value: bool2option("allow-auto-record-incoming", v));
|
await bind.mainSetOption(
|
||||||
final newValue = option2bool(
|
key: kOptionAllowAutoRecordIncoming,
|
||||||
'allow-auto-record-incoming',
|
value:
|
||||||
await bind.mainGetOption(
|
bool2option(kOptionAllowAutoRecordIncoming, v));
|
||||||
key: 'allow-auto-record-incoming'));
|
final newValue = option2bool(
|
||||||
setState(() {
|
kOptionAllowAutoRecordIncoming,
|
||||||
_autoRecordIncomingSession = newValue;
|
await bind.mainGetOption(
|
||||||
});
|
key: kOptionAllowAutoRecordIncoming));
|
||||||
},
|
setState(() {
|
||||||
|
_autoRecordIncomingSession = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Share Screen")),
|
title: Text(translate("Share Screen")),
|
||||||
tiles: shareScreenTiles,
|
tiles: shareScreenTiles,
|
||||||
),
|
),
|
||||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Enhancements")),
|
title: Text(translate("Enhancements")),
|
||||||
tiles: enhancementsTiles,
|
tiles: enhancementsTiles,
|
||||||
@@ -637,29 +741,32 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
|
|||||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||||
try {
|
try {
|
||||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
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) {
|
dialogManager.show((setState, close, context) {
|
||||||
setLang(v) async {
|
setLang(v) async {
|
||||||
if (lang != v) {
|
if (lang != v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
lang = v;
|
lang = v;
|
||||||
});
|
});
|
||||||
await bind.mainSetLocalOption(key: "lang", value: v);
|
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
|
||||||
HomePage.homeKey.currentState?.refreshPages();
|
HomePage.homeKey.currentState?.refreshPages();
|
||||||
Future.delayed(Duration(milliseconds: 200), close);
|
Future.delayed(Duration(milliseconds: 200), close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
getRadio(Text(translate('Default')), '', lang, setLang),
|
getRadio(Text(translate('Default')), defaultOptionLang, lang,
|
||||||
|
isOptFixed ? null : setLang),
|
||||||
Divider(color: MyTheme.border),
|
Divider(color: MyTheme.border),
|
||||||
] +
|
] +
|
||||||
langs.map((e) {
|
langs.map((e) {
|
||||||
final key = e[0] as String;
|
final key = e[0] as String;
|
||||||
final name = e[1] 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(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -683,13 +790,15 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
getRadio(
|
getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
|
||||||
Text(translate('Light')), ThemeMode.light, themeMode, setTheme),
|
isOptFixed ? null : setTheme),
|
||||||
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme),
|
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
|
||||||
|
isOptFixed ? null : setTheme),
|
||||||
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
||||||
setTheme)
|
isOptFixed ? null : setTheme)
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
@@ -698,7 +807,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
void showAbout(OverlayDialogManager dialogManager) {
|
void showAbout(OverlayDialogManager dialogManager) {
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text('${translate('About')} RustDesk'),
|
title: Text(translate('About RustDesk')),
|
||||||
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
||||||
Text('Version: $version'),
|
Text('Version: $version'),
|
||||||
InkWell(
|
InkWell(
|
||||||
@@ -777,11 +886,14 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
||||||
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
||||||
],
|
],
|
||||||
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
|
getter: () =>
|
||||||
asyncSetter: (value) async {
|
bind.mainGetUserDefaultOption(key: kOptionViewStyle),
|
||||||
await bind.mainSetUserDefaultOption(
|
asyncSetter: isOptionFixed(kOptionViewStyle)
|
||||||
key: 'view_style', value: value);
|
? null
|
||||||
},
|
: (value) async {
|
||||||
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionViewStyle, value: value);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_getPopupDialogRadioEntry(
|
_getPopupDialogRadioEntry(
|
||||||
title: 'Default Image Quality',
|
title: 'Default Image Quality',
|
||||||
@@ -792,16 +904,19 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
||||||
],
|
],
|
||||||
getter: () {
|
getter: () {
|
||||||
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
|
final v =
|
||||||
|
bind.mainGetUserDefaultOption(key: kOptionImageQuality);
|
||||||
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
||||||
return v;
|
return v;
|
||||||
},
|
},
|
||||||
asyncSetter: (value) async {
|
asyncSetter: isOptionFixed(kOptionImageQuality)
|
||||||
await bind.mainSetUserDefaultOption(
|
? null
|
||||||
key: 'image_quality', value: value);
|
: (value) async {
|
||||||
showCustomImageQuality.value =
|
await bind.mainSetUserDefaultOption(
|
||||||
value == kRemoteImageQualityCustom;
|
key: kOptionImageQuality, value: value);
|
||||||
},
|
showCustomImageQuality.value =
|
||||||
|
value == kRemoteImageQualityCustom;
|
||||||
|
},
|
||||||
tail: customImageQualitySetting(),
|
tail: customImageQualitySetting(),
|
||||||
showTail: showCustomImageQuality,
|
showTail: showCustomImageQuality,
|
||||||
notCloseValue: kRemoteImageQualityCustom,
|
notCloseValue: kRemoteImageQualityCustom,
|
||||||
@@ -810,11 +925,13 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
title: 'Default Codec',
|
title: 'Default Codec',
|
||||||
list: codecList,
|
list: codecList,
|
||||||
getter: () =>
|
getter: () =>
|
||||||
bind.mainGetUserDefaultOption(key: 'codec-preference'),
|
bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
|
||||||
asyncSetter: (value) async {
|
asyncSetter: isOptionFixed(kOptionCodecPreference)
|
||||||
await bind.mainSetUserDefaultOption(
|
? null
|
||||||
key: 'codec-preference', value: value);
|
: (value) async {
|
||||||
},
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionCodecPreference, value: value);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -829,13 +946,17 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
|
|
||||||
SettingsTile otherRow(String label, String key) {
|
SettingsTile otherRow(String label, String key) {
|
||||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||||
|
final isOptFixed = isOptionFixed(key);
|
||||||
return SettingsTile.switchTile(
|
return SettingsTile.switchTile(
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
title: Text(translate(label)),
|
title: Text(translate(label)),
|
||||||
onToggle: (b) async {
|
onToggle: isOptFixed
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
|
? null
|
||||||
setState(() {});
|
: (b) async {
|
||||||
},
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: key, value: b ? 'Y' : defaultOptionNo);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -849,11 +970,11 @@ class _RadioEntry {
|
|||||||
typedef _RadioEntryGetter = String Function();
|
typedef _RadioEntryGetter = String Function();
|
||||||
typedef _RadioEntrySetter = Future<void> Function(String);
|
typedef _RadioEntrySetter = Future<void> Function(String);
|
||||||
|
|
||||||
_getPopupDialogRadioEntry({
|
SettingsTile _getPopupDialogRadioEntry({
|
||||||
required String title,
|
required String title,
|
||||||
required List<_RadioEntry> list,
|
required List<_RadioEntry> list,
|
||||||
required _RadioEntryGetter getter,
|
required _RadioEntryGetter getter,
|
||||||
required _RadioEntrySetter asyncSetter,
|
required _RadioEntrySetter? asyncSetter,
|
||||||
Widget? tail,
|
Widget? tail,
|
||||||
RxBool? showTail,
|
RxBool? showTail,
|
||||||
String? notCloseValue,
|
String? notCloseValue,
|
||||||
@@ -873,21 +994,23 @@ _getPopupDialogRadioEntry({
|
|||||||
|
|
||||||
void showDialog() async {
|
void showDialog() async {
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
onChanged(String? value) async {
|
final onChanged = asyncSetter == null
|
||||||
if (value == null) return;
|
? null
|
||||||
await asyncSetter(value);
|
: (String? value) async {
|
||||||
init();
|
if (value == null) return;
|
||||||
if (value != notCloseValue) {
|
await asyncSetter(value);
|
||||||
close();
|
init();
|
||||||
}
|
if (value != notCloseValue) {
|
||||||
}
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Obx(
|
content: Obx(
|
||||||
() => Column(children: [
|
() => Column(children: [
|
||||||
...list
|
...list
|
||||||
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
||||||
groupValue.value, (String? value) => onChanged(value)))
|
groupValue.value, onChanged))
|
||||||
.toList(),
|
.toList(),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage:
|
offstage:
|
||||||
@@ -901,7 +1024,7 @@ _getPopupDialogRadioEntry({
|
|||||||
|
|
||||||
return SettingsTile(
|
return SettingsTile(
|
||||||
title: Text(translate(title)),
|
title: Text(translate(title)),
|
||||||
onPressed: (context) => showDialog(),
|
onPressed: asyncSetter == null ? null : (context) => showDialog(),
|
||||||
value: Padding(
|
value: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Obx(() => Text(translate(valueText.value))),
|
child: Obx(() => Text(translate(valueText.value))),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.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/model.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
@@ -509,7 +510,8 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setShouldAsync(bool v) async {
|
void setShouldAsync(bool v) async {
|
||||||
await bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
|
await bind.mainSetLocalOption(
|
||||||
|
key: syncAbOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
_syncAllFromRecent = true;
|
_syncAllFromRecent = true;
|
||||||
_timerCounter = 0;
|
_timerCounter = 0;
|
||||||
}
|
}
|
||||||
@@ -548,7 +550,7 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trySetCurrentToLast() {
|
trySetCurrentToLast() {
|
||||||
final name = bind.getLocalFlutterOption(k: 'current-ab-name');
|
final name = bind.getLocalFlutterOption(k: kOptionCurrentAbName);
|
||||||
if (addressbooks.containsKey(name)) {
|
if (addressbooks.containsKey(name)) {
|
||||||
_currentName.value = name;
|
_currentName.value = name;
|
||||||
}
|
}
|
||||||
@@ -647,6 +649,10 @@ class AbModel {
|
|||||||
return addressbooks.keys.toList();
|
return addressbooks.keys.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String personalAddressBookName() {
|
||||||
|
return _personalAddressBookName;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setCurrentName(String name) async {
|
Future<void> setCurrentName(String name) async {
|
||||||
final oldName = _currentName.value;
|
final oldName = _currentName.value;
|
||||||
if (addressbooks.containsKey(name)) {
|
if (addressbooks.containsKey(name)) {
|
||||||
|
|||||||
@@ -527,10 +527,18 @@ class ChatModel with ChangeNotifier {
|
|||||||
|
|
||||||
void onVoiceCallStarted() {
|
void onVoiceCallStarted() {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.connected;
|
_voiceCallStatus.value = VoiceCallStatus.connected;
|
||||||
|
if (isAndroid) {
|
||||||
|
parent.target?.invokeMethod("on_voice_call_started");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onVoiceCallClosed(String reason) {
|
void onVoiceCallClosed(String reason) {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
|
if (isAndroid) {
|
||||||
|
// We can always invoke "on_voice_call_closed"
|
||||||
|
// no matter if the `_voiceCallStatus` was `VoiceCallStatus.notStarted` or not.
|
||||||
|
parent.target?.invokeMethod("on_voice_call_closed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onVoiceCallIncoming() {
|
void onVoiceCallIncoming() {
|
||||||
|
|||||||
@@ -11,15 +11,10 @@ import './platform_model.dart';
|
|||||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/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 vram is enabled.
|
|
||||||
final useTextureRender = !isWeb &&
|
|
||||||
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
|
|
||||||
|
|
||||||
class _PixelbufferTexture {
|
class _PixelbufferTexture {
|
||||||
int _textureKey = -1;
|
int _textureKey = -1;
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasPixelbufferTextureRender();
|
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
@@ -28,26 +23,24 @@ class _PixelbufferTexture {
|
|||||||
int get display => _display;
|
int get display => _display;
|
||||||
|
|
||||||
create(int d, SessionID sessionId, FFI ffi) {
|
create(int d, SessionID sessionId, FFI ffi) {
|
||||||
if (support) {
|
_display = d;
|
||||||
_display = d;
|
_textureKey = bind.getNextTextureKey();
|
||||||
_textureKey = bind.getNextTextureKey();
|
_sessionId = sessionId;
|
||||||
_sessionId = sessionId;
|
|
||||||
|
|
||||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(bool unregisterTexture, FFI ffi) async {
|
||||||
if (!_destroying && support && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
if (unregisterTexture) {
|
||||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class GroupModel {
|
|||||||
GroupModel(this.parent);
|
GroupModel(this.parent);
|
||||||
|
|
||||||
Future<void> pull({force = true, quiet = false}) async {
|
Future<void> pull({force = true, quiet = false}) async {
|
||||||
|
if (bind.isDisableGroupPanel()) return;
|
||||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||||
if (!force && initialized) return;
|
if (!force && initialized) return;
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
|
|||||||
@@ -1152,4 +1152,27 @@ class InputModel {
|
|||||||
platformFFI.stopDesktopWebListener();
|
platformFFI.stopDesktopWebListener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onMobileBack() => tap(MouseButtons.right);
|
||||||
|
void onMobileHome() => tap(MouseButtons.wheel);
|
||||||
|
Future<void> onMobileApps() async {
|
||||||
|
sendMouse('down', MouseButtons.wheel);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
sendMouse('up', MouseButtons.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a key press event.
|
||||||
|
// `usbHidUsage` is the USB HID usage code of the key.
|
||||||
|
Future<void> tapHidKey(int usbHidUsage) async {
|
||||||
|
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onMobileVolumeUp() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
|
||||||
|
Future<void> onMobileVolumeDown() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
|
||||||
|
Future<void> onMobilePower() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
RxBool waitForImageDialogShow = true.obs;
|
RxBool waitForImageDialogShow = true.obs;
|
||||||
Timer? waitForImageTimer;
|
Timer? waitForImageTimer;
|
||||||
RxBool waitForFirstImage = true.obs;
|
RxBool waitForFirstImage = true.obs;
|
||||||
|
bool isRefreshing = false;
|
||||||
|
|
||||||
Rect? get rect => _rect;
|
Rect? get rect => _rect;
|
||||||
bool get isOriginalResolutionSet =>
|
bool get isOriginalResolutionSet =>
|
||||||
@@ -191,10 +192,10 @@ class FfiModel with ChangeNotifier {
|
|||||||
_permissions[k] = v == 'true';
|
_permissions[k] = v == 'true';
|
||||||
});
|
});
|
||||||
// Only inited at remote page
|
// Only inited at remote page
|
||||||
if (desktopType == DesktopType.remote) {
|
if (parent.target?.connType == ConnType.defaultConn) {
|
||||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||||
}
|
}
|
||||||
debugPrint('$_permissions');
|
debugPrint('updatePermission: $_permissions');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +245,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
handleMsgBox({
|
handleMsgBox({
|
||||||
'type': 'success',
|
'type': 'success',
|
||||||
'title': 'Successful',
|
'title': 'Successful',
|
||||||
'text': 'Connected, waiting for image...',
|
'text': kMsgboxTextWaitingForImage,
|
||||||
'link': '',
|
'link': '',
|
||||||
}, sessionId, peerId);
|
}, sessionId, peerId);
|
||||||
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
||||||
@@ -380,16 +381,27 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handleSyncPeerOption(evt, peerId);
|
_handleSyncPeerOption(evt, peerId);
|
||||||
} else if (name == 'follow_current_display') {
|
} else if (name == 'follow_current_display') {
|
||||||
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||||
|
} else if (name == 'use_texture_render') {
|
||||||
|
_handleUseTextureRender(evt, sessionId, peerId);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Unknown event name: $name');
|
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) {
|
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
|
||||||
final k = evt['k'];
|
final k = evt['k'];
|
||||||
final v = evt['v'];
|
final v = evt['v'];
|
||||||
if (k == kOptionViewOnly) {
|
if (k == kOptionToggleViewOnly) {
|
||||||
setViewOnly(peer, v as bool);
|
setViewOnly(peer, v as bool);
|
||||||
} else if (k == 'keyboard_mode') {
|
} else if (k == 'keyboard_mode') {
|
||||||
parent.target?.inputModel.updateKeyboardMode();
|
parent.target?.inputModel.updateKeyboardMode();
|
||||||
@@ -426,20 +438,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
||||||
final running = evt['running'] == 'true';
|
final running = evt['running'] == 'true';
|
||||||
parent.target?.elevationModel.onPortableServiceRunning(running);
|
parent.target?.elevationModel.onPortableServiceRunning(running);
|
||||||
if (running) {
|
|
||||||
if (pi.primaryDisplay != kInvalidDisplayIndex) {
|
|
||||||
if (pi.currentDisplay != pi.primaryDisplay) {
|
|
||||||
// Notify to switch display
|
|
||||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
|
||||||
'elevated_switch_display_msg', '', parent.target!.dialogManager);
|
|
||||||
bind.sessionSwitchDisplay(
|
|
||||||
isDesktop: isDesktop,
|
|
||||||
sessionId: sessionId,
|
|
||||||
value: Int32List.fromList([pi.primaryDisplay]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAliasChanged(Map<String, dynamic> evt) {
|
handleAliasChanged(Map<String, dynamic> evt) {
|
||||||
@@ -572,7 +570,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
showElevationError(sessionId, type, title, text, dialogManager);
|
showElevationError(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||||
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
||||||
} else if (text == 'Connected, waiting for image...') {
|
} else if (text == kMsgboxTextWaitingForImage) {
|
||||||
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
||||||
} else if (title == 'Privacy mode') {
|
} else if (title == 'Privacy mode') {
|
||||||
final hasRetry = evt['hasRetry'] == 'true';
|
final hasRetry = evt['hasRetry'] == 'true';
|
||||||
@@ -667,7 +665,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
waitForImageDialogShow.value = true;
|
waitForImageDialogShow.value = true;
|
||||||
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
||||||
if (waitForFirstImage.isTrue) {
|
if (waitForFirstImage.isTrue && !isRefreshing) {
|
||||||
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -765,7 +763,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
_touchMode = true;
|
_touchMode = true;
|
||||||
} else {
|
} else {
|
||||||
_touchMode = await bind.sessionGetOption(
|
_touchMode = await bind.sessionGetOption(
|
||||||
sessionId: sessionId, arg: 'touch-mode') !=
|
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
if (connType == ConnType.fileTransfer) {
|
if (connType == ConnType.fileTransfer) {
|
||||||
@@ -785,6 +783,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
Map<String, dynamic> features = json.decode(evt['features']);
|
Map<String, dynamic> features = json.decode(evt['features']);
|
||||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||||
@@ -797,7 +796,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
setViewOnly(
|
setViewOnly(
|
||||||
peerId,
|
peerId,
|
||||||
bind.sessionGetToggleOptionSync(
|
bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: kOptionViewOnly));
|
sessionId: sessionId, arg: kOptionToggleViewOnly));
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn) {
|
if (connType == ConnType.defaultConn) {
|
||||||
final platformAdditions = evt['platform_additions'];
|
final platformAdditions = evt['platform_additions'];
|
||||||
@@ -904,10 +903,12 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (parent.target?.connType == ConnType.defaultConn &&
|
if (parent.target?.connType == ConnType.defaultConn &&
|
||||||
parent.target != null &&
|
parent.target != null &&
|
||||||
parent.target!.ffiModel.permissions['keyboard'] != false) {
|
parent.target!.ffiModel.permissions['keyboard'] != false) {
|
||||||
Timer(
|
Timer(Duration(milliseconds: delayMSecs), () {
|
||||||
Duration(milliseconds: delayMSecs),
|
if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) {
|
||||||
() => parent.target!.dialogManager
|
parent.target!.dialogManager
|
||||||
.showMobileActionsOverlay(ffi: parent.target!));
|
.showMobileActionsOverlay(ffi: parent.target!);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -960,7 +961,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLastCursorId(Map<String, dynamic> evt) {
|
updateLastCursorId(Map<String, dynamic> evt) {
|
||||||
parent.target?.cursorModel.id = int.parse(evt['id']);
|
// int.parse(evt['id']) may cause FormatException
|
||||||
|
// Unhandled Exception: FormatException: Positive input exceeds the limit of integer 18446744071749110741
|
||||||
|
parent.target?.cursorModel.id = evt['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCursorId(Map<String, dynamic> evt) {
|
handleCursorId(Map<String, dynamic> evt) {
|
||||||
@@ -1156,6 +1159,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
late final SessionID sessionId;
|
late final SessionID sessionId;
|
||||||
|
|
||||||
|
bool _useTextureRender = false;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
final List<Function(String)> callbacksOnFirstImage = [];
|
final List<Function(String)> callbacksOnFirstImage = [];
|
||||||
@@ -1164,14 +1169,17 @@ class ImageModel with ChangeNotifier {
|
|||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get useTextureRender => _useTextureRender;
|
||||||
|
|
||||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||||
|
|
||||||
onRgba(int display, Uint8List rgba) {
|
onRgba(int display, Uint8List rgba) {
|
||||||
final pid = parent.target?.id;
|
final pid = parent.target?.id;
|
||||||
|
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
|
||||||
img.decodeImageFromPixels(
|
img.decodeImageFromPixels(
|
||||||
rgba,
|
rgba,
|
||||||
parent.target?.ffiModel.rect?.width.toInt() ?? 0,
|
rect?.width.toInt() ?? 0,
|
||||||
parent.target?.ffiModel.rect?.height.toInt() ?? 0,
|
rect?.height.toInt() ?? 0,
|
||||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||||
onPixelsCopied: () {
|
onPixelsCopied: () {
|
||||||
// Unlock the rgba memory from rust codes.
|
// Unlock the rgba memory from rust codes.
|
||||||
@@ -1203,11 +1211,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
if (parent.target != null) {
|
if (parent.target != null) {
|
||||||
await initializeCursorAndCanvas(parent.target!);
|
await initializeCursorAndCanvas(parent.target!);
|
||||||
}
|
}
|
||||||
if (parent.target?.ffiModel.isPeerAndroid ?? false) {
|
|
||||||
bind.sessionSetViewStyle(sessionId: sessionId, value: 'adaptive');
|
|
||||||
parent.target?.canvasModel.updateViewStyle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_image?.dispose();
|
||||||
_image = image;
|
_image = image;
|
||||||
if (image != null) notifyListeners();
|
if (image != null) notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -1231,6 +1236,24 @@ class ImageModel with ChangeNotifier {
|
|||||||
final yscale = size.height / _image!.height;
|
final yscale = size.height / _image!.height;
|
||||||
return min(xscale, yscale) / 1.5;
|
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 {
|
enum ScrollStyle {
|
||||||
@@ -1541,22 +1564,24 @@ class CanvasModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScale(double v) {
|
updateScale(double v, Offset focalPoint) {
|
||||||
if (parent.target?.imageModel.image == null) return;
|
if (parent.target?.imageModel.image == null) return;
|
||||||
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
|
final s = _scale;
|
||||||
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
|
||||||
final px0 = (offset.dx - r.left) * _scale;
|
|
||||||
final py0 = (offset.dy - r.top) * _scale;
|
|
||||||
_scale *= v;
|
_scale *= v;
|
||||||
final maxs = parent.target?.imageModel.maxScale ?? 1;
|
final maxs = parent.target?.imageModel.maxScale ?? 1;
|
||||||
final mins = parent.target?.imageModel.minScale ?? 1;
|
final mins = parent.target?.imageModel.minScale ?? 1;
|
||||||
if (_scale > maxs) _scale = maxs;
|
if (_scale > maxs) _scale = maxs;
|
||||||
if (_scale < mins) _scale = mins;
|
if (_scale < mins) _scale = mins;
|
||||||
r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
|
||||||
final px1 = (offset.dx - r.left) * _scale;
|
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
|
||||||
final py1 = (offset.dy - r.top) * _scale;
|
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
|
||||||
_x -= px1 - px0;
|
final adjustForKeyboard =
|
||||||
_y -= py1 - py0;
|
parent.target?.cursorModel.adjustForKeyboard() ?? 0.0;
|
||||||
|
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY
|
||||||
|
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2
|
||||||
|
_y = focalPoint.dy +
|
||||||
|
adjustForKeyboard -
|
||||||
|
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1587,7 +1612,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
// data for cursor
|
// data for cursor
|
||||||
class CursorData {
|
class CursorData {
|
||||||
final String peerId;
|
final String peerId;
|
||||||
final int id;
|
final String id;
|
||||||
final img2.Image image;
|
final img2.Image image;
|
||||||
double scale;
|
double scale;
|
||||||
Uint8List? data;
|
Uint8List? data;
|
||||||
@@ -1667,13 +1692,15 @@ const _forbiddenCursorPng =
|
|||||||
const _defaultCursorPng =
|
const _defaultCursorPng =
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
||||||
|
|
||||||
|
const kPreForbiddenCursorId = "-2";
|
||||||
final preForbiddenCursor = PredefinedCursor(
|
final preForbiddenCursor = PredefinedCursor(
|
||||||
png: _forbiddenCursorPng,
|
png: _forbiddenCursorPng,
|
||||||
id: -2,
|
id: kPreForbiddenCursorId,
|
||||||
);
|
);
|
||||||
|
const kPreDefaultCursorId = "-1";
|
||||||
final preDefaultCursor = PredefinedCursor(
|
final preDefaultCursor = PredefinedCursor(
|
||||||
png: _defaultCursorPng,
|
png: _defaultCursorPng,
|
||||||
id: -1,
|
id: kPreDefaultCursorId,
|
||||||
hotxGetter: (double w) => w / 2,
|
hotxGetter: (double w) => w / 2,
|
||||||
hotyGetter: (double h) => h / 2,
|
hotyGetter: (double h) => h / 2,
|
||||||
);
|
);
|
||||||
@@ -1683,7 +1710,7 @@ class PredefinedCursor {
|
|||||||
img2.Image? _image2;
|
img2.Image? _image2;
|
||||||
CursorData? _cache;
|
CursorData? _cache;
|
||||||
String png;
|
String png;
|
||||||
int id;
|
String id;
|
||||||
double Function(double)? hotxGetter;
|
double Function(double)? hotxGetter;
|
||||||
double Function(double)? hotyGetter;
|
double Function(double)? hotyGetter;
|
||||||
|
|
||||||
@@ -1698,13 +1725,22 @@ class PredefinedCursor {
|
|||||||
init() {
|
init() {
|
||||||
_image2 = img2.decodePng(base64Decode(png));
|
_image2 = img2.decodePng(base64Decode(png));
|
||||||
if (_image2 != null) {
|
if (_image2 != null) {
|
||||||
|
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||||
|
if (id == kPreForbiddenCursorId) {
|
||||||
|
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
||||||
|
}
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
final defaultImg = _image2!;
|
final defaultImg = _image2!;
|
||||||
// This function is called only one time, no need to care about the performance.
|
// This function is called only one time, no need to care about the performance.
|
||||||
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
||||||
|
_image?.dispose();
|
||||||
_image = await img.decodeImageFromPixels(
|
_image = await img.decodeImageFromPixels(
|
||||||
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
||||||
|
if (_image == null) {
|
||||||
|
print("decodeImageFromPixels failed, pre-defined cursor $id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
double scale = 1.0;
|
double scale = 1.0;
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
||||||
@@ -1732,13 +1768,15 @@ class PredefinedCursor {
|
|||||||
|
|
||||||
class CursorModel with ChangeNotifier {
|
class CursorModel with ChangeNotifier {
|
||||||
ui.Image? _image;
|
ui.Image? _image;
|
||||||
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
final _images = <String, Tuple3<ui.Image, double, double>>{};
|
||||||
CursorData? _cache;
|
CursorData? _cache;
|
||||||
final _cacheMap = <int, CursorData>{};
|
final _cacheMap = <String, CursorData>{};
|
||||||
final _cacheKeys = <String>{};
|
final _cacheKeys = <String>{};
|
||||||
double _x = -10000;
|
double _x = -10000;
|
||||||
double _y = -10000;
|
double _y = -10000;
|
||||||
int _id = -1;
|
// int.parse(evt['id']) may cause FormatException
|
||||||
|
// So we use String here.
|
||||||
|
String _id = "-1";
|
||||||
double _hotx = 0;
|
double _hotx = 0;
|
||||||
double _hoty = 0;
|
double _hoty = 0;
|
||||||
double _displayOriginX = 0;
|
double _displayOriginX = 0;
|
||||||
@@ -1752,6 +1790,33 @@ class CursorModel with ChangeNotifier {
|
|||||||
String peerId = '';
|
String peerId = '';
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
// Only for mobile, touch mode
|
||||||
|
// To block touch event above the KeyHelpTools
|
||||||
|
//
|
||||||
|
// A better way is to not listen events from the KeyHelpTools.
|
||||||
|
// But we're now using a Container(child: Stack(...)) to wrap the KeyHelpTools,
|
||||||
|
// and the listener is on the Container.
|
||||||
|
Rect? _keyHelpToolsRect;
|
||||||
|
// `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap()
|
||||||
|
// Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position.
|
||||||
|
bool _lastIsBlocked = false;
|
||||||
|
double _yForKeyboardAdjust = 0;
|
||||||
|
|
||||||
|
keyHelpToolsVisibilityChanged(Rect? r) {
|
||||||
|
_keyHelpToolsRect = r;
|
||||||
|
if (r == null) {
|
||||||
|
_lastIsBlocked = false;
|
||||||
|
} else {
|
||||||
|
// Block the touch event is safe here.
|
||||||
|
// `lastIsBlocked` is only used in onDoubleTap() to block the touch event from the KeyHelpTools.
|
||||||
|
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
|
||||||
|
_lastIsBlocked = true;
|
||||||
|
}
|
||||||
|
_yForKeyboardAdjust = _y;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastIsBlocked => _lastIsBlocked;
|
||||||
|
|
||||||
ui.Image? get image => _image;
|
ui.Image? get image => _image;
|
||||||
CursorData? get cache => _cache;
|
CursorData? get cache => _cache;
|
||||||
|
|
||||||
@@ -1765,7 +1830,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
double get hotx => _hotx;
|
double get hotx => _hotx;
|
||||||
double get hoty => _hoty;
|
double get hoty => _hoty;
|
||||||
|
|
||||||
set id(int id) => _id = id;
|
set id(String id) => _id = id;
|
||||||
|
|
||||||
bool get isPeerControlProtected =>
|
bool get isPeerControlProtected =>
|
||||||
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
|
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
|
||||||
@@ -1796,28 +1861,52 @@ class CursorModel with ChangeNotifier {
|
|||||||
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom;
|
||||||
|
get scale => parent.target?.canvasModel.scale ?? 1.0;
|
||||||
|
|
||||||
double adjustForKeyboard() {
|
double adjustForKeyboard() {
|
||||||
|
if (keyboardHeight < 100) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
final m = MediaQueryData.fromWindow(ui.window);
|
final m = MediaQueryData.fromWindow(ui.window);
|
||||||
var keyboardHeight = m.viewInsets.bottom;
|
|
||||||
final size = m.size;
|
final size = m.size;
|
||||||
if (keyboardHeight < 100) return 0;
|
|
||||||
final s = parent.target?.canvasModel.scale ?? 1.0;
|
|
||||||
final thresh = (size.height - keyboardHeight) / 2;
|
final thresh = (size.height - keyboardHeight) / 2;
|
||||||
var h = (_y - getVisibleRect().top) * s; // local physical display height
|
final h = (_yForKeyboardAdjust - getVisibleRect().top) *
|
||||||
|
scale; // local physical display height
|
||||||
return h - thresh;
|
return h - thresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
move(double x, double y) {
|
// mobile Soft keyboard, block touch event from the KeyHelpTools
|
||||||
moveLocal(x, y);
|
shouldBlock(double x, double y) {
|
||||||
parent.target?.inputModel.moveMouse(_x, _y);
|
if (!(parent.target?.ffiModel.touchMode ?? false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_keyHelpToolsRect == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLocal(double x, double y) {
|
move(double x, double y) {
|
||||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
if (shouldBlock(x, y)) {
|
||||||
|
_lastIsBlocked = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_lastIsBlocked = false;
|
||||||
|
moveLocal(x, y, adjust: adjustForKeyboard());
|
||||||
|
parent.target?.inputModel.moveMouse(_x, _y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLocal(double x, double y, {double adjust = 0}) {
|
||||||
final xoffset = parent.target?.canvasModel.x ?? 0;
|
final xoffset = parent.target?.canvasModel.x ?? 0;
|
||||||
final yoffset = parent.target?.canvasModel.y ?? 0;
|
final yoffset = parent.target?.canvasModel.y ?? 0;
|
||||||
_x = (x - xoffset) / scale + _displayOriginX;
|
_x = (x - xoffset) / scale + _displayOriginX;
|
||||||
_y = (y - yoffset) / scale + _displayOriginY;
|
_y = (y - yoffset + adjust) / scale + _displayOriginY;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1937,8 +2026,13 @@ class CursorModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposeImages() {
|
||||||
|
_images.forEach((_, v) => v.item1.dispose());
|
||||||
|
_images.clear();
|
||||||
|
}
|
||||||
|
|
||||||
updateCursorData(Map<String, dynamic> evt) async {
|
updateCursorData(Map<String, dynamic> evt) async {
|
||||||
final id = int.parse(evt['id']);
|
final id = evt['id'];
|
||||||
final hotx = double.parse(evt['hotx']);
|
final hotx = double.parse(evt['hotx']);
|
||||||
final hoty = double.parse(evt['hoty']);
|
final hoty = double.parse(evt['hoty']);
|
||||||
final width = int.parse(evt['width']);
|
final width = int.parse(evt['width']);
|
||||||
@@ -1947,7 +2041,11 @@ class CursorModel with ChangeNotifier {
|
|||||||
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||||
final image = await img.decodeImageFromPixels(
|
final image = await img.decodeImageFromPixels(
|
||||||
rgba, width, height, ui.PixelFormat.rgba8888);
|
rgba, width, height, ui.PixelFormat.rgba8888);
|
||||||
|
if (image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
||||||
|
_images[id]?.item1.dispose();
|
||||||
_images[id] = Tuple3(image, hotx, hoty);
|
_images[id] = Tuple3(image, hotx, hoty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1959,7 +2057,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
Future<bool> _updateCache(
|
Future<bool> _updateCache(
|
||||||
Uint8List rgba,
|
Uint8List rgba,
|
||||||
ui.Image image,
|
ui.Image image,
|
||||||
int id,
|
String id,
|
||||||
double hotx,
|
double hotx,
|
||||||
double hoty,
|
double hoty,
|
||||||
int w,
|
int w,
|
||||||
@@ -2062,7 +2160,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
_x = -10000;
|
_x = -10000;
|
||||||
_x = -10000;
|
_x = -10000;
|
||||||
_image = null;
|
_image = null;
|
||||||
_images.clear();
|
disposeImages();
|
||||||
|
|
||||||
_clearCache();
|
_clearCache();
|
||||||
_cache = null;
|
_cache = null;
|
||||||
@@ -2305,6 +2403,7 @@ class FFI {
|
|||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
ffiModel.waitForImageTimer?.cancel();
|
ffiModel.waitForImageTimer?.cancel();
|
||||||
ffiModel.waitForImageTimer = null;
|
ffiModel.waitForImageTimer = null;
|
||||||
@@ -2340,9 +2439,10 @@ class FFI {
|
|||||||
cursorModel.peerId = id;
|
cursorModel.peerId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isNewPeer = tabWindowId == null;
|
||||||
// If tabWindowId != null, this session is a "tab -> window" one.
|
// If tabWindowId != null, this session is a "tab -> window" one.
|
||||||
// Else this session is a new one.
|
// Else this session is a new one.
|
||||||
if (tabWindowId == null) {
|
if (isNewPeer) {
|
||||||
// ignore: unused_local_variable
|
// ignore: unused_local_variable
|
||||||
final addRes = bind.sessionAddSync(
|
final addRes = bind.sessionAddSync(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
@@ -2361,20 +2461,32 @@ class FFI {
|
|||||||
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId);
|
final addRes = bind.sessionAddExistedSync(
|
||||||
|
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||||
if (addRes != '') {
|
if (addRes != '') {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'Unreachable, failed to add existed session to $id, $addRes');
|
'Unreachable, failed to add existed session to $id, $addRes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.sessionTryAddDisplay(
|
|
||||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
|
||||||
ffiModel.pi.currentDisplay = display;
|
ffiModel.pi.currentDisplay = display;
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn && useTextureRender) {
|
if (isDesktop && connType == ConnType.defaultConn) {
|
||||||
textureModel.updateCurrentDisplay(display ?? 0);
|
textureModel.updateCurrentDisplay(display ?? 0);
|
||||||
}
|
}
|
||||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
|
||||||
|
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
|
||||||
|
// Though the stream is returned immediately, the stream may not be ready.
|
||||||
|
// Any operations that depend on the stream should be carefully handled.
|
||||||
|
late final Stream<EventToUI> stream;
|
||||||
|
if (isNewPeer || display == null || displays == null) {
|
||||||
|
stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||||
|
} else {
|
||||||
|
// We have to put displays in `sessionStart()` to make sure the stream is ready
|
||||||
|
// and then the displays' capturing requests can be sent.
|
||||||
|
stream = bind.sessionStartWithDisplays(
|
||||||
|
sessionId: sessionId, id: id, displays: Int32List.fromList(displays));
|
||||||
|
}
|
||||||
|
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
platformFFI.setRgbaCallback((int display, Uint8List data) {
|
platformFFI.setRgbaCallback((int display, Uint8List data) {
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
@@ -2385,17 +2497,8 @@ class FFI {
|
|||||||
|
|
||||||
final cb = ffiModel.startEventListener(sessionId, id);
|
final cb = ffiModel.startEventListener(sessionId, id);
|
||||||
|
|
||||||
// Force refresh displays.
|
imageModel.updateUserTextureRender();
|
||||||
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
|
|
||||||
if (displays != null) {
|
|
||||||
for (final display in displays) {
|
|
||||||
bind.sessionRefresh(sessionId: sessionId, display: display);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final hasPixelBufferTextureRender = bind.mainHasPixelbufferTextureRender();
|
|
||||||
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
||||||
|
|
||||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||||
// Preserved for the rgba data.
|
// Preserved for the rgba data.
|
||||||
stream.listen((message) {
|
stream.listen((message) {
|
||||||
@@ -2444,20 +2547,23 @@ class FFI {
|
|||||||
}
|
}
|
||||||
} else if (message is EventToUI_Rgba) {
|
} else if (message is EventToUI_Rgba) {
|
||||||
final display = message.field0;
|
final display = message.field0;
|
||||||
if (hasPixelBufferTextureRender) {
|
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
|
||||||
debugPrint("EventToUI_Rgba display:$display");
|
//debugPrint("EventToUI_Rgba display:$display");
|
||||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
} else {
|
} else {
|
||||||
// Fetch the image buffer from rust codes.
|
// Fetch the image buffer from rust codes.
|
||||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||||
if (sz == 0) {
|
if (sz == 0) {
|
||||||
|
platformFFI.nextRgba(sessionId, display);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||||
if (rgba != null) {
|
if (rgba != null) {
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
imageModel.onRgba(display, rgba);
|
imageModel.onRgba(display, rgba);
|
||||||
|
} else {
|
||||||
|
platformFFI.nextRgba(sessionId, display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (message is EventToUI_Texture) {
|
} else if (message is EventToUI_Texture) {
|
||||||
@@ -2633,6 +2739,7 @@ class PeerInfo with ChangeNotifier {
|
|||||||
|
|
||||||
bool get isSupportMultiDisplay =>
|
bool get isSupportMultiDisplay =>
|
||||||
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
||||||
|
bool get forceTextureRender => currentDisplay == kAllDisplayValue;
|
||||||
|
|
||||||
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
||||||
|
|
||||||
@@ -2641,30 +2748,32 @@ class PeerInfo with ChangeNotifier {
|
|||||||
bool get isAmyuniIdd =>
|
bool get isAmyuniIdd =>
|
||||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||||
|
|
||||||
Display? tryGetDisplay() {
|
Display? tryGetDisplay({int? display}) {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay == kAllDisplayValue) {
|
display ??= currentDisplay;
|
||||||
|
if (display == kAllDisplayValue) {
|
||||||
return displays[0];
|
return displays[0];
|
||||||
} else {
|
} else {
|
||||||
if (currentDisplay > 0 && currentDisplay < displays.length) {
|
if (display > 0 && display < displays.length) {
|
||||||
return displays[currentDisplay];
|
return displays[display];
|
||||||
} else {
|
} else {
|
||||||
return displays[0];
|
return displays[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Display? tryGetDisplayIfNotAllDisplay() {
|
Display? tryGetDisplayIfNotAllDisplay({int? display}) {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay == kAllDisplayValue) {
|
display ??= currentDisplay;
|
||||||
|
if (display == kAllDisplayValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay >= 0 && currentDisplay < displays.length) {
|
if (display >= 0 && display < displays.length) {
|
||||||
return displays[currentDisplay];
|
return displays[display];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2688,6 +2797,12 @@ class PeerInfo with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect? getDisplayRect(int display) {
|
||||||
|
final d = tryGetDisplayIfNotAllDisplay(display: display);
|
||||||
|
if (d == null) return null;
|
||||||
|
return Rect.fromLTWH(d.x, d.y, d.width.toDouble(), d.height.toDouble());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasKey = 'canvas';
|
const canvasKey = 'canvas';
|
||||||
|
|||||||
@@ -117,9 +117,13 @@ class PlatformFFI {
|
|||||||
? DynamicLibrary.open('librustdesk.so')
|
? DynamicLibrary.open('librustdesk.so')
|
||||||
: isWindows
|
: isWindows
|
||||||
? DynamicLibrary.open('librustdesk.dll')
|
? DynamicLibrary.open('librustdesk.dll')
|
||||||
: isMacOS
|
:
|
||||||
? DynamicLibrary.open("liblibrustdesk.dylib")
|
// Use executable itself as the dynamic library for MacOS.
|
||||||
: DynamicLibrary.process();
|
// Multiple dylib instances will cause some global instances to be invalid.
|
||||||
|
// eg. `lazy_static` objects in rust side, will be created more than once, which is not expected.
|
||||||
|
//
|
||||||
|
// isMacOS? DynamicLibrary.open("liblibrustdesk.dylib") :
|
||||||
|
DynamicLibrary.process();
|
||||||
debugPrint('initializing FFI $_appType');
|
debugPrint('initializing FFI $_appType');
|
||||||
try {
|
try {
|
||||||
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
||||||
@@ -132,9 +136,10 @@ class PlatformFFI {
|
|||||||
_ffiBind = RustdeskImpl(dylib);
|
_ffiBind = RustdeskImpl(dylib);
|
||||||
|
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
// Start a dbus service, no need to await
|
if (isMain) {
|
||||||
_ffiBind.mainStartDbusServer();
|
// Start a dbus service for uri links, no need to await
|
||||||
_ffiBind.mainStartPa();
|
_ffiBind.mainStartDbusServer();
|
||||||
|
}
|
||||||
} else if (isMacOS && isMain) {
|
} else if (isMacOS && isMain) {
|
||||||
// Start ipc service for uri links.
|
// Start ipc service for uri links.
|
||||||
_ffiBind.mainStartIpcUrlServer();
|
_ffiBind.mainStartIpcUrlServer();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,9 +23,6 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
int get currentTab => _currentTab;
|
int get currentTab => _currentTab;
|
||||||
int _currentTab = 0; // index in tabNames
|
int _currentTab = 0; // index in tabNames
|
||||||
static const int maxTabCount = 5;
|
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 = [
|
static const List<String> tabNames = [
|
||||||
'Recent sessions',
|
'Recent sessions',
|
||||||
'Favorites',
|
'Favorites',
|
||||||
@@ -44,7 +42,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
true,
|
true,
|
||||||
!isWeb,
|
!isWeb,
|
||||||
!(bind.isDisableAb() || bind.isDisableAccount()),
|
!(bind.isDisableAb() || bind.isDisableAccount()),
|
||||||
!bind.isDisableAccount(),
|
!(bind.isDisableGroupPanel() || bind.isDisableAccount()),
|
||||||
]);
|
]);
|
||||||
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
||||||
List<bool> get isVisibleEnabled => () {
|
List<bool> get isVisibleEnabled => () {
|
||||||
@@ -72,7 +70,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
PeerTabModel(this.parent) {
|
PeerTabModel(this.parent) {
|
||||||
// visible
|
// visible
|
||||||
try {
|
try {
|
||||||
final option = bind.getLocalFlutterOption(k: kPeerTabVisible);
|
final option = bind.getLocalFlutterOption(k: kOptionPeerTabVisible);
|
||||||
if (option.isNotEmpty) {
|
if (option.isNotEmpty) {
|
||||||
List<dynamic> decodeList = jsonDecode(option);
|
List<dynamic> decodeList = jsonDecode(option);
|
||||||
if (decodeList.length == _isVisible.length) {
|
if (decodeList.length == _isVisible.length) {
|
||||||
@@ -88,7 +86,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// order
|
// order
|
||||||
try {
|
try {
|
||||||
final option = bind.getLocalFlutterOption(k: kPeerTabOrder);
|
final option = bind.getLocalFlutterOption(k: kOptionPeerTabOrder);
|
||||||
if (option.isNotEmpty) {
|
if (option.isNotEmpty) {
|
||||||
List<dynamic> decodeList = jsonDecode(option);
|
List<dynamic> decodeList = jsonDecode(option);
|
||||||
if (decodeList.length == maxTabCount) {
|
if (decodeList.length == maxTabCount) {
|
||||||
@@ -112,7 +110,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// init currentTab
|
// init currentTab
|
||||||
_currentTab =
|
_currentTab =
|
||||||
int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0;
|
int.tryParse(bind.getLocalFlutterOption(k: kOptionPeerTabIndex)) ?? 0;
|
||||||
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
||||||
_currentTab = 0;
|
_currentTab = 0;
|
||||||
}
|
}
|
||||||
@@ -222,7 +220,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kPeerTabVisible, v: jsonEncode(_isVisible));
|
k: kOptionPeerTabVisible, v: jsonEncode(_isVisible));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -258,7 +256,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
for (int i = 0; i < list.length; i++) {
|
for (int i = 0; i < list.length; i++) {
|
||||||
orders[i] = list[i];
|
orders[i] = list[i];
|
||||||
}
|
}
|
||||||
bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders));
|
bind.setLocalFlutterOption(k: kOptionPeerTabOrder, v: jsonEncode(orders));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -77,7 +78,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
String get approveMode => _approveMode;
|
String get approveMode => _approveMode;
|
||||||
|
|
||||||
setVerificationMethod(String method) async {
|
setVerificationMethod(String method) async {
|
||||||
await bind.mainSetOption(key: "verification-method", value: method);
|
await bind.mainSetOption(key: kOptionVerificationMethod, value: method);
|
||||||
/*
|
/*
|
||||||
if (method != kUsePermanentPassword) {
|
if (method != kUsePermanentPassword) {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
@@ -99,7 +100,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setApproveMode(String mode) async {
|
setApproveMode(String mode) async {
|
||||||
await bind.mainSetOption(key: 'approve-mode', value: mode);
|
await bind.mainSetOption(key: kOptionApproveMode, value: mode);
|
||||||
/*
|
/*
|
||||||
if (mode != 'password') {
|
if (mode != 'password') {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
@@ -125,8 +126,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
/*
|
/*
|
||||||
// initital _hideCm at startup
|
// initital _hideCm at startup
|
||||||
final verificationMethod =
|
final verificationMethod =
|
||||||
bind.mainGetOptionSync(key: "verification-method");
|
bind.mainGetOptionSync(key: kOptionVerificationMethod);
|
||||||
final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
|
final approveMode = bind.mainGetOptionSync(key: kOptionApproveMode);
|
||||||
_hideCm = option2bool(
|
_hideCm = option2bool(
|
||||||
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
||||||
if (!(approveMode == 'password' &&
|
if (!(approveMode == 'password' &&
|
||||||
@@ -176,6 +177,11 @@ class ServerModel with ChangeNotifier {
|
|||||||
await timerCallback();
|
await timerCallback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial keyboard status is off on mobile
|
||||||
|
if (isMobile) {
|
||||||
|
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 1. check android permission
|
/// 1. check android permission
|
||||||
@@ -187,18 +193,19 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (androidVersion < 30 ||
|
if (androidVersion < 30 ||
|
||||||
!await AndroidPermissionManager.check(kRecordAudio)) {
|
!await AndroidPermissionManager.check(kRecordAudio)) {
|
||||||
_audioOk = false;
|
_audioOk = false;
|
||||||
bind.mainSetOption(key: "enable-audio", value: "N");
|
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final audioOption = await bind.mainGetOption(key: 'enable-audio');
|
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||||
_audioOk = audioOption.isEmpty;
|
_audioOk = audioOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// file
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
_fileOk = false;
|
_fileOk = false;
|
||||||
bind.mainSetOption(key: "enable-file-transfer", value: "N");
|
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final fileOption = await bind.mainGetOption(key: 'enable-file-transfer');
|
final fileOption =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||||
_fileOk = fileOption.isEmpty;
|
_fileOk = fileOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,10 +216,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
var update = false;
|
var update = false;
|
||||||
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
||||||
final verificationMethod =
|
final verificationMethod =
|
||||||
await bind.mainGetOption(key: "verification-method");
|
await bind.mainGetOption(key: kOptionVerificationMethod);
|
||||||
final temporaryPasswordLength =
|
final temporaryPasswordLength =
|
||||||
await bind.mainGetOption(key: "temporary-password-length");
|
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(
|
var hideCm = option2bool(
|
||||||
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
||||||
@@ -225,8 +232,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_approveMode = approveMode;
|
_approveMode = approveMode;
|
||||||
update = true;
|
update = true;
|
||||||
}
|
}
|
||||||
var stopped = option2bool(
|
var stopped = await mainGetBoolOption(kOptionStopService);
|
||||||
"stop-service", await bind.mainGetOption(key: "stop-service"));
|
|
||||||
final oldPwdText = _serverPasswd.text;
|
final oldPwdText = _serverPasswd.text;
|
||||||
if (stopped ||
|
if (stopped ||
|
||||||
verificationMethod == kUsePermanentPassword ||
|
verificationMethod == kUsePermanentPassword ||
|
||||||
@@ -283,7 +289,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_audioOk = !_audioOk;
|
_audioOk = !_audioOk;
|
||||||
bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +309,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fileOk = !_fileOk;
|
_fileOk = !_fileOk;
|
||||||
bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableFileTransfer,
|
||||||
|
value: _fileOk ? defaultOptionYes : 'N');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +321,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (_inputOk) {
|
if (_inputOk) {
|
||||||
parent.target?.invokeMethod("stop_input");
|
parent.target?.invokeMethod("stop_input");
|
||||||
bind.mainSetOption(key: "enable-keyboard", value: 'N');
|
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||||
} else {
|
} else {
|
||||||
if (parent.target != null) {
|
if (parent.target != null) {
|
||||||
/// the result of toggle-on depends on user actions in the settings page.
|
/// the result of toggle-on depends on user actions in the settings page.
|
||||||
@@ -336,6 +345,20 @@ class ServerModel with ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> checkFloatingWindowPermission() async {
|
||||||
|
debugPrint("androidVersion $androidVersion");
|
||||||
|
if (androidVersion < 23) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
|
debugPrint("alert window permission already granted");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var res = await AndroidPermissionManager.request(kSystemAlertWindow);
|
||||||
|
debugPrint("alert window permission request result: $res");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle the screen sharing service.
|
/// Toggle the screen sharing service.
|
||||||
toggleService() async {
|
toggleService() async {
|
||||||
if (_isStart) {
|
if (_isStart) {
|
||||||
@@ -363,6 +386,12 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await checkRequestNotificationPermission();
|
await checkRequestNotificationPermission();
|
||||||
|
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
||||||
|
await checkFloatingWindowPermission();
|
||||||
|
}
|
||||||
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
|
}
|
||||||
final res = await parent.target?.dialogManager
|
final res = await parent.target?.dialogManager
|
||||||
.show<bool>((setState, close, context) {
|
.show<bool>((setState, close, context) {
|
||||||
submit() => close(true);
|
submit() => close(true);
|
||||||
@@ -398,7 +427,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
await bind.mainStartService();
|
await bind.mainStartService();
|
||||||
updateClientState();
|
updateClientState();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
WakelockPlus.enable();
|
androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +474,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case "input":
|
case "input":
|
||||||
if (_inputOk != value) {
|
if (_inputOk != value) {
|
||||||
bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableKeyboard,
|
||||||
|
value: value ? defaultOptionYes : 'N');
|
||||||
}
|
}
|
||||||
_inputOk = value;
|
_inputOk = value;
|
||||||
break;
|
break;
|
||||||
@@ -489,6 +520,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (_clients.length != oldClientLenght) {
|
if (_clients.length != oldClientLenght) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +555,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isAndroid && !client.authorized) showLoginDialog(client);
|
if (isAndroid && !client.authorized) showLoginDialog(client);
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to call loginRequest,error:$e");
|
debugPrint("Failed to call loginRequest,error:$e");
|
||||||
}
|
}
|
||||||
@@ -550,37 +583,60 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showLoginDialog(Client client) {
|
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) {
|
parent.target?.dialogManager.show((setState, close, context) {
|
||||||
cancel() {
|
cancel() {
|
||||||
sendLoginResponse(client, false);
|
onCancel();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
sendLoginResponse(client, true);
|
onSubmit();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title:
|
title:
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
Text(translate(
|
Text(translate(title)),
|
||||||
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
IconButton(onPressed: close, icon: const Icon(Icons.close))
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close))
|
|
||||||
]),
|
]),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(translate("Do you accept?")),
|
Text(translate(contentTitle)),
|
||||||
ClientInfo(client),
|
ClientInfo(client),
|
||||||
Text(
|
Text(
|
||||||
translate("android_new_connection_tip"),
|
translate(content),
|
||||||
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -620,6 +676,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
final index = _clients.indexOf(client);
|
final index = _clients.indexOf(client);
|
||||||
tabController.remove(index);
|
tabController.remove(index);
|
||||||
_clients.remove(client);
|
_clients.remove(client);
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,6 +700,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (desktopType == DesktopType.cm && _clients.isEmpty) {
|
if (desktopType == DesktopType.cm && _clients.isEmpty) {
|
||||||
hideCmWindow();
|
hideCmWindow();
|
||||||
}
|
}
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("onClientRemove failed,error:$e");
|
debugPrint("onClientRemove failed,error:$e");
|
||||||
@@ -654,6 +712,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
tabController.state.value.tabs.clear();
|
tabController.state.value.tabs.clear();
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
void jumpTo(int id) {
|
void jumpTo(int id) {
|
||||||
@@ -676,10 +735,14 @@ class ServerModel with ChangeNotifier {
|
|||||||
_clients[index].inVoiceCall = client.inVoiceCall;
|
_clients[index].inVoiceCall = client.inVoiceCall;
|
||||||
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
||||||
if (client.incomingVoiceCall) {
|
if (client.incomingVoiceCall) {
|
||||||
// Has incoming phone call, let's set the window on top.
|
if (isAndroid) {
|
||||||
Future.delayed(Duration.zero, () {
|
showVoiceCallDialog(client);
|
||||||
windowOnTop(null);
|
} else {
|
||||||
});
|
// Has incoming phone call, let's set the window on top.
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
windowOnTop(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -687,6 +750,27 @@ class ServerModel with ChangeNotifier {
|
|||||||
debugPrint("updateVoiceCallState failed: $e");
|
debugPrint("updateVoiceCallState failed: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void androidUpdatekeepScreenOn() async {
|
||||||
|
if (!isAndroid) return;
|
||||||
|
var floatingWindowDisabled =
|
||||||
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
|
final keepScreenOn = floatingWindowDisabled
|
||||||
|
? KeepScreenOn.never
|
||||||
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
||||||
|
final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
|
||||||
|
(keepScreenOn == KeepScreenOn.duringControlled &&
|
||||||
|
_clients.map((e) => !e.disconnected).isNotEmpty);
|
||||||
|
if (on != await WakelockPlus.enabled) {
|
||||||
|
if (on) {
|
||||||
|
WakelockPlus.enable();
|
||||||
|
} else {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ClientType {
|
enum ClientType {
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ class StateGlobal {
|
|||||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||||
final RxBool showRemoteToolBar = false.obs;
|
final RxBool showRemoteToolBar = false.obs;
|
||||||
final svcStatus = SvcStatus.notReady.obs;
|
final svcStatus = SvcStatus.notReady.obs;
|
||||||
// Only used for macOS
|
|
||||||
bool? closeOnFullscreen;
|
|
||||||
final RxBool isFocused = false.obs;
|
final RxBool isFocused = false.obs;
|
||||||
|
|
||||||
String _inputSource = '';
|
String _inputSource = '';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
|
||||||
Future<ui.Image> decodeImageFromPixels(
|
Future<ui.Image?> decodeImageFromPixels(
|
||||||
Uint8List pixels,
|
Uint8List pixels,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -13,41 +13,82 @@ Future<ui.Image> decodeImageFromPixels(
|
|||||||
int? rowBytes,
|
int? rowBytes,
|
||||||
int? targetWidth,
|
int? targetWidth,
|
||||||
int? targetHeight,
|
int? targetHeight,
|
||||||
VoidCallback? onPixelsCopied,
|
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
|
||||||
bool allowUpscaling = true,
|
bool allowUpscaling = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (targetWidth != null) {
|
if (targetWidth != null) {
|
||||||
assert(allowUpscaling || targetWidth <= width);
|
assert(allowUpscaling || targetWidth <= width);
|
||||||
|
if (!(allowUpscaling || targetWidth <= width)) {
|
||||||
|
print("not allow upscaling but targetWidth > width");
|
||||||
|
onPixelsCopied?.call();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (targetHeight != null) {
|
if (targetHeight != null) {
|
||||||
assert(allowUpscaling || targetHeight <= height);
|
assert(allowUpscaling || targetHeight <= height);
|
||||||
}
|
if (!(allowUpscaling || targetHeight <= height)) {
|
||||||
|
print("not allow upscaling but targetHeight > height");
|
||||||
final ui.ImmutableBuffer buffer =
|
onPixelsCopied?.call();
|
||||||
await ui.ImmutableBuffer.fromUint8List(pixels);
|
return null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ui.Codec codec = await descriptor.instantiateCodec(
|
final ui.ImmutableBuffer buffer;
|
||||||
targetWidth: targetWidth,
|
try {
|
||||||
targetHeight: targetHeight,
|
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||||
);
|
onPixelsCopied?.call();
|
||||||
|
} catch (e) {
|
||||||
|
onPixelsCopied?.call();
|
||||||
|
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();
|
codec.dispose();
|
||||||
buffer.dispose();
|
buffer.dispose();
|
||||||
descriptor.dispose();
|
descriptor.dispose();
|
||||||
|
|||||||
@@ -174,7 +174,9 @@ class RustDeskMultiWindowManager {
|
|||||||
windowId: windowId, peerId: remoteId);
|
windowId: windowId, peerId: remoteId);
|
||||||
}
|
}
|
||||||
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
||||||
WindowController.fromWindowId(windowId).show();
|
if (methodName != kWindowEventNewRemoteDesktop) {
|
||||||
|
WindowController.fromWindowId(windowId).show();
|
||||||
|
}
|
||||||
registerActiveWindow(windowId);
|
registerActiveWindow(windowId);
|
||||||
return MultiWindowCallResult(windowId, null);
|
return MultiWindowCallResult(windowId, null);
|
||||||
}
|
}
|
||||||
@@ -334,10 +336,10 @@ class RustDeskMultiWindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeAllSubWindows() async {
|
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) {
|
if (type == WindowType.Main) {
|
||||||
// skip main window, use window manager instead
|
// skip main window, use window manager instead
|
||||||
return;
|
return;
|
||||||
@@ -345,7 +347,7 @@ class RustDeskMultiWindowManager {
|
|||||||
|
|
||||||
List<int> windows = [];
|
List<int> windows = [];
|
||||||
try {
|
try {
|
||||||
windows = await DesktopMultiWindow.getAllSubWindowIds();
|
windows = _findWindowsByType(type);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
||||||
return;
|
return;
|
||||||
@@ -355,14 +357,9 @@ class RustDeskMultiWindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (final wId in windows) {
|
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);
|
await saveWindowPosition(type, windowId: wId);
|
||||||
try {
|
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).setPreventClose(false);
|
||||||
await WindowController.fromWindowId(wId).close();
|
await WindowController.fromWindowId(wId).close();
|
||||||
_activeWindows.remove(wId);
|
_activeWindows.remove(wId);
|
||||||
@@ -371,7 +368,6 @@ class RustDeskMultiWindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await _notifyActiveWindow();
|
|
||||||
clearWindowType(type);
|
clearWindowType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,14 +400,6 @@ class RustDeskMultiWindowManager {
|
|||||||
await _notifyActiveWindow();
|
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`]
|
/// Remove active window which has [`windowId`]
|
||||||
///
|
///
|
||||||
/// [Availability]
|
/// [Availability]
|
||||||
|
|||||||
@@ -59,15 +59,13 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String sessionAddExistedSync(
|
String sessionAddExistedSync(
|
||||||
{required String id, required UuidValue sessionId, dynamic hint}) {
|
{required String id,
|
||||||
|
required UuidValue sessionId,
|
||||||
|
required Int32List displays,
|
||||||
|
dynamic hint}) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void sessionTryAddDisplay(
|
|
||||||
{required UuidValue sessionId,
|
|
||||||
required Int32List displays,
|
|
||||||
dynamic hint}) {}
|
|
||||||
|
|
||||||
String sessionAddSync(
|
String sessionAddSync(
|
||||||
{required UuidValue sessionId,
|
{required UuidValue sessionId,
|
||||||
required String id,
|
required String id,
|
||||||
@@ -94,6 +92,14 @@ class RustdeskImpl {
|
|||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<EventToUI> sessionStartWithDisplays(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required String id,
|
||||||
|
required Int32List displays,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool?> sessionGetRemember(
|
Future<bool?> sessionGetRemember(
|
||||||
{required UuidValue sessionId, dynamic hint}) {
|
{required UuidValue sessionId, dynamic hint}) {
|
||||||
return Future(
|
return Future(
|
||||||
@@ -203,12 +209,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}) {
|
int getNextTextureKey({dynamic hint}) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -943,7 +943,7 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
String mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1412,18 +1412,10 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mainStartPa({dynamic hint}) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mainHideDocker({dynamic hint}) {
|
bool mainHideDocker({dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mainHasPixelbufferTextureRender({dynamic hint}) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mainHasFileClipboard({dynamic hint}) {
|
bool mainHasFileClipboard({dynamic hint}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1470,6 +1462,10 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDisableGroupPanel({dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isDisableAccount({dynamic hint}) {
|
bool isDisableAccount({dynamic hint}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1610,5 +1606,21 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mainIsOptionFixed({required String key, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mainGetUseTextureRender({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mainHasValidBotSync({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> mainVerifyBot({required String token, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
|
resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363"
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -790,13 +790,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.18.1"
|
version: "0.19.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.2.4+39
|
version: 1.2.7+46
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -79,7 +79,7 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
||||||
uni_links: ^0.5.1
|
uni_links: ^0.5.1
|
||||||
uni_links_desktop: ^0.1.7
|
uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
|
||||||
path: ^1.8.1
|
path: ^1.8.1
|
||||||
auto_size_text: ^3.0.0
|
auto_size_text: ^3.0.0
|
||||||
bot_toast: ^4.0.3
|
bot_toast: ^4.0.3
|
||||||
@@ -114,6 +114,9 @@ dev_dependencies:
|
|||||||
flutter_lints: ^2.0.2
|
flutter_lints: ^2.0.2
|
||||||
ffigen: ^8.0.2
|
ffigen: ^8.0.2
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
intl: ^0.19.0
|
||||||
|
|
||||||
# rerun: flutter pub run flutter_launcher_icons
|
# rerun: flutter pub run flutter_launcher_icons
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
image_path: "../res/icon.png"
|
image_path: "../res/icon.png"
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ flutter pub get
|
|||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
||||||
# call `flutter clean` if cargo build fails
|
# call `flutter clean` if cargo build fails
|
||||||
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
||||||
cargo build --features "flutter,flutter_texture_render"
|
cargo build --features flutter
|
||||||
flutter run $@
|
flutter run $@
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# RustDesk web
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
### Current and planned
|
|
||||||
|
|
||||||
- [x] Outgoing
|
|
||||||
- [ ] Address book
|
|
||||||
- [ ] Force relay
|
|
||||||
|
|
||||||
### Unsupported
|
|
||||||
|
|
||||||
1. Incoming
|
|
||||||
2. LAN
|
|
||||||
3. Gpu texture render. We use WebGL instead.
|
|
||||||
|
|
||||||
### No plans
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg viewBox="0 0 375 375" style="width:32px;height:32px;margin:0 4px 4px 0" xmlns="http://www.w3.org/2000/svg"><rect transform="matrix(.91553 0 0 .91553 -152.92 116.76)" x="167.03" y="-127.54" width="409.6" height="409.6" rx="64" ry="64" fill="#0071ff"></rect><path d="M150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,19 +0,0 @@
|
|||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY = 'Desktop session not ready';
|
|
||||||
export const LOGIN_MSG_DESKTOP_XSESSION_FAILED = 'Desktop xsession failed';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER = 'Desktop session another user login';
|
|
||||||
export const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND = 'Desktop xorg not found';
|
|
||||||
// ls /usr/share/xsessions/
|
|
||||||
export const LOGIN_MSG_DESKTOP_NO_DESKTOP = 'Desktop none';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY =
|
|
||||||
'Desktop session not ready, password empty';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG =
|
|
||||||
'Desktop session not ready, password wrong';
|
|
||||||
export const LOGIN_MSG_PASSWORD_EMPTY = 'Empty Password';
|
|
||||||
export const LOGIN_MSG_PASSWORD_WRONG = 'Wrong Password';
|
|
||||||
export const LOGIN_MSG_2FA_WRONG = 'Wrong 2FA Code';
|
|
||||||
export const REQUIRE_2FA = '2FA Required';
|
|
||||||
export const LOGIN_MSG_NO_PASSWORD_ACCESS = 'No Password Access';
|
|
||||||
export const LOGIN_MSG_OFFLINE = 'Offline';
|
|
||||||
export const LOGIN_SCREEN_WAYLAND = 'Wayland login screen is not supported';
|
|
||||||
export const SCRAP_X11_REQUIRED = 'x11 expected';
|
|
||||||
export const SCRAP_X11_REF_URL = 'https://rustdesk.com/docs/en/manual/linux/#x11-required';
|
|
||||||
@@ -1,792 +0,0 @@
|
|||||||
import Connection from "./connection";
|
|
||||||
import PORT from "./connection";
|
|
||||||
import _sodium from "libsodium-wrappers";
|
|
||||||
import { CursorData } from "./message";
|
|
||||||
import { loadVp9 } from "./codec";
|
|
||||||
import { checkIfRetry, version } from "./gen_js_from_hbb";
|
|
||||||
import { initZstd, translate } from "./common";
|
|
||||||
import PCMPlayer from "pcm-player";
|
|
||||||
|
|
||||||
window.curConn = undefined;
|
|
||||||
window.isMobile = () => {
|
|
||||||
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
|
||||||
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDesktop() {
|
|
||||||
return !isMobile();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function msgbox(type, title, text, link) {
|
|
||||||
if (!type || (type == 'error' && !text)) return;
|
|
||||||
const text2 = text.toLowerCase();
|
|
||||||
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
|
|
||||||
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, link: link ?? '', hasRetry }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function jsonfyForDart(payload) {
|
|
||||||
var tmp = {};
|
|
||||||
for (const [key, value] of Object.entries(payload)) {
|
|
||||||
if (!key) continue;
|
|
||||||
if (value instanceof String || typeof value == 'string') {
|
|
||||||
tmp[key] = value;
|
|
||||||
} else if (value instanceof Uint8Array) {
|
|
||||||
tmp[key] = '[' + value.toString() + ']';
|
|
||||||
} else {
|
|
||||||
tmp[key] = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pushEvent(name, payload) {
|
|
||||||
payload = jsonfyForDart(payload);
|
|
||||||
payload.name = name;
|
|
||||||
onGlobalEvent(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
let yuvWorker;
|
|
||||||
let yuvCanvas;
|
|
||||||
let gl;
|
|
||||||
let pixels;
|
|
||||||
let flipPixels;
|
|
||||||
let oldSize;
|
|
||||||
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
|
|
||||||
gl = canvas.getContext("webgl");
|
|
||||||
} else {
|
|
||||||
yuvWorker = new Worker("./yuv.js");
|
|
||||||
}
|
|
||||||
let testSpeed = [0, 0];
|
|
||||||
|
|
||||||
export function draw(display, frame) {
|
|
||||||
if (yuvWorker) {
|
|
||||||
// frame's (y/u/v).bytes already detached, can not transferrable any more.
|
|
||||||
yuvWorker.postMessage({ display, frame });
|
|
||||||
} else {
|
|
||||||
var tm0 = new Date().getTime();
|
|
||||||
yuvCanvas.drawFrame(frame);
|
|
||||||
var width = canvas.width;
|
|
||||||
var height = canvas.height;
|
|
||||||
var size = width * height * 4;
|
|
||||||
if (size != oldSize) {
|
|
||||||
pixels = new Uint8Array(size);
|
|
||||||
flipPixels = new Uint8Array(size);
|
|
||||||
oldSize = size;
|
|
||||||
}
|
|
||||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
||||||
const row = width * 4;
|
|
||||||
const end = (height - 1) * row;
|
|
||||||
for (let i = 0; i < size; i += row) {
|
|
||||||
flipPixels.set(pixels.subarray(i, i + row), end - i);
|
|
||||||
}
|
|
||||||
onRgba(display, flipPixels);
|
|
||||||
testSpeed[1] += new Date().getTime() - tm0;
|
|
||||||
testSpeed[0] += 1;
|
|
||||||
if (testSpeed[0] > 30) {
|
|
||||||
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
|
||||||
testSpeed = [0, 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
|
|
||||||
if (testCanvas && currentFrame) {
|
|
||||||
var ctx = testCanvas.getContext("2d");
|
|
||||||
testCanvas.width = frame.format.displayWidth;
|
|
||||||
testCanvas.height = frame.format.displayHeight;
|
|
||||||
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
|
|
||||||
img.data.set(currentFrame);
|
|
||||||
ctx.putImageData(img, 0, 0);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendOffCanvas(c) {
|
|
||||||
let canvas = c.transferControlToOffscreen();
|
|
||||||
yuvWorker.postMessage({ canvas }, [canvas]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setConn(conn) {
|
|
||||||
window.curConn = conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConn() {
|
|
||||||
return window.curConn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startConn(id) {
|
|
||||||
setByName('remote_id', id);
|
|
||||||
await curConn.start(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close() {
|
|
||||||
getConn()?.close();
|
|
||||||
setConn(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function newConn() {
|
|
||||||
window.curConn?.close();
|
|
||||||
const conn = new Connection();
|
|
||||||
setConn(conn);
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sodium;
|
|
||||||
export async function verify(signed, pk) {
|
|
||||||
if (!sodium) {
|
|
||||||
await _sodium.ready;
|
|
||||||
sodium = _sodium;
|
|
||||||
}
|
|
||||||
if (typeof pk == 'string') {
|
|
||||||
pk = decodeBase64(pk);
|
|
||||||
}
|
|
||||||
return sodium.crypto_sign_open(signed, pk);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeBase64(pk) {
|
|
||||||
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genBoxKeyPair() {
|
|
||||||
const pair = sodium.crypto_box_keypair();
|
|
||||||
const sk = pair.privateKey;
|
|
||||||
const pk = pair.publicKey;
|
|
||||||
return [sk, pk];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genSecretKey() {
|
|
||||||
return sodium.crypto_secretbox_keygen();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function seal(unsigned, theirPk, ourSk) {
|
|
||||||
const nonce = Uint8Array.from(Array(24).fill(0));
|
|
||||||
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeOnce(value) {
|
|
||||||
var byteArray = Array(24).fill(0);
|
|
||||||
|
|
||||||
for (var index = 0; index < byteArray.length && value > 0; index++) {
|
|
||||||
var byte = value & 0xff;
|
|
||||||
byteArray[index] = byte;
|
|
||||||
value = (value - byte) / 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uint8Array.from(byteArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function encrypt(unsigned, nonce, key) {
|
|
||||||
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrypt(signed, nonce, key) {
|
|
||||||
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.setByName = (name, value) => {
|
|
||||||
switch (name) {
|
|
||||||
case 'remote_id':
|
|
||||||
localStorage.setItem('remote-id', value);
|
|
||||||
break;
|
|
||||||
case 'connect':
|
|
||||||
newConn();
|
|
||||||
startConn(value);
|
|
||||||
break;
|
|
||||||
case 'login':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.setRemember(value.remember);
|
|
||||||
curConn.login({
|
|
||||||
os_login: {
|
|
||||||
username: value.os_username,
|
|
||||||
password: value.os_password,
|
|
||||||
},
|
|
||||||
password: value.password,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'close':
|
|
||||||
close();
|
|
||||||
break;
|
|
||||||
case 'refresh':
|
|
||||||
curConn.refresh();
|
|
||||||
break;
|
|
||||||
case 'reconnect':
|
|
||||||
curConn?.reconnect();
|
|
||||||
break;
|
|
||||||
case 'toggle_option':
|
|
||||||
curConn.toggleOption(value);
|
|
||||||
break;
|
|
||||||
case 'toggle_privacy_mode':
|
|
||||||
curConn.togglePrivacyMode(value);
|
|
||||||
break;
|
|
||||||
case 'image_quality':
|
|
||||||
curConn.setImageQuality(value);
|
|
||||||
break;
|
|
||||||
case 'lock_screen':
|
|
||||||
curConn.lockScreen();
|
|
||||||
break;
|
|
||||||
case 'ctrl_alt_del':
|
|
||||||
curConn.ctrlAltDel();
|
|
||||||
break;
|
|
||||||
case 'switch_display':
|
|
||||||
curConn.switchDisplay(value);
|
|
||||||
break;
|
|
||||||
case 'remove':
|
|
||||||
const peers = getPeers();
|
|
||||||
delete peers[value];
|
|
||||||
localStorage.setItem('peers', JSON.stringify(peers));
|
|
||||||
break;
|
|
||||||
case 'input_key':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
|
||||||
break;
|
|
||||||
case 'input_string':
|
|
||||||
curConn.inputString(value);
|
|
||||||
break;
|
|
||||||
case 'send_mouse':
|
|
||||||
if (!curConn) return;
|
|
||||||
let mask = 0;
|
|
||||||
value = JSON.parse(value);
|
|
||||||
switch (value.type) {
|
|
||||||
case 'down':
|
|
||||||
mask = 1;
|
|
||||||
break;
|
|
||||||
case 'up':
|
|
||||||
mask = 2;
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
mask = 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (value.buttons) {
|
|
||||||
case 'left':
|
|
||||||
mask |= 1 << 3;
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
mask |= 2 << 3;
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
mask |= 4 << 3;
|
|
||||||
}
|
|
||||||
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
|
||||||
break;
|
|
||||||
case 'send_2fa':
|
|
||||||
curConn.send2fa(value);
|
|
||||||
break;
|
|
||||||
case 'option':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
localStorage.setItem(value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'options':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
for (const [key, value] of Object.entries(value)) {
|
|
||||||
localStorage.setItem(key, value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'option:local':
|
|
||||||
case 'option:flutter:local':
|
|
||||||
case 'option:flutter:peer':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
localStorage.setItem(name + ':' + value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'option:user:default':
|
|
||||||
setUserDefaultOption(value);
|
|
||||||
break;
|
|
||||||
case 'option:session':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.setOption(value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'option:peer':
|
|
||||||
setPeerOption(value);
|
|
||||||
break;
|
|
||||||
case 'option:toggle':
|
|
||||||
return curConn.toggleOption(value);
|
|
||||||
case 'input_os_password':
|
|
||||||
curConn.inputOsPassword(value);
|
|
||||||
break;
|
|
||||||
case 'session_add_sync':
|
|
||||||
return sessionAdd(value);
|
|
||||||
case 'session_start':
|
|
||||||
sessionStart(value);
|
|
||||||
break;
|
|
||||||
case 'session_close':
|
|
||||||
sessionClose(value);
|
|
||||||
break;
|
|
||||||
case 'elevate_with_logon':
|
|
||||||
curConn.elevateWithLogon(value);
|
|
||||||
break;
|
|
||||||
case 'forget':
|
|
||||||
curConn.setRemember(false);
|
|
||||||
break;
|
|
||||||
case 'peer_has_password':
|
|
||||||
const options = getPeers()[value] || {};
|
|
||||||
return (options['password'] ?? '') !== '';
|
|
||||||
case 'peer_exists':
|
|
||||||
return !(!getPeers()[value]);
|
|
||||||
case 'restart':
|
|
||||||
curConn.restart();
|
|
||||||
break;
|
|
||||||
case 'fav':
|
|
||||||
return localStorage.setItem('fav', value);
|
|
||||||
case 'query_onlines':
|
|
||||||
queryOnlines(value);
|
|
||||||
break;
|
|
||||||
case 'change_prefer_codec':
|
|
||||||
curConn.changePreferCodec(value);
|
|
||||||
case 'cursor':
|
|
||||||
setCustomCursor(value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.getByName = (name, arg) => {
|
|
||||||
let v = _getByName(name, arg);
|
|
||||||
if (typeof v == 'string' || v instanceof String) return v;
|
|
||||||
if (v == undefined || v == null) return '';
|
|
||||||
return JSON.stringify(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getByName(name, arg) {
|
|
||||||
switch (name) {
|
|
||||||
case 'remote_id':
|
|
||||||
return localStorage.getItem('remote-id');
|
|
||||||
case 'remember':
|
|
||||||
return curConn.getRemember();
|
|
||||||
case 'toggle_option':
|
|
||||||
return curConn.getOption(arg) || false;
|
|
||||||
case 'option':
|
|
||||||
return localStorage.getItem(arg);
|
|
||||||
case 'options':
|
|
||||||
const keys = [
|
|
||||||
'custom-rendezvous-server',
|
|
||||||
'relay-server',
|
|
||||||
'api-server',
|
|
||||||
'key'
|
|
||||||
];
|
|
||||||
const obj = {};
|
|
||||||
keys.forEach(key => {
|
|
||||||
const v = localStorage.getItem(key);
|
|
||||||
if (v) obj[key] = v;
|
|
||||||
});
|
|
||||||
return JSON.stringify(obj);
|
|
||||||
case 'option:local':
|
|
||||||
case 'option:flutter:local':
|
|
||||||
case 'option:flutter:peer':
|
|
||||||
return localStorage.getItem(name + ':' + arg);
|
|
||||||
case 'image_quality':
|
|
||||||
return curConn.getImageQuality();
|
|
||||||
case 'translate':
|
|
||||||
arg = JSON.parse(arg);
|
|
||||||
return translate(arg.locale, arg.text);
|
|
||||||
case 'option:user:default':
|
|
||||||
return getUserDefaultOption(arg);
|
|
||||||
case 'option:session':
|
|
||||||
if (curConn) {
|
|
||||||
return curConn.getOption(arg);
|
|
||||||
} else {
|
|
||||||
return getUserDefaultOption(arg);
|
|
||||||
}
|
|
||||||
case 'option:peer':
|
|
||||||
return getPeerOption(arg);
|
|
||||||
case 'option:toggle':
|
|
||||||
return curConn.getToggleOption(arg);
|
|
||||||
case 'get_conn_status':
|
|
||||||
if (curConn) {
|
|
||||||
return curConn.getStatus();
|
|
||||||
} else {
|
|
||||||
return JSON.stringify({ status_num: 0 });
|
|
||||||
}
|
|
||||||
case 'test_if_valid_server':
|
|
||||||
break;
|
|
||||||
case 'version':
|
|
||||||
return version;
|
|
||||||
case 'load_recent_peers':
|
|
||||||
loadRecentPeers();
|
|
||||||
break;
|
|
||||||
case 'load_fav_peers':
|
|
||||||
loadFavPeers();
|
|
||||||
break;
|
|
||||||
case 'fav':
|
|
||||||
return localStorage.getItem('fav') ?? '[]';
|
|
||||||
case 'load_recent_peers_sync':
|
|
||||||
return JSON.stringify({
|
|
||||||
peers: JSON.stringify(getRecentPeers())
|
|
||||||
});
|
|
||||||
case 'api_server':
|
|
||||||
return getApiServer();
|
|
||||||
case 'is_using_public_server':
|
|
||||||
return !localStorage.getItem('custom-rendezvous-server');
|
|
||||||
case 'get_version_number':
|
|
||||||
return getVersionNumber(arg);
|
|
||||||
case 'audit_server':
|
|
||||||
return getAuditServer(arg);
|
|
||||||
case 'alternative_codecs':
|
|
||||||
return getAlternativeCodecs();
|
|
||||||
case 'screen_info':
|
|
||||||
return JSON.stringify({
|
|
||||||
frame: {
|
|
||||||
l: window.screenX,
|
|
||||||
t: window.screenY,
|
|
||||||
r: window.screenX + window.innerWidth,
|
|
||||||
b: window.screenY + window.innerHeight,
|
|
||||||
},
|
|
||||||
visibleFrame: {
|
|
||||||
l: window.screen.availLeft,
|
|
||||||
t: window.screen.availTop,
|
|
||||||
r: window.screen.availLeft + window.screen.availWidth,
|
|
||||||
b: window.screen.availTop + window.screen.availHeight,
|
|
||||||
},
|
|
||||||
scaleFactor: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
case 'main_display':
|
|
||||||
return JSON.stringify({
|
|
||||||
w: window.screen.availWidth,
|
|
||||||
h: window.screen.availHeight,
|
|
||||||
scaleFactor: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let opusWorker = new Worker("./libopus.js");
|
|
||||||
let pcmPlayer;
|
|
||||||
|
|
||||||
export function initAudio(channels, sampleRate) {
|
|
||||||
pcmPlayer = newAudioPlayer(channels, sampleRate);
|
|
||||||
opusWorker.postMessage({ channels, sampleRate });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function playAudio(packet) {
|
|
||||||
opusWorker.postMessage(packet, [packet.buffer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.init = async () => {
|
|
||||||
if (yuvWorker) {
|
|
||||||
yuvWorker.onmessage = (e) => {
|
|
||||||
onRgba(e.data.display, e.data.frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opusWorker.onmessage = (e) => {
|
|
||||||
pcmPlayer.feed(e.data);
|
|
||||||
}
|
|
||||||
loadVp9(() => { });
|
|
||||||
await initZstd();
|
|
||||||
console.log('init done');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPeers() {
|
|
||||||
return getJsonObj('peers');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getJsonObj(key) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(key)) || {};
|
|
||||||
} catch (e) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function newAudioPlayer(channels, sampleRate) {
|
|
||||||
return new PCMPlayer({
|
|
||||||
channels,
|
|
||||||
sampleRate,
|
|
||||||
flushingTime: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copyToClipboard(text) {
|
|
||||||
if (window.clipboardData && window.clipboardData.setData) {
|
|
||||||
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
|
|
||||||
return window.clipboardData.setData("Text", text);
|
|
||||||
}
|
|
||||||
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
|
|
||||||
var textarea = document.createElement("textarea");
|
|
||||||
textarea.textContent = text;
|
|
||||||
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
try {
|
|
||||||
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.warn("Copy to clipboard failed.", ex);
|
|
||||||
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dup to the function in hbb_common, lib.rs
|
|
||||||
// Maybe we need to move this function to js part.
|
|
||||||
export function getVersionNumber(v) {
|
|
||||||
try {
|
|
||||||
let versions = v.split('-');
|
|
||||||
|
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
// The first part is the version number.
|
|
||||||
// 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10
|
|
||||||
// to leave space for patch version.
|
|
||||||
if (versions.length > 0) {
|
|
||||||
let last = 0;
|
|
||||||
for (let x of versions[0].split('.')) {
|
|
||||||
last = parseInt(x) || 0;
|
|
||||||
n = n * 1000 + last;
|
|
||||||
}
|
|
||||||
n -= last;
|
|
||||||
n += last * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versions.length > 1) {
|
|
||||||
n += parseInt(versions[1]) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore the rest
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to parse version number: "' + v + '" ' + e.message);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cursor for the flutter-view element
|
|
||||||
function setCustomCursor(value) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
// document querySelector or evaluate can not find the custom element
|
|
||||||
var body = document.body;
|
|
||||||
for (var i = 0; i < body.children.length; i++) {
|
|
||||||
var child = body.children[i];
|
|
||||||
if (child.tagName == 'FLUTTER-VIEW') {
|
|
||||||
child.style.cursor = `url(${obj.url}) ${obj.hotx} ${obj.hoty}, auto`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to set custom cursor: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================== options begin ==========================
|
|
||||||
function setUserDefaultOption(value) {
|
|
||||||
try {
|
|
||||||
const ojb = JSON.parse(value);
|
|
||||||
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
|
|
||||||
userDefaultOptions[ojb.name] = ojb.value;
|
|
||||||
localStorage.setItem('user-default-options', JSON.stringify(userDefaultOptions));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to set user default options: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserDefaultOption(value) {
|
|
||||||
const defaultOptions = {
|
|
||||||
'view_style': 'original',
|
|
||||||
'scroll_style': 'scrollauto',
|
|
||||||
'image_quality': 'balanced',
|
|
||||||
'codec-preference': 'auto',
|
|
||||||
'custom_image_quality': '50',
|
|
||||||
'custom-fps': '30',
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
|
|
||||||
return userDefaultOptions[value] || defaultOptions[value] || '';
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to get user default options: ' + e.message);
|
|
||||||
return defaultOptions[value] || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPeerOption(value) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
const options = getPeers()[obj.id] || {};
|
|
||||||
return options[obj.name] ?? getUserDefaultOption(obj.name);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to get peer option: "' + value + '", ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPeerOption(param) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(param);
|
|
||||||
const id = obj.id;
|
|
||||||
const name = obj.name;
|
|
||||||
const value = obj.value;
|
|
||||||
const peers = getPeers();
|
|
||||||
const options = peers[id] || {};
|
|
||||||
|
|
||||||
if (value == undefined) {
|
|
||||||
delete options[name];
|
|
||||||
} else {
|
|
||||||
options[name] = value;
|
|
||||||
}
|
|
||||||
options["tm"] = new Date().getTime();
|
|
||||||
peers[id] = options;
|
|
||||||
localStorage.setItem("peers", JSON.stringify(peers));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to set peer option: "' + value + '", ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ========================= options end ===========================
|
|
||||||
|
|
||||||
// ========================== peers begin ==========================
|
|
||||||
function getRecentPeers() {
|
|
||||||
const peers = [];
|
|
||||||
for (const [id, value] of Object.entries(getPeers())) {
|
|
||||||
if (!id) continue;
|
|
||||||
const tm = value['tm'];
|
|
||||||
const info = value['info'];
|
|
||||||
const cardInfo = {
|
|
||||||
id: id,
|
|
||||||
username: info['username'] || '',
|
|
||||||
hostname: info['hostname'] || '',
|
|
||||||
platform: info['platform'] || '',
|
|
||||||
alias: value.alias || '',
|
|
||||||
};
|
|
||||||
if (!tm || !cardInfo) continue;
|
|
||||||
peers.push([tm, id, cardInfo]);
|
|
||||||
}
|
|
||||||
return peers.sort().reverse().map(x => x[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadRecentPeers() {
|
|
||||||
const peersRecent = getRecentPeers();
|
|
||||||
if (peersRecent) {
|
|
||||||
onRegisteredEvent(JSON.stringify({ name: 'load_recent_peers', peers: JSON.stringify(peersRecent) }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFavPeers() {
|
|
||||||
try {
|
|
||||||
const fav = localStorage.getItem('fav') ?? '[]';
|
|
||||||
const favs = JSON.parse(fav);
|
|
||||||
const peersFav = getRecentPeers().filter(x => favs.includes(x.id));
|
|
||||||
if (peersFav) {
|
|
||||||
onRegisteredEvent(JSON.stringify({ name: 'load_fav_peers', peers: JSON.stringify(peersFav) }));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load fav peers: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryOnlines(value) {
|
|
||||||
// TODO: implement this
|
|
||||||
}
|
|
||||||
// ========================== peers end ===========================
|
|
||||||
|
|
||||||
// ========================== session begin ==========================
|
|
||||||
function sessionAdd(value) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(value);
|
|
||||||
window.curConn?.close();
|
|
||||||
const conn = new Connection();
|
|
||||||
setConn(conn);
|
|
||||||
return '';
|
|
||||||
} catch (e) {
|
|
||||||
return e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionStart(value) {
|
|
||||||
try {
|
|
||||||
const conn = getConn();
|
|
||||||
if (!conn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(value);
|
|
||||||
if (data['id']) {
|
|
||||||
startConn(data['id']);
|
|
||||||
} else {
|
|
||||||
msgbox('error', 'Error', 'No id found in session data ' + value, '');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: better error handling
|
|
||||||
msgbox('error', 'Error', e.message, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionClose(value) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
// ========================== session end ===========================
|
|
||||||
|
|
||||||
// ========================== settings begin ==========================
|
|
||||||
function increasePort(host, offset) {
|
|
||||||
function isIPv6(str) {
|
|
||||||
const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$/;
|
|
||||||
return ipv6Pattern.test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isIPv6(host)) {
|
|
||||||
if (host.startsWith('[')) {
|
|
||||||
let tmp = host.split(']:');
|
|
||||||
if (tmp.length === 2) {
|
|
||||||
let port = parseInt(tmp[1]) || 0;
|
|
||||||
if (port > 0) {
|
|
||||||
return `${tmp[0]}]:${port + offset}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (host.includes(':')) {
|
|
||||||
let tmp = host.split(':');
|
|
||||||
if (tmp.length === 2) {
|
|
||||||
let port = parseInt(tmp[1]) || 0;
|
|
||||||
if (port > 0) {
|
|
||||||
return `${tmp[0]}:${port + offset}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlternativeCodecs() {
|
|
||||||
return JSON.stringify({
|
|
||||||
vp8: true,
|
|
||||||
av1: false,
|
|
||||||
h264: false,
|
|
||||||
h265: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ========================== settings end ===========================
|
|
||||||
|
|
||||||
// ========================== server begin ==========================
|
|
||||||
function getApiServer() {
|
|
||||||
const api_server = localStorage.getItem('api-server');
|
|
||||||
if (api_server) {
|
|
||||||
return api_server;
|
|
||||||
}
|
|
||||||
|
|
||||||
const custom_rendezvous_server = localStorage.getItem('custom-rendezvous-server');
|
|
||||||
if (custom_rendezvous_server) {
|
|
||||||
let s = increasePort(custom_rendezvous_server, -2);
|
|
||||||
if (s == custom_rendezvous_server) {
|
|
||||||
return `http://${s}:${PORT - 2}`;
|
|
||||||
} else {
|
|
||||||
return `http://${s}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'https://admin.rustdesk.com';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuditServer(typ) {
|
|
||||||
if (!localStorage.getItem("access_token")) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const api_server = getApiServer();
|
|
||||||
if (!api_server || api_server.includes('rustdesk.com')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return api_server + '/api/audit/' + typ;
|
|
||||||
}
|
|
||||||
// ========================== server end ============================
|
|
||||||
1
flutter/web/v1/README.md
Normal file
1
flutter/web/v1/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v1 is not compatible with current Flutter source code.
|
||||||
@@ -3,19 +3,19 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "python ./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && python ./ts_proto.py && tsc && vite build",
|
"build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4",
|
||||||
"vite": "2.8"
|
"vite": "^2.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-sha256": "^1.3.0",
|
"fast-sha256": "^1.3.0",
|
||||||
"libsodium": "^0.7.9",
|
"libsodium": "^0.7.9",
|
||||||
"libsodium-wrappers": "^0.7.9",
|
"libsodium-wrappers": "^0.7.9",
|
||||||
"pcm-player": "^0.0.11",
|
"pcm-player": "^0.0.11",
|
||||||
"ts-proto": "^1.169.1",
|
"ts-proto": "^1.101.0",
|
||||||
"wasm-feature-detect": "^1.2.11",
|
"wasm-feature-detect": "^1.2.11",
|
||||||
"zstddec": "^0.0.2"
|
"zstddec": "^0.0.2"
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export async function loadVp9(callback) {
|
|||||||
// Multithreading is used only if `options.threading` is true.
|
// Multithreading is used only if `options.threading` is true.
|
||||||
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
|
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
|
||||||
// currently available in Firefox and Chrome with experimental flags enabled.
|
// currently available in Firefox and Chrome with experimental flags enabled.
|
||||||
// All major browsers disabled SharedArrayBuffer by default on January 5, 2018
|
// 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer
|
||||||
const isSIMD = await simd();
|
const isSIMD = await simd();
|
||||||
console.log('isSIMD: ' + isSIMD);
|
console.log('isSIMD: ' + isSIMD);
|
||||||
window.OGVLoader.loadClass(
|
window.OGVLoader.loadClass(
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user