mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 22:11:30 +08:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d06de00fb | ||
|
|
6f8af9d114 | ||
|
|
0a672f092a | ||
|
|
ef62f1db29 | ||
|
|
7f804a0e45 | ||
|
|
b2dff336ce | ||
|
|
a6571e71e4 | ||
|
|
81f711eb00 | ||
|
|
c8a8e06558 | ||
|
|
2c079f53a9 | ||
|
|
322ffe288e | ||
|
|
c340eb0e57 | ||
|
|
4e953291ed | ||
|
|
1dea5fee0e | ||
|
|
9f24b46fee | ||
|
|
0808c41a1c | ||
|
|
296c6df462 | ||
|
|
13ee3e907d | ||
|
|
ce7d794b4c | ||
|
|
fb10069632 | ||
|
|
43a7677644 | ||
|
|
58fa32d7ea | ||
|
|
934d6c3987 | ||
|
|
2d7c6ef21f | ||
|
|
99a97e6a6c | ||
|
|
017a10e8c8 | ||
|
|
41ffa8ba08 | ||
|
|
e029d00cfa | ||
|
|
268534d5e7 | ||
|
|
a7d2bc63f9 | ||
|
|
559115c43c | ||
|
|
1277c7d60c | ||
|
|
9b69c7e972 | ||
|
|
a903f710ea | ||
|
|
b75f4daa47 | ||
|
|
fef44ffa57 | ||
|
|
5a812e3b2f | ||
|
|
910dcf2036 | ||
|
|
44a28aa5bd | ||
|
|
f7f947beb9 | ||
|
|
d03a9e2baf | ||
|
|
ca22316e95 | ||
|
|
ef99c479aa | ||
|
|
fa9260c763 | ||
|
|
fab11c8ffa | ||
|
|
9bd9658a92 | ||
|
|
213880c14d | ||
|
|
0550397046 | ||
|
|
f7a5a506f6 | ||
|
|
0f34c50bd2 | ||
|
|
055826e26f | ||
|
|
a30582c840 | ||
|
|
d106d97b99 | ||
|
|
d4410e78e2 | ||
|
|
e3fcc6cce3 | ||
|
|
265d08fc3b | ||
|
|
7c8329c5c6 | ||
|
|
d3947c9a19 | ||
|
|
cd99331668 | ||
|
|
472e18b10a | ||
|
|
d443f5de28 | ||
|
|
3242d132f6 | ||
|
|
e66d2facd4 | ||
|
|
f15b8dc0da | ||
|
|
3275824aec | ||
|
|
965cb704ec | ||
|
|
938e165470 | ||
|
|
9058ef3344 | ||
|
|
ed39cc3038 | ||
|
|
a77752c4cb | ||
|
|
c9940957f0 | ||
|
|
c90d72d720 | ||
|
|
2c30bd9d24 | ||
|
|
f2dc8e21a8 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-22.04 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
|
||||
14
.github/workflows/flutter-build.yml
vendored
14
.github/workflows/flutter-build.yml
vendored
@@ -39,7 +39,7 @@ env:
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.3"
|
||||
VERSION: "1.4.4"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -1001,6 +1001,8 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# Increase Gradle JVM memory for CI builds
|
||||
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
case ${{ matrix.job.target }} in
|
||||
@@ -1208,6 +1210,8 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# Increase Gradle JVM memory for CI builds
|
||||
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
@@ -1443,7 +1447,8 @@ jobs:
|
||||
rpm \
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils
|
||||
xz-utils \
|
||||
libssl-dev
|
||||
# we have libopus compiled by us.
|
||||
apt-get remove -y libopus-dev || true
|
||||
# output devs
|
||||
@@ -1723,12 +1728,13 @@ jobs:
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils \
|
||||
zip
|
||||
zip \
|
||||
libssl-dev
|
||||
# arm-linux needs CMake and vcokg built from source as there
|
||||
# are no prebuilts available from Kitware and Microsoft
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
# install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
|
||||
apt-get install -y gcc-8 g++-8 libssl-dev
|
||||
apt-get install -y gcc-8 g++-8
|
||||
# bootstrap CMake amd add it to PATH
|
||||
git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
|
||||
pushd /tmp/cmake
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.3"
|
||||
VERSION: "1.4.4"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: "1.4.3"
|
||||
release-tag: "1.4.3"
|
||||
version: "1.4.4"
|
||||
release-tag: "1.4.4"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
439
Cargo.lock
generated
439
Cargo.lock
generated
@@ -328,13 +328,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.11"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5"
|
||||
checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
@@ -936,18 +936,43 @@ dependencies = [
|
||||
"thiserror 1.0.61",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"polling 3.7.2",
|
||||
"rustix 1.1.2",
|
||||
"slab",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
|
||||
dependencies = [
|
||||
"calloop",
|
||||
"calloop 0.13.0",
|
||||
"rustix 0.38.34",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
|
||||
dependencies = [
|
||||
"calloop 0.14.3",
|
||||
"rustix 1.1.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.13"
|
||||
@@ -1269,6 +1294,23 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -2299,9 +2341,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -3320,6 +3362,7 @@ name = "hbb_common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"backtrace",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -3345,12 +3388,14 @@ dependencies = [
|
||||
"protobuf-codegen",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde 1.0.203",
|
||||
"serde_derive",
|
||||
"serde_json 1.0.118",
|
||||
"sha2",
|
||||
"smithay-client-toolkit 0.20.0",
|
||||
"socket2 0.3.19",
|
||||
"sodiumoxide",
|
||||
"sysinfo",
|
||||
@@ -3358,13 +3403,14 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-socks 0.5.2-3",
|
||||
"tokio-socks",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"toml 0.7.8",
|
||||
"tungstenite",
|
||||
"url",
|
||||
"uuid",
|
||||
"webpki-roots 1.0.4",
|
||||
"whoami",
|
||||
"winapi 0.3.9",
|
||||
"zstd 0.13.1",
|
||||
@@ -3506,7 +3552,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#17c1dbb38450fe4a64aeba78fb50bec32f364a16"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -3518,18 +3564,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
@@ -3537,9 +3585,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.6"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
@@ -3550,7 +3598,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.0",
|
||||
"webpki-roots 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3571,17 +3619,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.12"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
|
||||
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
@@ -3750,9 +3802,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.9.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde 1.0.203",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
@@ -3872,7 +3934,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -4154,6 +4216,12 @@ version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
@@ -4299,12 +4367,6 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -4642,7 +4704,7 @@ dependencies = [
|
||||
"nokhwa-bindings-windows",
|
||||
"nokhwa-core",
|
||||
"paste",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4690,7 +4752,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"image 0.25.1",
|
||||
"mozjpeg",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5215,6 +5277,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.5.3+3.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.104"
|
||||
@@ -5223,6 +5294,7 @@ checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -5927,18 +5999,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.34.0"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases 0.2.1",
|
||||
@@ -5948,7 +6020,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -5956,9 +6028,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
@@ -5969,7 +6041,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -5977,9 +6049,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.12"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
@@ -6338,8 +6410,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.15"
|
||||
source = "git+https://github.com/rustdesk-org/reqwest#9e859438203a71eb86ddc294fbebfde14cba7f7c"
|
||||
version = "0.12.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.22.1",
|
||||
@@ -6354,18 +6427,14 @@ dependencies = [
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde 1.0.203",
|
||||
"serde_json 1.0.118",
|
||||
@@ -6374,16 +6443,15 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-socks 0.5.2",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.9",
|
||||
"windows-registry",
|
||||
"webpki-roots 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6527,7 +6595,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -6584,6 +6652,7 @@ dependencies = [
|
||||
"objc",
|
||||
"objc_id",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"os-version",
|
||||
"pam",
|
||||
"parity-tokio-ipc",
|
||||
@@ -6642,7 +6711,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -6696,10 +6765,23 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.26"
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -6719,16 +6801,7 @@ dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework 3.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
"security-framework 3.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6742,9 +6815,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.5.1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys 0.8.7",
|
||||
@@ -6755,7 +6828,7 @@ dependencies = [
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.2.0",
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -6763,15 +6836,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.1"
|
||||
version = "0.103.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
|
||||
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -6871,6 +6944,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"webm",
|
||||
"winapi 0.3.9",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6882,7 +6956,7 @@ dependencies = [
|
||||
"ab_glyph",
|
||||
"log",
|
||||
"memmap2",
|
||||
"smithay-client-toolkit",
|
||||
"smithay-client-toolkit 0.19.2",
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
@@ -6901,9 +6975,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.2.0"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.10.1",
|
||||
@@ -6914,9 +6988,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.14.0"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys 0.8.7",
|
||||
"libc",
|
||||
@@ -7200,8 +7274,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"calloop 0.13.0",
|
||||
"calloop-wayland-source 0.3.0",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -7218,6 +7292,33 @@ dependencies = [
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"calloop 0.14.3",
|
||||
"calloop-wayland-source 0.4.1",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
"memmap2",
|
||||
"rustix 1.1.2",
|
||||
"thiserror 2.0.17",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-csd-frame",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-experimental",
|
||||
"wayland-protocols-misc",
|
||||
"wayland-protocols-wlr",
|
||||
"wayland-scanner",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.2.2"
|
||||
@@ -7709,11 +7810,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.11",
|
||||
"thiserror-impl 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7729,9 +7830,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.93",
|
||||
"quote 1.0.36",
|
||||
@@ -7927,23 +8028,11 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-socks"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"futures-util",
|
||||
"thiserror 1.0.61",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.26.2"
|
||||
@@ -8079,6 +8168,24 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -8097,6 +8204,7 @@ version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -8252,7 +8360,7 @@ dependencies = [
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.17",
|
||||
"utf-8",
|
||||
"webpki-roots 0.26.9",
|
||||
]
|
||||
@@ -8701,13 +8809,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.6"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993"
|
||||
checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix 0.38.34",
|
||||
"rustix 1.1.2",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
@@ -8715,12 +8823,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.5"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943"
|
||||
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"rustix 0.38.34",
|
||||
"rustix 1.1.2",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
@@ -8749,9 +8857,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.3"
|
||||
version = "0.32.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa"
|
||||
checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"wayland-backend",
|
||||
@@ -8759,6 +8867,32 @@ dependencies = [
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-experimental"
|
||||
version = "20250721.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-misc"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.3"
|
||||
@@ -8787,20 +8921,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.4"
|
||||
version = "0.31.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6"
|
||||
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.93",
|
||||
"quick-xml 0.34.0",
|
||||
"quick-xml 0.37.5",
|
||||
"quote 1.0.36",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.4"
|
||||
version = "0.31.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
|
||||
checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
@@ -8846,9 +8980,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "0.26.8"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4"
|
||||
checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -8864,9 +8998,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.0"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -9191,17 +9325,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
@@ -9315,29 +9438,13 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
"windows_i686_gnullvm 0.53.0",
|
||||
"windows_i686_msvc 0.53.0",
|
||||
"windows_x86_64_gnu 0.53.0",
|
||||
"windows_x86_64_gnullvm 0.53.0",
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.1"
|
||||
@@ -9374,12 +9481,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -9410,12 +9511,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.32.0"
|
||||
@@ -9446,24 +9541,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -9494,12 +9577,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.32.0"
|
||||
@@ -9530,12 +9607,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -9554,12 +9625,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -9590,12 +9655,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winit"
|
||||
version = "0.30.9"
|
||||
@@ -9608,7 +9667,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
"bytemuck",
|
||||
"calloop",
|
||||
"calloop 0.13.0",
|
||||
"cfg_aliases 0.2.1",
|
||||
"concurrent-queue",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -9630,7 +9689,7 @@ dependencies = [
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.34",
|
||||
"sctk-adwaita",
|
||||
"smithay-client-toolkit",
|
||||
"smithay-client-toolkit 0.19.2",
|
||||
"smol_str",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -83,6 +83,8 @@ shutdown_hooks = "0.1"
|
||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||
stunclient = "0.4"
|
||||
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"}
|
||||
reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
|
||||
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||
@@ -165,13 +167,6 @@ fontdb = "0.23"
|
||||
bytemuck = "1.23"
|
||||
ttf-parser = "0.25"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||
@@ -192,6 +187,9 @@ termios = "0.3"
|
||||
terminfo = "0.8"
|
||||
winit = "0.30"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
jni = "0.21"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</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>] | [<a href="docs/README-NO.md">Norsk</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>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
|
||||
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.3
|
||||
version: 1.4.4
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.3
|
||||
version: 1.4.4
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
85
docs/CODE_OF_CONDUCT-RO.md
Normal file
85
docs/CODE_OF_CONDUCT-RO.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Codul de Conduită al Contributorilor
|
||||
|
||||
## Angajamentul Nostru
|
||||
|
||||
Noi, ca membri, contribuitori și lideri, ne angajăm să facem ca participarea în comunitatea noastră să fie o experiență fără hărțuire pentru toată lumea, indiferent de vârstă, dimensiunea corpului, dizabilități vizibile sau invizibile, etnie, caracteristici sexuale, identitate și exprimare de gen, nivel de experiență, educație, statut socio-economic, naționalitate, aspect personal, rasă, religie sau identitate și orientare sexuală.
|
||||
|
||||
Ne angajăm să acționăm și să interacționăm în moduri care contribuie la o comunitate deschisă, primitoare, diversă, incluzivă și sănătoasă.
|
||||
|
||||
## Standardele Noastre
|
||||
|
||||
Exemple de comportamente care contribuie la un mediu pozitiv pentru comunitatea noastră includ:
|
||||
|
||||
* Demonstrarea empatiei și a bunătății față de ceilalți
|
||||
* Respectarea opiniilor, punctelor de vedere și experiențelor diferite
|
||||
* Oferirea și acceptarea cu grație a feedback-ului constructiv
|
||||
* Asumarea responsabilității și cererea de scuze celor afectați de greșelile noastre și învățarea din experiență
|
||||
* Concentrarea pe ceea ce este cel mai bun nu doar pentru noi ca indivizi, ci pentru întreaga comunitate
|
||||
|
||||
Exemple de comportamente inacceptabile includ:
|
||||
|
||||
* Utilizarea limbajului sau imaginilor sexualizate, precum și atenția sau avansurile sexuale de orice fel
|
||||
* Trollare, insulte sau comentarii denigratoare și atacuri personale sau politice
|
||||
* Hărțuire publică sau privată
|
||||
* Publicarea informațiilor private ale altora, cum ar fi adresa fizică sau de e-mail, fără permisiunea explicită
|
||||
* Alte comportamente care ar putea fi considerate inadecvate într-un cadru profesional
|
||||
|
||||
## Responsabilități de Aplicare
|
||||
|
||||
Liderii comunității sunt responsabili pentru clarificarea și aplicarea standardelor noastre de comportament acceptabil și vor lua măsuri corective adecvate și echitabile ca răspuns la orice comportament pe care îl consideră inadecvat, amenințător, ofensator sau dăunător.
|
||||
|
||||
Liderii comunității au dreptul și responsabilitatea de a elimina, edita sau respinge comentarii, commit-uri, cod, editări wiki, probleme și alte contribuții care nu se aliniază acestui Cod de Conduită și vor comunica motivele pentru deciziile de moderare atunci când este cazul.
|
||||
|
||||
## Domeniu de Aplicare
|
||||
|
||||
Acest Cod de Conduită se aplică în toate spațiile comunității și se aplică și atunci când un individ reprezintă oficial comunitatea în spații publice.
|
||||
Exemple de reprezentare a comunității includ utilizarea unei adrese de e-mail oficiale, postarea printr-un cont oficial de social media sau acționarea ca reprezentant desemnat la un eveniment online sau offline.
|
||||
|
||||
## Aplicare
|
||||
|
||||
Cazurile de comportament abuziv, hărțuitor sau altfel inacceptabil pot fi raportate liderilor comunității responsabili pentru aplicare la [info@rustdesk.com](mailto:info@rustdesk.com).
|
||||
Toate plângerile vor fi revizuite și investigate prompt și corect.
|
||||
|
||||
Toți liderii comunității sunt obligați să respecte confidențialitatea și securitatea persoanei care raportează orice incident.
|
||||
|
||||
## Ghiduri de Aplicare
|
||||
|
||||
Liderii comunității vor urma aceste Ghiduri privind Impactul Comunității pentru a stabili consecințele pentru orice acțiune pe care o consideră o încălcare a acestui Cod de Conduită:
|
||||
|
||||
### 1. Corectare
|
||||
|
||||
**Impact asupra comunității**: Utilizarea limbajului neadecvat sau alte comportamente considerate neprofesionale sau nedorite în comunitate.
|
||||
|
||||
**Consecință**: O avertizare scrisă și privată din partea liderilor comunității, oferind claritate asupra naturii încălcării și o explicație despre motivul pentru care comportamentul a fost inadecvat. Poate fi cerută o scuză publică.
|
||||
|
||||
### 2. Avertisment
|
||||
|
||||
**Impact asupra comunității**: Încălcare printr-un incident singular sau o serie de acțiuni.
|
||||
|
||||
**Consecință**: Un avertisment cu consecințe pentru continuarea comportamentului. Nicio interacțiune cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, pentru o perioadă specificată. Aceasta include evitarea interacțiunilor în spațiile comunității, precum și pe canale externe, cum ar fi rețelele sociale. Încălcarea acestor termeni poate duce la o suspendare temporară sau permanentă.
|
||||
|
||||
### 3. Suspendare Temporară
|
||||
|
||||
**Impact asupra comunității**: O încălcare serioasă a standardelor comunității, inclusiv comportament neadecvat susținut.
|
||||
|
||||
**Consecință**: Suspendare temporară de la orice tip de interacțiune sau comunicare publică cu comunitatea pentru o perioadă specificată. Nicio interacțiune publică sau privată cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, nu este permisă în această perioadă. Încălcarea acestor termeni poate duce la o interdicție permanentă.
|
||||
|
||||
### 4. Interdicție Permanentă
|
||||
|
||||
**Impact asupra comunității**: Demonstrând un tipar de încălcare a standardelor comunității, inclusiv comportament neadecvat susținut, hărțuire a unei persoane sau agresiune față de sau denigrare a unor grupuri de persoane.
|
||||
|
||||
**Consecință**: Interdicție permanentă de la orice tip de interacțiune publică în cadrul comunității.
|
||||
|
||||
## Atribuire
|
||||
|
||||
Acest Cod de Conduită este adaptat din [Contributor Covenant][homepage], versiunea 2.0, disponibil la [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Ghidurile privind Impactul Comunității au fost inspirate de [scara de aplicare a codului de conduită Mozilla][Mozilla CoC].
|
||||
|
||||
Pentru răspunsuri la întrebări frecvente despre acest cod de conduită, vezi FAQ la [https://www.contributor-covenant.org/faq][FAQ]. Traduceri sunt disponibile la [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
31
docs/CONTRIBUTING-RO.md
Normal file
31
docs/CONTRIBUTING-RO.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contribuții la RustDesk
|
||||
|
||||
RustDesk primește cu plăcere contribuții din partea tuturor. Iată ghidurile dacă te gândești să ne ajuți:
|
||||
|
||||
## Contribuții
|
||||
|
||||
Contribuțiile la RustDesk sau la dependențele sale ar trebui făcute sub forma de pull request-uri pe GitHub. Fiecare pull request va fi revizuit de un contributor principal (cineva cu permisiunea de a aplica patch-uri) și fie va fi integrat în arborele principal, fie vor fi oferite sugestii pentru modificările necesare. Toate contribuțiile trebuie să urmeze acest format, chiar și cele ale contributorilor principali.
|
||||
|
||||
Dacă dorești să lucrezi la o problemă, te rugăm să o revendici mai întâi comentând pe GitHub issue-ul pe care vrei să lucrezi. Aceasta previne eforturi duplicate din partea contributorilor asupra aceleiași probleme.
|
||||
|
||||
## Lista de verificare pentru Pull Request
|
||||
|
||||
- Creează un branch din branch-ul `master` și, dacă este necesar, fă rebase la branch-ul `master` curent înainte de a trimite pull request-ul. Dacă nu se poate integra curat cu `master`, ți se poate cere să faci rebase la modificările tale.
|
||||
|
||||
- Commit-urile ar trebui să fie cât mai mici posibil, asigurând totodată că fiecare commit este corect independent (adică fiecare commit ar trebui să compileze și să treacă testele).
|
||||
|
||||
- Commit-urile trebuie să fie însoțite de un semnătura Developer Certificate of Origin (http://developercertificate.org), care indică faptul că tu (și angajatorul tău, dacă este cazul) ești de acord să respecți termenii [licenței proiectului](../LICENCE). În git, aceasta este opțiunea `-s` la `git commit`.
|
||||
|
||||
- Dacă patch-ul tău nu este revizuit sau ai nevoie ca o anumită persoană să-l revizuiască, poți @-reply unui reviewer cerând o revizuire în pull request sau într-un comentariu, sau poți solicita o revizuire prin [email](mailto:info@rustdesk.com).
|
||||
|
||||
- Adaugă teste relevante pentru bug-ul corectat sau pentru funcționalitatea nouă.
|
||||
|
||||
Pentru instrucțiuni specifice git, vezi [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## Conduită
|
||||
|
||||
[Codul de Conduită RustDesk](https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md)
|
||||
|
||||
## Comunicare
|
||||
|
||||
Contributorii RustDesk frecventează [Discord](https://discord.gg/nDceKgxnkV).
|
||||
181
docs/README-RO.md
Normal file
181
docs/README-RO.md
Normal file
@@ -0,0 +1,181 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - desktopul tău la distanță"><br>
|
||||
<a href="../README.md#raw-steps-to-build">Construire</a> •
|
||||
<a href="../README.md#how-to-build-with-docker">Docker</a> •
|
||||
<a href="../README.md#file-structure">Structură</a> •
|
||||
<a href="../README.md#snapshot">Capturi</a><br>
|
||||
[<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>] | [<a href="README-RO.md">Română</a>]<br>
|
||||
<b>Avem nevoie de ajutorul tău pentru a traduce acest README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> și <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> în limba ta maternă</b>
|
||||
</p>
|
||||
|
||||
> [!Atenție]
|
||||
> **Declinare de responsabilitate privind utilizarea abuzivă:** <br>
|
||||
> Dezvoltatorii RustDesk nu susțin sau aprobă utilizarea neetică sau ilegală a acestui software. Utilizarea abuzivă, cum ar fi accesul neautorizat, controlul sau invadarea intimității, este strict împotriva regulilor noastre. Autorii nu sunt responsabili pentru utilizarea necorespunzătoare a aplicației.
|
||||
|
||||
|
||||
Conversați cu noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Încă o soluție de desktop la distanță scrisă în Rust. Funcționează imediat, fără configurare necesară. Ai control total asupra datelor tale, fără probleme de securitate. Poți folosi serverul nostru de rendezvous/relay, [să-ți configurezi propriul server](https://rustdesk.com/server) sau [să scrii propriul server de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk primește contribuții de la oricine. Vezi [CONTRIBUTING.md](../docs/CONTRIBUTING.md) pentru ajutor la început.
|
||||
|
||||
[**ÎNTREBĂRI FRECVENTE (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**DESCĂRCARE BINARE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**BUILD NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
[<img src="https://flathub.org/api/badge?svg&locale=en"
|
||||
alt="Get it on Flathub"
|
||||
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
|
||||
|
||||
## Dependențe
|
||||
|
||||
Versiunile desktop folosesc Flutter sau Sciter (depreciat) pentru interfață; acest ghid este pentru Sciter doar, deoarece este mai ușor și mai prietenos pentru început. Vezi [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) pentru construire cu Flutter.
|
||||
|
||||
Te rugăm să descarci singur librăria dinamică Sciter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Pași pentru construire (Raw Steps to build)
|
||||
|
||||
- Pregătește mediul de dezvoltare Rust și mediul de construire C++
|
||||
|
||||
- Instalează [vcpkg](https://github.com/microsoft/vcpkg) și setează corect variabila de mediu `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
|
||||
|
||||
- rulează `cargo run`
|
||||
|
||||
## [Construire](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Cum se construiește pe Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Instalează vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2023.04.15
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Repară libvpx (Pentru Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Cum să construiești cu Docker
|
||||
|
||||
Începe prin clonarea repository-ului și construirea imaginii Docker:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Apoi, de fiecare dată când trebuie să construiești aplicația, rulează comanda următoare:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Reține că prima construire poate dura mai mult până când dependențele sunt în cache; construirile ulterioare vor fi mai rapide. De asemenea, dacă trebuie să specifici argumente diferite comenzii de build, le poți adăuga la finalul comenzii în poziția `<OPTIONAL-ARGS>`. De exemplu, pentru a construi o versiune optimizată de release, adaugă `--release`. Executabilul rezultat va fi disponibil în folderul `target` pe sistemul tău, și poate fi rulat cu:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Sau, dacă rulezi un executabil release:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, altfel aplicația poate să nu găsească resursele necesare. De asemenea, reține că alte subcomenzi cargo, cum ar fi `install` sau `run`, nu sunt acceptate în prezent prin această metodă, deoarece ar instala sau rula programul în interiorul containerului în loc de gazdă.
|
||||
|
||||
## Structura fișierelor
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funcții fs pentru transfer de fișiere și alte funcții utilitare
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: capturare ecran
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control tastatură/mouse specific platformei
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementare copy/paste pentru fișiere pentru Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interfață Sciter învechită (depreciată)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servicii audio/clipboard/input/video și conexiuni de rețea
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inițiază o conexiune peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
|
||||
|
||||
## Capturi de ecran
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
9
docs/SECURITY-RO.md
Normal file
9
docs/SECURITY-RO.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Politica de Securitate
|
||||
|
||||
## Raportarea unei Vulnerabilități
|
||||
|
||||
Acordăm o mare importanță securității proiectului. Încurajăm toți utilizatorii să ne raporteze orice vulnerabilități pe care le descoperă.
|
||||
Dacă găsești o vulnerabilitate de securitate în proiectul RustDesk, te rugăm să o raportezi responsabil trimițând un e-mail la info@rustdesk.com.
|
||||
|
||||
În acest moment, nu avem un program de recompense pentru descoperirea de bug-uri. Suntem o echipă mică care încearcă să rezolve o problemă mare.
|
||||
Te rugăm să raportezi orice vulnerabilitate în mod responsabil, astfel încât să putem continua să construim o aplicație sigură pentru întreaga comunitate.
|
||||
@@ -55,8 +55,7 @@
|
||||
],
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--filesystem=home",
|
||||
"--device=dri",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id "com.google.protobuf" version "0.9.4"
|
||||
id "com.android.application"
|
||||
@@ -30,8 +32,37 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
// Add rustls-platform-verifier Android support
|
||||
String findRustlsPlatformVerifierMavenDir() {
|
||||
def dependencyText = providers.exec {
|
||||
it.workingDir = new File("../..")
|
||||
commandLine("cargo", "metadata", "--format-version", "1")
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
def dependencyJson = new JsonSlurper().parseText(dependencyText)
|
||||
def pkg = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }
|
||||
|
||||
if (pkg == null) {
|
||||
throw new GradleException("rustls-platform-verifier-android package not found in cargo metadata!")
|
||||
}
|
||||
|
||||
def manifestPath = file(pkg.manifest_path)
|
||||
def mavenDir = new File(manifestPath.parentFile, "maven")
|
||||
|
||||
if (!mavenDir.exists()) {
|
||||
throw new GradleException("Maven directory not found at: ${mavenDir.path}")
|
||||
}
|
||||
|
||||
println("✓ Found rustls-platform-verifier maven repo at: ${mavenDir.path}")
|
||||
return mavenDir.path
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = findRustlsPlatformVerifierMavenDir()
|
||||
metadataSources.artifact()
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -67,7 +98,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.carriez.flutter_hbb"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
@@ -97,8 +128,10 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation "rustls:rustls-platform-verifier:0.1.1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Keep class members from protobuf generated code.
|
||||
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
|
||||
# Keep rustls-platform-verifier classes for JNI
|
||||
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
|
||||
@@ -23,6 +23,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="RustDesk"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
|
||||
@@ -62,7 +62,13 @@ class MainActivity : FlutterActivity() {
|
||||
channelTag
|
||||
)
|
||||
initFlutterChannel(flutterMethodChannel!!)
|
||||
thread { setCodecInfo() }
|
||||
thread {
|
||||
try {
|
||||
setCodecInfo()
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to setCodecInfo: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import ffi.FFI
|
||||
|
||||
class MainApplication : Application() {
|
||||
companion object {
|
||||
private const val TAG = "MainApplication"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "App start")
|
||||
FFI.onAppStart(applicationContext)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ object FFI {
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun onAppStart(ctx: Context)
|
||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -60,6 +62,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -44,7 +44,7 @@ import 'package:flutter_hbb/native/win32.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
|
||||
import 'package:flutter_hbb/native/common.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
|
||||
final globalKey = GlobalKey<NavigatorState>();
|
||||
final navigationBarKey = GlobalKey();
|
||||
@@ -1681,13 +1681,12 @@ class LastWindowPosition {
|
||||
this.offsetHeight, this.isMaximized, this.isFullscreen);
|
||||
|
||||
bool equals(LastWindowPosition other) {
|
||||
return (
|
||||
(width == other.width) &&
|
||||
(height == other.height) &&
|
||||
(offsetWidth == other.offsetWidth) &&
|
||||
(offsetHeight == other.offsetHeight) &&
|
||||
(isMaximized == other.isMaximized) &&
|
||||
(isFullscreen == other.isFullscreen));
|
||||
return ((width == other.width) &&
|
||||
(height == other.height) &&
|
||||
(offsetWidth == other.offsetWidth) &&
|
||||
(offsetHeight == other.offsetHeight) &&
|
||||
(isMaximized == other.isMaximized) &&
|
||||
(isFullscreen == other.isFullscreen));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -1736,22 +1735,29 @@ final Debouncer _saveWindowDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||
|
||||
/// Save window position and size on exit
|
||||
/// Note that windowId must be provided if it's subwindow
|
||||
Future<void> saveWindowPosition(WindowType type, {int? windowId, bool? flush}) async {
|
||||
Future<void> saveWindowPosition(WindowType type,
|
||||
{int? windowId, bool? flush}) async {
|
||||
if (type != WindowType.Main && windowId == null) {
|
||||
debugPrint(
|
||||
"Error: windowId cannot be null when saving positions for sub window");
|
||||
}
|
||||
|
||||
late Offset position;
|
||||
late Size sz;
|
||||
Offset? position;
|
||||
Size? sz;
|
||||
late bool isMaximized;
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||
|
||||
setPreFrame() {
|
||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
position = Offset(
|
||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||
if (lpos != null) {
|
||||
if (lpos.offsetWidth != null && lpos.offsetHeight != null) {
|
||||
position = Offset(lpos.offsetWidth!, lpos.offsetHeight!);
|
||||
}
|
||||
if (lpos.width != null && lpos.height != null) {
|
||||
sz = Size(lpos.width!, lpos.height!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@@ -1791,24 +1797,25 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId, bool? flush}) a
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isWindows) {
|
||||
if (isWindows && position != null) {
|
||||
const kMinOffset = -10000;
|
||||
const kMaxOffset = 10000;
|
||||
if (position.dx < kMinOffset ||
|
||||
position.dy < kMinOffset ||
|
||||
position.dx > kMaxOffset ||
|
||||
position.dy > kMaxOffset) {
|
||||
if (position!.dx < kMinOffset ||
|
||||
position!.dy < kMinOffset ||
|
||||
position!.dx > kMaxOffset ||
|
||||
position!.dy > kMaxOffset) {
|
||||
debugPrint("Invalid position: $position, ignore saving position");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final pos = LastWindowPosition(
|
||||
sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
|
||||
final pos = LastWindowPosition(sz?.width, sz?.height, position?.dx,
|
||||
position?.dy, isMaximized, isFullscreen);
|
||||
|
||||
final WindowKey key = (type: type, windowId: windowId);
|
||||
|
||||
final bool haveNewWindowPosition = (_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
|
||||
final bool haveNewWindowPosition =
|
||||
(_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
|
||||
final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
|
||||
|
||||
if (haveNewWindowPosition || isPreviousNewWindowPositionPending) {
|
||||
@@ -1834,10 +1841,11 @@ Future<void> _saveWindowPositionActual(WindowKey key) async {
|
||||
await bind.setLocalFlutterOption(
|
||||
k: windowFramePrefix + key.type.name, v: pos.toString());
|
||||
|
||||
if ((key.type == WindowType.RemoteDesktop || key.type == WindowType.ViewCamera) &&
|
||||
if ((key.type == WindowType.RemoteDesktop ||
|
||||
key.type == WindowType.ViewCamera) &&
|
||||
key.windowId != null) {
|
||||
await _saveSessionWindowPosition(
|
||||
key.type, key.windowId!, pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
|
||||
await _saveSessionWindowPosition(key.type, key.windowId!,
|
||||
pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2941,7 +2949,7 @@ Future<void> updateSystemWindowTheme() async {
|
||||
///
|
||||
/// Note: not found a general solution for rust based AVFoundation bingding.
|
||||
/// [AVFoundation] crate has compile error.
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host");
|
||||
|
||||
enum PermissionAuthorizeType {
|
||||
undetermined,
|
||||
|
||||
156
flutter/lib/common/widgets/custom_scale_base.dart
Normal file
156
flutter/lib/common/widgets/custom_scale_base.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/utils/scale.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
|
||||
/// Base class providing shared custom scale control logic for both mobile and desktop widgets.
|
||||
/// Implementations must provide [ffi] and [onScaleChanged] getters.
|
||||
abstract class CustomScaleControls<T extends StatefulWidget> extends State<T> {
|
||||
/// FFI instance for session interaction
|
||||
FFI get ffi;
|
||||
|
||||
/// Callback invoked when scale value changes
|
||||
ValueChanged<int>? get onScaleChanged;
|
||||
|
||||
late int _scaleValue;
|
||||
late final Debouncer<int> _debouncerScale;
|
||||
// Normalized slider position in [0, 1]. We map it nonlinearly to percent.
|
||||
double _scalePos = 0.0;
|
||||
|
||||
int get scaleValue => _scaleValue;
|
||||
double get scalePos => _scalePos;
|
||||
|
||||
int mapPosToPercent(double p) => _mapPosToPercent(p);
|
||||
|
||||
static const int minPercent = kScaleCustomMinPercent;
|
||||
static const int pivotPercent = kScaleCustomPivotPercent; // 100% should be at 1/3 of track
|
||||
static const int maxPercent = kScaleCustomMaxPercent;
|
||||
static const double pivotPos = kScaleCustomPivotPos; // first 1/3 → up to 100%
|
||||
static const double detentEpsilon = kScaleCustomDetentEpsilon; // snap range around pivot (~0.6%)
|
||||
|
||||
// Clamp helper for local use
|
||||
int _clampScale(int v) => clampCustomScalePercent(v);
|
||||
|
||||
// Map normalized position [0,1] → percent [5,1000] with 100 at 1/3 width.
|
||||
int _mapPosToPercent(double p) {
|
||||
if (p <= 0.0) return minPercent;
|
||||
if (p >= 1.0) return maxPercent;
|
||||
if (p <= pivotPos) {
|
||||
final q = p / pivotPos; // 0..1
|
||||
final v = minPercent + q * (pivotPercent - minPercent);
|
||||
return _clampScale(v.round());
|
||||
} else {
|
||||
final q = (p - pivotPos) / (1.0 - pivotPos); // 0..1
|
||||
final v = pivotPercent + q * (maxPercent - pivotPercent);
|
||||
return _clampScale(v.round());
|
||||
}
|
||||
}
|
||||
|
||||
// Map percent [5,1000] → normalized position [0,1]
|
||||
double _mapPercentToPos(int percent) {
|
||||
final p = _clampScale(percent);
|
||||
if (p <= pivotPercent) {
|
||||
final q = (p - minPercent) / (pivotPercent - minPercent);
|
||||
return q * pivotPos;
|
||||
} else {
|
||||
final q = (p - pivotPercent) / (maxPercent - pivotPercent);
|
||||
return pivotPos + q * (1.0 - pivotPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap normalized position to the pivot when close to it
|
||||
double _snapNormalizedPos(double p) {
|
||||
if ((p - pivotPos).abs() <= detentEpsilon) return pivotPos;
|
||||
if (p < 0.0) return 0.0;
|
||||
if (p > 1.0) return 1.0;
|
||||
return p;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scaleValue = 100;
|
||||
_debouncerScale = Debouncer<int>(
|
||||
kDebounceCustomScaleDuration,
|
||||
onChanged: (v) async {
|
||||
await _applyScale(v);
|
||||
},
|
||||
initialValue: _scaleValue,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
final v = await getSessionCustomScalePercent(ffi.sessionId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_scaleValue = v;
|
||||
_scalePos = _mapPercentToPos(v);
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Failed to get initial value: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyScale(int v) async {
|
||||
v = clampCustomScalePercent(v);
|
||||
setState(() {
|
||||
_scaleValue = v;
|
||||
});
|
||||
try {
|
||||
await bind.sessionSetFlutterOption(
|
||||
sessionId: ffi.sessionId,
|
||||
k: kCustomScalePercentKey,
|
||||
v: v.toString());
|
||||
final curStyle = await bind.sessionGetViewStyle(sessionId: ffi.sessionId);
|
||||
if (curStyle != kRemoteViewStyleCustom) {
|
||||
await bind.sessionSetViewStyle(
|
||||
sessionId: ffi.sessionId, value: kRemoteViewStyleCustom);
|
||||
}
|
||||
await ffi.canvasModel.updateViewStyle();
|
||||
if (isMobile) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
onScaleChanged?.call(v);
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Apply failed: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
void nudgeScale(int delta) {
|
||||
final next = _clampScale(_scaleValue + delta);
|
||||
setState(() {
|
||||
_scaleValue = next;
|
||||
_scalePos = _mapPercentToPos(next);
|
||||
});
|
||||
onScaleChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncerScale.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onSliderChanged(double v) {
|
||||
final snapped = _snapNormalizedPos(v);
|
||||
final next = _mapPosToPercent(snapped);
|
||||
if (next != _scaleValue || snapped != _scalePos) {
|
||||
setState(() {
|
||||
_scalePos = snapped;
|
||||
_scaleValue = next;
|
||||
});
|
||||
onScaleChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,29 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'address_book.dart';
|
||||
|
||||
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
|
||||
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
|
||||
'', dialogManager);
|
||||
void clientClose(SessionID sessionId, FFI ffi) async {
|
||||
if (allowAskForNoteAtEndOfConnection(ffi, true)) {
|
||||
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
|
||||
return;
|
||||
}
|
||||
closeConnection();
|
||||
} else {
|
||||
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
|
||||
'', ffi.dialogManager);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ValidationRule {
|
||||
@@ -1509,56 +1518,71 @@ showSetOSAccount(
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildNoteTextField({
|
||||
required TextEditingController controller,
|
||||
required VoidCallback onEscape,
|
||||
}) {
|
||||
final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
onEscape();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('input note here'),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: EdgeInsets.all(12),
|
||||
),
|
||||
minLines: 5,
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
).workaroundFreezeLinuxMint();
|
||||
}
|
||||
|
||||
showAuditDialog(FFI ffi) async {
|
||||
final controller = TextEditingController(text: ffi.auditNote);
|
||||
final controller = TextEditingController(
|
||||
text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId));
|
||||
ffi.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
var text = controller.text;
|
||||
bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
|
||||
ffi.auditNote = text;
|
||||
close();
|
||||
}
|
||||
|
||||
late final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
close();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Note')),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
height: 120,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: const InputDecoration.collapsed(
|
||||
hintText: 'input note here',
|
||||
),
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
child: buildNoteTextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
).workaroundFreezeLinuxMint()),
|
||||
onEscape: close,
|
||||
)),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit)
|
||||
@@ -1569,6 +1593,223 @@ showAuditDialog(FFI ffi) async {
|
||||
});
|
||||
}
|
||||
|
||||
bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) {
|
||||
if (ffi == null) {
|
||||
return false;
|
||||
}
|
||||
return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) &&
|
||||
bind
|
||||
.sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn")
|
||||
.isNotEmpty &&
|
||||
bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty &&
|
||||
bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty &&
|
||||
(!closedByControlling ||
|
||||
bind.willSessionCloseCloseSession(sessionId: ffi.sessionId));
|
||||
}
|
||||
|
||||
// return value: close canceled
|
||||
// true: return
|
||||
// false: go on
|
||||
Future<bool> desktopTryShowTabAuditDialogCloseCancelled(
|
||||
{required String id, required DesktopTabController tabController}) async {
|
||||
try {
|
||||
final page =
|
||||
tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page;
|
||||
final ffi = (page as dynamic).ffi;
|
||||
final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi);
|
||||
return res;
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show audit dialog: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// return value:
|
||||
// true: return
|
||||
// false: go on
|
||||
Future<bool> showConnEndAuditDialogCloseCanceled(
|
||||
{required FFI ffi, String? type, String? title, String? text}) async {
|
||||
final res = await _showConnEndAuditDialogCloseCanceled(
|
||||
ffi: ffi, type: type, title: title, text: text);
|
||||
if (res == true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// return value:
|
||||
// true: return
|
||||
// false / null: go on
|
||||
Future<bool?> _showConnEndAuditDialogCloseCanceled({
|
||||
required FFI ffi,
|
||||
String? type,
|
||||
String? title,
|
||||
String? text,
|
||||
}) async {
|
||||
final closedByControlling = type == null;
|
||||
final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling);
|
||||
if (!showDialog) {
|
||||
return false;
|
||||
}
|
||||
ffi.dialogManager.dismissAll();
|
||||
|
||||
Future<void> updateAuditNoteByGuid(String auditGuid, String note) async {
|
||||
debugPrint('Updating audit note for GUID: $auditGuid, note: $note');
|
||||
try {
|
||||
final apiServer = await bind.mainGetApiServer();
|
||||
if (apiServer.isEmpty) {
|
||||
debugPrint('API server is empty, cannot update audit note');
|
||||
return;
|
||||
}
|
||||
final url = '$apiServer/api/audit';
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
final body = jsonEncode({
|
||||
'guid': auditGuid,
|
||||
'note': note,
|
||||
});
|
||||
|
||||
final response = await http.put(
|
||||
Uri.parse(url),
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('Successfully updated audit note for GUID: $auditGuid');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error updating audit note: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final controller = TextEditingController();
|
||||
bool askForNote =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
bool isInProgress = false;
|
||||
|
||||
return await ffi.dialogManager.show<bool>((setState, close, context) {
|
||||
cancel() {
|
||||
close(true);
|
||||
}
|
||||
|
||||
set() async {
|
||||
if (isInProgress) return;
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
var text = controller.text;
|
||||
if (text.isNotEmpty) {
|
||||
await updateAuditNoteByGuid(
|
||||
bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text)
|
||||
.timeout(const Duration(seconds: 6), onTimeout: () {
|
||||
debugPrint('updateAuditNoteByGuid timeout after 6s');
|
||||
});
|
||||
}
|
||||
// Save the "ask for note" preference
|
||||
if (!isOptFixed) {
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, askForNote);
|
||||
}
|
||||
}
|
||||
|
||||
submit() async {
|
||||
await set();
|
||||
close(false);
|
||||
}
|
||||
|
||||
final buttons = [
|
||||
dialogButton('OK', onPressed: isInProgress ? null : submit)
|
||||
];
|
||||
if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||
buttons.add(dialogButton('Retry', onPressed: () async {
|
||||
await set();
|
||||
close(true);
|
||||
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false);
|
||||
}));
|
||||
if (type == 'relay-hint2') {
|
||||
buttons.add(dialogButton('Connect via relay', onPressed: () async {
|
||||
await set();
|
||||
close(true);
|
||||
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (closedByControlling) {
|
||||
buttons.add(dialogButton('Cancel',
|
||||
onPressed: isInProgress ? null : cancel, isOutline: true));
|
||||
}
|
||||
|
||||
Widget content;
|
||||
if (closedByControlling) {
|
||||
content = SelectionArea(
|
||||
child: msgboxContent(
|
||||
'info', 'Close', 'Are you sure to close the connection?'));
|
||||
} else {
|
||||
content =
|
||||
SelectionArea(child: msgboxContent(type, title ?? '', text ?? ''));
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: SizedBox(
|
||||
width: 350,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
content,
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: buildNoteTextField(
|
||||
controller: controller,
|
||||
onEscape: cancel,
|
||||
),
|
||||
),
|
||||
if (!isOptFixed) ...[
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
askForNote = !askForNote;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: askForNote,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
askForNote = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate('note-at-conn-end-tip'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isInProgress)
|
||||
const LinearProgressIndicator().marginOnly(top: 4),
|
||||
],
|
||||
)),
|
||||
actions: buttons,
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showConfirmSwitchSidesDialog(
|
||||
SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -2121,15 +2362,20 @@ void showWindowsSessionsDialog(
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
msgboxContent(type, title, text).marginOnly(bottom: 12),
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
dialogButton('Connect', onPressed: submit, isOutline: false),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -400,6 +400,8 @@ Future<bool?> loginDialog() async {
|
||||
String? passwordMsg;
|
||||
var isInProgress = false;
|
||||
final RxString curOP = ''.obs;
|
||||
// Track hover state for the close icon
|
||||
bool isCloseHovered = false;
|
||||
|
||||
final loginOptions = [].obs;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
@@ -557,21 +559,27 @@ Future<bool?> loginDialog() async {
|
||||
Text(
|
||||
translate('Login'),
|
||||
).marginOnly(top: MyTheme.dialogPadding),
|
||||
InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => isCloseHovered = true),
|
||||
onExit: (_) => setState(() => isCloseHovered = false),
|
||||
child: InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: isCloseHovered
|
||||
? Colors.white
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
).marginOnly(top: 10, right: 15),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
checkScreenSize: true,
|
||||
position: draggablePositions.chatWindow,
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -395,7 +396,10 @@ class _DraggableState extends State<Draggable> {
|
||||
_chatModel?.setChatWindowPosition(position);
|
||||
}
|
||||
|
||||
checkScreenSize() {}
|
||||
checkScreenSize() {
|
||||
// Ensure the draggable always stays within current screen bounds
|
||||
widget.position.tryAdjust(widget.width, widget.height, 1);
|
||||
}
|
||||
|
||||
checkKeyboard() {
|
||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
@@ -517,6 +521,12 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
_lastBottomHeight = bottomHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
position.tryAdjust(_width, _height, 1);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
checkKeyboard();
|
||||
|
||||
@@ -58,6 +58,7 @@ const String kWindowActionRebuild = "rebuild";
|
||||
const String kWindowEventHide = "hide";
|
||||
const String kWindowEventShow = "show";
|
||||
const String kWindowConnect = "connect";
|
||||
const String kWindowBumpMouse = "bump_mouse";
|
||||
|
||||
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
@@ -78,6 +79,7 @@ const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionViewStyle = "view_style";
|
||||
const String kOptionScrollStyle = "scroll_style";
|
||||
const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
|
||||
const String kOptionImageQuality = "image_quality";
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionTextureRender = "use-texture-render";
|
||||
@@ -158,11 +160,15 @@ const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||
|
||||
// network options
|
||||
const String kOptionAllowWebSocket = "allow-websocket";
|
||||
const String kOptionAllowInsecureTLSFallback = "allow-insecure-tls-fallback";
|
||||
const String kOptionDisableUdp = "disable-udp";
|
||||
const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
|
||||
|
||||
// buildin opitons
|
||||
// builtin options
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||
@@ -319,13 +325,15 @@ const kRemoteViewStyleAdaptive = 'adaptive';
|
||||
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
|
||||
const kRemoteViewStyleCustom = 'custom';
|
||||
|
||||
|
||||
/// [kRemoteScrollStyleAuto] Scroll image auto by position.
|
||||
const kRemoteScrollStyleAuto = 'scrollauto';
|
||||
|
||||
/// [kRemoteScrollStyleBar] Scroll image with scroll bar.
|
||||
const kRemoteScrollStyleBar = 'scrollbar';
|
||||
|
||||
/// [kRemoteScrollStyleEdge] Scroll image auto at edges.
|
||||
const kRemoteScrollStyleEdge = 'scrolledge';
|
||||
|
||||
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
|
||||
const kScrollModeDefault = 'default';
|
||||
|
||||
@@ -353,12 +361,14 @@ const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
|
||||
};
|
||||
|
||||
// Scale custom related constants
|
||||
const String kCustomScalePercentKey = 'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
|
||||
const String kCustomScalePercentKey =
|
||||
'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
|
||||
const int kScaleCustomMinPercent = 5;
|
||||
const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track
|
||||
const int kScaleCustomMaxPercent = 1000;
|
||||
const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100%
|
||||
const double kScaleCustomDetentEpsilon = 0.006; // snap range around pivot (~0.6%)
|
||||
const double kScaleCustomDetentEpsilon =
|
||||
0.006; // snap range around pivot (~0.6%)
|
||||
const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300);
|
||||
|
||||
// ================================ mobile ================================
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -760,9 +761,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
'scaleFactor': screen.scaleFactor,
|
||||
};
|
||||
|
||||
bool isChattyMethod(String methodName) {
|
||||
switch (methodName) {
|
||||
case kWindowBumpMouse: return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
if (!isChattyMethod(call.method)) {
|
||||
debugPrint(
|
||||
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
}
|
||||
if (call.method == kWindowMainWindowOnTop) {
|
||||
windowOnTop(null);
|
||||
} else if (call.method == kWindowGetWindowInfo) {
|
||||
@@ -793,6 +804,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
);
|
||||
} else if (call.method == kWindowBumpMouse) {
|
||||
return RdPlatformChannel.instance.bumpMouse(
|
||||
dx: call.arguments['dx'],
|
||||
dy: call.arguments['dy']);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
final args = call.arguments.split(',');
|
||||
int? windowId;
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/printer_model.dart';
|
||||
@@ -560,6 +561,12 @@ class _GeneralState extends State<_General> {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||
}
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'note-at-conn-end-tip',
|
||||
kOptionAllowAskForNoteAtEndOfConnection,
|
||||
isServer: false,
|
||||
));
|
||||
return _Card(title: 'Other', children: children);
|
||||
}
|
||||
|
||||
@@ -1177,7 +1184,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
],
|
||||
),
|
||||
enabled: tmpEnabled && !locked),
|
||||
numericOneTimePassword,
|
||||
if (usePassword) numericOneTimePassword,
|
||||
if (usePassword) radios[1],
|
||||
if (usePassword)
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
@@ -1585,6 +1592,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget switchWidget(IconData icon, String title, String tooltipMessage,
|
||||
String optionKey) =>
|
||||
listTile(
|
||||
icon: icon,
|
||||
title: title,
|
||||
showTooltip: true,
|
||||
tooltipMessage: tooltipMessage,
|
||||
trailing: Switch(
|
||||
value: mainGetBoolOptionSync(optionKey),
|
||||
onChanged: locked || isOptionFixed(optionKey)
|
||||
? null
|
||||
: (value) {
|
||||
mainSetBoolOption(optionKey, value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
|
||||
final divider = const Divider(height: 1, indent: 16, endIndent: 16);
|
||||
return _Card(
|
||||
title: 'Network',
|
||||
children: [
|
||||
@@ -1596,33 +1624,65 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
listTile(
|
||||
icon: Icons.dns_outlined,
|
||||
title: 'ID/Relay Server',
|
||||
onTap: () => showServerSettings(gFFI.dialogManager),
|
||||
onTap: () => showServerSettings(gFFI.dialogManager, setState),
|
||||
),
|
||||
if (!hideServer && (!hideProxy || !hideWebSocket))
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideProxy && !hideServer) divider,
|
||||
if (!hideProxy)
|
||||
listTile(
|
||||
icon: Icons.network_ping_outlined,
|
||||
title: 'Socks5/Http(s) Proxy',
|
||||
onTap: changeSocks5Proxy,
|
||||
),
|
||||
if (!hideProxy && !hideWebSocket)
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideWebSocket && (!hideServer || !hideProxy)) divider,
|
||||
if (!hideWebSocket)
|
||||
listTile(
|
||||
icon: Icons.web_asset_outlined,
|
||||
title: 'Use WebSocket',
|
||||
showTooltip: true,
|
||||
tooltipMessage: 'websocket_tip',
|
||||
trailing: Switch(
|
||||
value: mainGetBoolOptionSync(kOptionAllowWebSocket),
|
||||
onChanged: locked
|
||||
? null
|
||||
: (value) {
|
||||
mainSetBoolOption(kOptionAllowWebSocket, value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
switchWidget(
|
||||
Icons.web_asset_outlined,
|
||||
'Use WebSocket',
|
||||
'${translate('websocket_tip')}\n\n${translate('server-oss-not-support-tip')}',
|
||||
kOptionAllowWebSocket),
|
||||
if (!isWeb)
|
||||
futureBuilder(
|
||||
future: bind.mainIsUsingPublicServer(),
|
||||
hasData: (isUsingPublicServer) {
|
||||
if (isUsingPublicServer) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
if (!hideServer || !hideProxy || !hideWebSocket)
|
||||
divider,
|
||||
switchWidget(
|
||||
Icons.no_encryption_outlined,
|
||||
'Allow insecure TLS fallback',
|
||||
'allow-insecure-tls-fallback-tip',
|
||||
kOptionAllowInsecureTLSFallback),
|
||||
if (!outgoingOnly) divider,
|
||||
if (!outgoingOnly)
|
||||
listTile(
|
||||
icon: Icons.lan_outlined,
|
||||
title: 'Disable UDP',
|
||||
showTooltip: true,
|
||||
tooltipMessage:
|
||||
'${translate('disable-udp-tip')}\n\n${translate('server-oss-not-support-tip')}',
|
||||
trailing: Switch(
|
||||
value: bind.mainGetOptionSync(
|
||||
key: kOptionDisableUdp) ==
|
||||
'Y',
|
||||
onChanged:
|
||||
locked || isOptionFixed(kOptionDisableUdp)
|
||||
? null
|
||||
: (value) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionDisableUdp,
|
||||
value: value ? 'Y' : 'N');
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1685,6 +1745,13 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
|
||||
|
||||
onEdgeScrollEdgeThicknessChanged(double value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: kOptionEdgeScrollEdgeThickness, value: value.round().toString());
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
return _Card(title: 'Default Scroll Style', children: [
|
||||
_Radio(context,
|
||||
value: kRemoteScrollStyleAuto,
|
||||
@@ -1696,6 +1763,23 @@ class _DisplayState extends State<_Display> {
|
||||
groupValue: groupValue,
|
||||
label: 'Scrollbar',
|
||||
onChanged: isOptFixed ? null : onChanged),
|
||||
if (!isWeb) ...[
|
||||
_Radio(context,
|
||||
value: kRemoteScrollStyleEdge,
|
||||
groupValue: groupValue,
|
||||
label: 'ScrollEdge',
|
||||
onChanged: isOptFixed ? null : onChanged),
|
||||
Offstage(
|
||||
offstage: groupValue != kRemoteScrollStyleEdge,
|
||||
child: EdgeThicknessControl(
|
||||
value: double.tryParse(bind.mainGetUserDefaultOption(
|
||||
key: kOptionEdgeScrollEdgeThickness)) ??
|
||||
100.0,
|
||||
onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness)
|
||||
? null
|
||||
: onEdgeScrollEdgeThicknessChanged,
|
||||
)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1737,9 +1821,9 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
Widget trackpadSpeed(BuildContext context) {
|
||||
final initSpeed = (int.tryParse(
|
||||
bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
final initSpeed =
|
||||
(int.tryParse(bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
final curSpeed = SimpleWrapper(initSpeed);
|
||||
void onDebouncer(int v) {
|
||||
bind.mainSetUserDefaultOption(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
@@ -52,7 +53,7 @@ enum MouseFocusScope {
|
||||
}
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
const FileManagerPage(
|
||||
FileManagerPage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -67,9 +68,16 @@ class FileManagerPage extends StatefulWidget {
|
||||
final bool? forceRelay;
|
||||
final String? connToken;
|
||||
final DesktopTabController? tabController;
|
||||
final SimpleWrapper<State<FileManagerPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
State<StatefulWidget> createState() {
|
||||
final state = _FileManagerPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
@@ -139,12 +147,26 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
}
|
||||
|
||||
Widget willPopScope(Widget child) {
|
||||
if (isWeb) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(_ffi.sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Overlay(key: _overlayKeyState.key, initialEntries: [
|
||||
OverlayEntry(builder: (_) {
|
||||
return Scaffold(
|
||||
return willPopScope(Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
@@ -160,7 +182,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Flexible(flex: 2, child: statusList())
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
||||
@@ -40,7 +41,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
label: params['id'],
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(params['id']),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: params['id'],
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(params['id']);
|
||||
},
|
||||
page: FileManagerPage(
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
@@ -69,7 +78,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: FileManagerPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
@@ -132,6 +149,14 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
|
||||
@@ -25,7 +25,7 @@ class _PortForward {
|
||||
}
|
||||
|
||||
class PortForwardPage extends StatefulWidget {
|
||||
const PortForwardPage({
|
||||
PortForwardPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -42,9 +42,16 @@ class PortForwardPage extends StatefulWidget {
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final SimpleWrapper<State<PortForwardPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
State<PortForwardPage> createState() {
|
||||
final state = _PortForwardPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _PortForwardPageState extends State<PortForwardPage>
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -72,7 +73,10 @@ class RemotePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
with AutomaticKeepAliveClientMixin, MultiWindowListener {
|
||||
with
|
||||
AutomaticKeepAliveClientMixin,
|
||||
MultiWindowListener,
|
||||
TickerProviderStateMixin {
|
||||
Timer? _timer;
|
||||
String keyboardMode = "legacy";
|
||||
bool _isWindowBlur = false;
|
||||
@@ -112,11 +116,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
_ffi.canvasModel.activateLocalCursor();
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
_ffi.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||
});
|
||||
_ffi.canvasModel.initializeEdgeScrollFallback(this);
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
@@ -395,7 +401,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi.dialogManager);
|
||||
clientClose(sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(providers: [
|
||||
@@ -408,6 +414,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void enterView(PointerEnterEvent evt) {
|
||||
_ffi.canvasModel.rearmEdgeScroll();
|
||||
|
||||
_cursorOverImage.value = true;
|
||||
_firstEnterImage.value = true;
|
||||
if (_onEnterOrLeaveImage4Toolbar != null) {
|
||||
@@ -427,6 +435,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void leaveView(PointerExitEvent evt) {
|
||||
_ffi.canvasModel.disableEdgeScroll();
|
||||
|
||||
if (_ffi.ffiModel.keyboard) {
|
||||
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
||||
}
|
||||
@@ -625,7 +635,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
onHover: (evt) {},
|
||||
child: child);
|
||||
});
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
@@ -680,9 +690,20 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
Widget _buildScrollAutoNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
double sizeScale = s;
|
||||
if (widget.ffi.ffiModel.isPeerLinux) {
|
||||
final displays = widget.ffi.ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
sizeScale = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
return CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: c.x / sizeScale,
|
||||
y: c.y / sizeScale,
|
||||
scale: sizeScale),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -695,17 +716,19 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
if (rect == null) {
|
||||
return Container();
|
||||
}
|
||||
final isPeerLinux = ffiModel.isPeerLinux;
|
||||
final curDisplay = ffiModel.pi.currentDisplay;
|
||||
for (var i = 0; i < displays.length; i++) {
|
||||
final textureId = widget.ffi.textureModel
|
||||
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
|
||||
if (true) {
|
||||
// both "textureId.value != -1" and "true" seems ok
|
||||
final sizeScale = isPeerLinux ? s / displays[i].scale : s;
|
||||
children.add(Positioned(
|
||||
left: (displays[i].x - rect.left) * s + offset.dx,
|
||||
top: (displays[i].y - rect.top) * s + offset.dy,
|
||||
width: displays[i].width * s,
|
||||
height: displays[i].height * s,
|
||||
width: displays[i].width * sizeScale,
|
||||
height: displays[i].height * sizeScale,
|
||||
child: Obx(() => Texture(
|
||||
textureId: textureId.value,
|
||||
filterQuality:
|
||||
|
||||
@@ -80,7 +80,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: peerId!,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(peerId),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: peerId!,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(peerId!);
|
||||
},
|
||||
page: RemotePage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId!,
|
||||
@@ -316,7 +324,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
translate('Close'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
proc: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(key);
|
||||
cancelFunc();
|
||||
},
|
||||
@@ -369,6 +383,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
@@ -423,7 +445,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:xterm/xterm.dart';
|
||||
import 'terminal_connection_manager.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
TerminalPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -25,15 +25,23 @@ class TerminalPage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() => _TerminalPageState();
|
||||
State<TerminalPage> createState() {
|
||||
final state = _TerminalPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -53,18 +61,30 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||
_cellHeight = ph * 1.0;
|
||||
|
||||
// Schedule the setState for the next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
|
||||
|
||||
// Check if this is a new connection or additional terminal
|
||||
// Note: When a connection exists, the ref count will be > 1 after this terminal is added
|
||||
final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) &&
|
||||
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
|
||||
|
||||
final isExistingConnection =
|
||||
TerminalConnectionManager.hasConnection(widget.id) &&
|
||||
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
|
||||
|
||||
if (!isExistingConnection) {
|
||||
// First terminal - show loading dialog, wait for onReady
|
||||
_ffi.dialogManager
|
||||
@@ -87,30 +107,48 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// This method ensures that the number of visible rows is an integer by computing the
|
||||
// extra space left after dividing the available height by the height of a single
|
||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||
EdgeInsets _calculatePadding(double heightPx) {
|
||||
if (_cellHeight == null) {
|
||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||
}
|
||||
final rows = (heightPx / _cellHeight!).floor();
|
||||
final extraSpace = heightPx - rows * _cellHeight!;
|
||||
final topBottom = extraSpace / 2.0;
|
||||
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final heightPx = constraints.maxHeight;
|
||||
return TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
@@ -61,12 +62,21 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
String? connToken,
|
||||
}) {
|
||||
final tabKey = '${peerId}_$terminalId';
|
||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||
final tabLabel =
|
||||
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
|
||||
return TabInfo(
|
||||
key: tabKey,
|
||||
label: '$peerId #$terminalId',
|
||||
label: tabLabel,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabKey,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
// Close the terminal session first
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi != null) {
|
||||
@@ -407,6 +417,14 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
|
||||
@@ -360,7 +360,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi.dialogManager);
|
||||
clientClose(sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(providers: [
|
||||
@@ -527,7 +527,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -79,7 +80,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
label: peerId!,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(peerId),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: peerId!,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(peerId!);
|
||||
},
|
||||
page: ViewCameraPage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId!,
|
||||
@@ -287,7 +296,13 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
translate('Close'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
proc: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(key);
|
||||
cancelFunc();
|
||||
},
|
||||
@@ -340,6 +355,14 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
@@ -393,7 +416,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: ViewCameraPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
|
||||
@@ -26,6 +26,7 @@ import '../../common/shared_state.dart';
|
||||
import './popup_menu.dart';
|
||||
import './kb_layout_type_chooser.dart';
|
||||
import 'package:flutter_hbb/utils/scale.dart';
|
||||
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
|
||||
|
||||
class ToolbarState {
|
||||
late RxBool _pin;
|
||||
@@ -153,135 +154,6 @@ class _ToolbarTheme {
|
||||
typedef DismissFunc = void Function();
|
||||
|
||||
class RemoteMenuEntry {
|
||||
static MenuEntryRadios<String> viewStyle(
|
||||
String remoteId,
|
||||
FFI ffi,
|
||||
EdgeInsets padding, {
|
||||
DismissFunc? dismissFunc,
|
||||
DismissCallback? dismissCallback,
|
||||
RxString? rxViewStyle,
|
||||
}) {
|
||||
return MenuEntryRadios<String>(
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale original'),
|
||||
value: kRemoteViewStyleOriginal,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: dismissCallback,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale adaptive'),
|
||||
value: kRemoteViewStyleAdaptive,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: dismissCallback,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale custom'),
|
||||
value: kRemoteViewStyleCustom,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: dismissCallback,
|
||||
),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
// null means peer id is not found, which there's no need to care about
|
||||
final viewStyle =
|
||||
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
||||
if (rxViewStyle != null) {
|
||||
rxViewStyle.value = viewStyle;
|
||||
}
|
||||
return viewStyle;
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionSetViewStyle(
|
||||
sessionId: ffi.sessionId, value: newValue);
|
||||
if (rxViewStyle != null) {
|
||||
rxViewStyle.value = newValue;
|
||||
}
|
||||
ffi.canvasModel.updateViewStyle();
|
||||
if (dismissFunc != null) {
|
||||
dismissFunc();
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntrySwitch2<String> showRemoteCursor(
|
||||
String remoteId,
|
||||
SessionID sessionId,
|
||||
EdgeInsets padding, {
|
||||
DismissFunc? dismissFunc,
|
||||
DismissCallback? dismissCallback,
|
||||
}) {
|
||||
final state = ShowRemoteCursorState.find(remoteId);
|
||||
final optKey = 'show-remote-cursor';
|
||||
return MenuEntrySwitch2<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () {
|
||||
return state;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: optKey);
|
||||
state.value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: optKey);
|
||||
if (dismissFunc != null) {
|
||||
dismissFunc();
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntrySwitch<String> disableClipboard(
|
||||
SessionID sessionId,
|
||||
EdgeInsets? padding, {
|
||||
DismissFunc? dismissFunc,
|
||||
DismissCallback? dismissCallback,
|
||||
}) {
|
||||
return createSwitchMenuEntry(
|
||||
sessionId,
|
||||
'Disable clipboard',
|
||||
'disable-clipboard',
|
||||
padding,
|
||||
true,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntrySwitch<String> createSwitchMenuEntry(
|
||||
SessionID sessionId,
|
||||
String text,
|
||||
String option,
|
||||
EdgeInsets? padding,
|
||||
bool dismissOnClicked, {
|
||||
DismissFunc? dismissFunc,
|
||||
DismissCallback? dismissCallback,
|
||||
}) {
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate(text),
|
||||
getter: () async {
|
||||
return bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
if (dismissFunc != null) {
|
||||
dismissFunc();
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
}
|
||||
|
||||
static MenuEntryButton<String> insertLock(
|
||||
SessionID sessionId,
|
||||
EdgeInsets? padding, {
|
||||
@@ -639,7 +511,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
menuStyle: MenuStyle(
|
||||
padding:
|
||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||
menuChildrenGetter: (_) => [buildMonitorSubmenuWidget(context)]);
|
||||
}
|
||||
|
||||
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||
@@ -850,7 +722,7 @@ class _ControlMenu extends StatelessWidget {
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
ffi: ffi,
|
||||
menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) {
|
||||
menuChildrenGetter: (_) => toolbarControls(context, id, ffi).map((e) {
|
||||
if (e.divider) {
|
||||
return Divider();
|
||||
} else {
|
||||
@@ -1061,12 +933,13 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
_screenAdjustor.updateScreen();
|
||||
menuChildrenGetter() {
|
||||
menuChildrenGetter(_IconSubmenuButtonState state) {
|
||||
final menuChildren = <Widget>[
|
||||
_screenAdjustor.adjustWindow(context),
|
||||
viewStyle(customPercent: _customPercent),
|
||||
scrollStyle(),
|
||||
scrollStyle(state, colorScheme),
|
||||
imageQuality(),
|
||||
codec(),
|
||||
if (ffi.connType == ConnType.defaultConn)
|
||||
@@ -1141,14 +1014,14 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
return Column(children: [
|
||||
...v.map((e) {
|
||||
final isCustom = e.value == kRemoteViewStyleCustom;
|
||||
final child = isCustom
|
||||
? Text(translate('Scale custom'))
|
||||
: e.child;
|
||||
final child =
|
||||
isCustom ? Text(translate('Scale custom')) : e.child;
|
||||
// Whether the current selection is already custom
|
||||
final bool isGroupCustomSelected =
|
||||
e.groupValue == kRemoteViewStyleCustom;
|
||||
// Keep menu open when switching INTO custom so the slider is visible immediately
|
||||
final bool keepOpenForThisItem = isCustom && !isGroupCustomSelected;
|
||||
final bool keepOpenForThisItem =
|
||||
isCustom && !isGroupCustomSelected;
|
||||
return RdoMenuButton<String>(
|
||||
value: e.value,
|
||||
groupValue: e.groupValue,
|
||||
@@ -1167,7 +1040,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
}).toList(),
|
||||
// Only show a divider when custom is NOT selected
|
||||
if (!isCustomSelected) Divider(),
|
||||
_customControlsIfCustomSelected(onChanged: (v) => customPercent.value = v),
|
||||
_customControlsIfCustomSelected(
|
||||
onChanged: (v) => customPercent.value = v),
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -1182,12 +1056,14 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
duration: Duration(milliseconds: 220),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: isCustom ? _CustomScaleMenuControls(ffi: ffi, onChanged: onChanged) : SizedBox.shrink(),
|
||||
child: isCustom
|
||||
? _CustomScaleMenuControls(ffi: ffi, onChanged: onChanged)
|
||||
: SizedBox.shrink(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
scrollStyle() {
|
||||
scrollStyle(_IconSubmenuButtonState state, ColorScheme colorScheme) {
|
||||
return futureBuilder(future: () async {
|
||||
final viewStyle =
|
||||
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
||||
@@ -1195,16 +1071,34 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
viewStyle == kRemoteViewStyleCustom;
|
||||
final scrollStyle =
|
||||
await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
|
||||
return {'visible': visible, 'scrollStyle': scrollStyle};
|
||||
final edgeScrollEdgeThickness = await bind
|
||||
.sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId);
|
||||
return {
|
||||
'visible': visible,
|
||||
'scrollStyle': scrollStyle,
|
||||
'edgeScrollEdgeThickness': edgeScrollEdgeThickness,
|
||||
};
|
||||
}(), hasData: (data) {
|
||||
final visible = data['visible'] as bool;
|
||||
if (!visible) return Offstage();
|
||||
final groupValue = data['scrollStyle'] as String;
|
||||
onChange(String? value) async {
|
||||
final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int;
|
||||
|
||||
onChangeScrollStyle(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionSetScrollStyle(
|
||||
sessionId: ffi.sessionId, value: value);
|
||||
widget.ffi.canvasModel.updateScrollStyle();
|
||||
state.setState(() {});
|
||||
}
|
||||
|
||||
onChangeEdgeScrollEdgeThickness(double? value) async {
|
||||
if (value == null) return;
|
||||
final newThickness = value.round();
|
||||
await bind.sessionSetEdgeScrollEdgeThickness(
|
||||
sessionId: ffi.sessionId, value: newThickness);
|
||||
widget.ffi.canvasModel.updateEdgeScrollEdgeThickness(newThickness);
|
||||
state.setState(() {});
|
||||
}
|
||||
|
||||
return Obx(() => Column(children: [
|
||||
@@ -1213,8 +1107,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
value: kRemoteScrollStyleAuto,
|
||||
groupValue: groupValue,
|
||||
onChanged: widget.ffi.canvasModel.imageOverflow.value
|
||||
? (value) => onChange(value)
|
||||
? (value) => onChangeScrollStyle(value)
|
||||
: null,
|
||||
closeOnActivate: groupValue != kRemoteScrollStyleEdge,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
RdoMenuButton<String>(
|
||||
@@ -1222,10 +1117,30 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
value: kRemoteScrollStyleBar,
|
||||
groupValue: groupValue,
|
||||
onChanged: widget.ffi.canvasModel.imageOverflow.value
|
||||
? (value) => onChange(value)
|
||||
? (value) => onChangeScrollStyle(value)
|
||||
: null,
|
||||
closeOnActivate: groupValue != kRemoteScrollStyleEdge,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
if (!isWeb) ...[
|
||||
RdoMenuButton<String>(
|
||||
child: Text(translate('ScrollEdge')),
|
||||
value: kRemoteScrollStyleEdge,
|
||||
groupValue: groupValue,
|
||||
closeOnActivate: false,
|
||||
onChanged: widget.ffi.canvasModel.imageOverflow.value
|
||||
? (value) => onChangeScrollStyle(value)
|
||||
: null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
Offstage(
|
||||
offstage: groupValue != kRemoteScrollStyleEdge,
|
||||
child: EdgeThicknessControl(
|
||||
value: edgeScrollEdgeThickness.toDouble(),
|
||||
onChanged: onChangeEdgeScrollEdgeThickness,
|
||||
colorScheme: colorScheme,
|
||||
)),
|
||||
],
|
||||
Divider(),
|
||||
]));
|
||||
});
|
||||
@@ -1312,132 +1227,21 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
class _CustomScaleMenuControls extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final ValueChanged<int>? onChanged;
|
||||
const _CustomScaleMenuControls({Key? key, required this.ffi, this.onChanged}) : super(key: key);
|
||||
const _CustomScaleMenuControls({Key? key, required this.ffi, this.onChanged})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_CustomScaleMenuControls> createState() => _CustomScaleMenuControlsState();
|
||||
State<_CustomScaleMenuControls> createState() =>
|
||||
_CustomScaleMenuControlsState();
|
||||
}
|
||||
|
||||
class _CustomScaleMenuControlsState extends State<_CustomScaleMenuControls> {
|
||||
late int _value;
|
||||
late final Debouncer<int> _debouncerScale;
|
||||
// Normalized slider position in [0, 1]. We map it nonlinearly to percent.
|
||||
double _pos = 0.0;
|
||||
|
||||
// Piecewise mapping constants (moved to consts.dart)
|
||||
static const int _minPercent = kScaleCustomMinPercent;
|
||||
static const int _pivotPercent = kScaleCustomPivotPercent; // 100% should be at 1/3 of track
|
||||
static const int _maxPercent = kScaleCustomMaxPercent;
|
||||
static const double _pivotPos = kScaleCustomPivotPos; // first 1/3 → up to 100%
|
||||
static const double _detentEpsilon = kScaleCustomDetentEpsilon; // snap range around pivot (~0.6%)
|
||||
|
||||
// Clamp helper for local use
|
||||
int _clamp(int v) => clampCustomScalePercent(v);
|
||||
|
||||
// Map normalized position [0,1] → percent [5,1000] with 100 at 1/3 width.
|
||||
int _mapPosToPercent(double p) {
|
||||
if (p <= 0.0) return _minPercent;
|
||||
if (p >= 1.0) return _maxPercent;
|
||||
if (p <= _pivotPos) {
|
||||
final q = p / _pivotPos; // 0..1
|
||||
final v = _minPercent + q * (_pivotPercent - _minPercent);
|
||||
return _clamp(v.round());
|
||||
} else {
|
||||
final q = (p - _pivotPos) / (1.0 - _pivotPos); // 0..1
|
||||
final v = _pivotPercent + q * (_maxPercent - _pivotPercent);
|
||||
return _clamp(v.round());
|
||||
}
|
||||
}
|
||||
|
||||
// Map percent [5,1000] → normalized position [0,1]
|
||||
double _mapPercentToPos(int percent) {
|
||||
final p = _clamp(percent);
|
||||
if (p <= _pivotPercent) {
|
||||
final q = (p - _minPercent) / (_pivotPercent - _minPercent);
|
||||
return q * _pivotPos;
|
||||
} else {
|
||||
final q = (p - _pivotPercent) / (_maxPercent - _pivotPercent);
|
||||
return _pivotPos + q * (1.0 - _pivotPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap normalized position to the pivot when close to it
|
||||
double _snapNormalizedPos(double p) {
|
||||
if ((p - _pivotPos).abs() <= _detentEpsilon) return _pivotPos;
|
||||
if (p < 0.0) return 0.0;
|
||||
if (p > 1.0) return 1.0;
|
||||
return p;
|
||||
}
|
||||
class _CustomScaleMenuControlsState
|
||||
extends CustomScaleControls<_CustomScaleMenuControls> {
|
||||
@override
|
||||
FFI get ffi => widget.ffi;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_value = 100;
|
||||
_debouncerScale = Debouncer<int>(
|
||||
kDebounceCustomScaleDuration,
|
||||
onChanged: (v) async {
|
||||
await _apply(v);
|
||||
},
|
||||
initialValue: _value,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
final v = await getSessionCustomScalePercent(widget.ffi.sessionId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_value = v;
|
||||
_pos = _mapPercentToPos(v);
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Failed to get initial value: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Future<void> _apply(int v) async {
|
||||
v = clampCustomScalePercent(v);
|
||||
setState(() {
|
||||
_value = v;
|
||||
});
|
||||
try {
|
||||
await bind.sessionSetFlutterOption(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
k: kCustomScalePercentKey,
|
||||
v: v.toString());
|
||||
final curStyle = await bind.sessionGetViewStyle(sessionId: widget.ffi.sessionId);
|
||||
if (curStyle != kRemoteViewStyleCustom) {
|
||||
await bind.sessionSetViewStyle(
|
||||
sessionId: widget.ffi.sessionId, value: kRemoteViewStyleCustom);
|
||||
}
|
||||
await widget.ffi.canvasModel.updateViewStyle();
|
||||
if (isMobile) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
widget.onChanged?.call(v);
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Apply failed: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
void _nudge(int delta) {
|
||||
final next = _clamp(_value + delta);
|
||||
setState(() {
|
||||
_value = next;
|
||||
_pos = _mapPercentToPos(next);
|
||||
});
|
||||
widget.onChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncerScale.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
ValueChanged<int>? get onScaleChanged => widget.onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1446,7 +1250,7 @@ class _CustomScaleMenuControlsState extends State<_CustomScaleMenuControls> {
|
||||
|
||||
final sliderControl = Semantics(
|
||||
label: translate('Custom scale slider'),
|
||||
value: '$_value%',
|
||||
value: '$scaleValue%',
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: colorScheme.primary,
|
||||
@@ -1454,34 +1258,24 @@ class _CustomScaleMenuControlsState extends State<_CustomScaleMenuControls> {
|
||||
overlayColor: colorScheme.primary.withOpacity(0.1),
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
thumbShape: _RectValueThumbShape(
|
||||
min: _minPercent.toDouble(),
|
||||
max: _maxPercent.toDouble(),
|
||||
min: CustomScaleControls.minPercent.toDouble(),
|
||||
max: CustomScaleControls.maxPercent.toDouble(),
|
||||
width: 52,
|
||||
height: 24,
|
||||
radius: 4,
|
||||
// Display the mapped percent for the current normalized value
|
||||
displayValueForNormalized: (t) => _mapPosToPercent(t),
|
||||
displayValueForNormalized: (t) => mapPosToPercent(t),
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: _pos,
|
||||
value: scalePos,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
// Use a wide range of divisions (calculated as (_maxPercent - _minPercent)) to provide ~1% precision increments.
|
||||
// Use a wide range of divisions (calculated as (CustomScaleControls.maxPercent - CustomScaleControls.minPercent)) to provide ~1% precision increments.
|
||||
// This allows users to set precise scale values. Lower values would require more fine-tuning via the +/- buttons, which is undesirable for big ranges.
|
||||
divisions: (_maxPercent - _minPercent).round(),
|
||||
onChanged: (v) {
|
||||
final snapped = _snapNormalizedPos(v);
|
||||
final next = _mapPosToPercent(snapped);
|
||||
if (next != _value || snapped != _pos) {
|
||||
setState(() {
|
||||
_pos = snapped;
|
||||
_value = next;
|
||||
});
|
||||
widget.onChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
},
|
||||
divisions:
|
||||
(CustomScaleControls.maxPercent - CustomScaleControls.minPercent)
|
||||
.round(),
|
||||
onChanged: onSliderChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1497,7 +1291,7 @@ class _CustomScaleMenuControlsState extends State<_CustomScaleMenuControls> {
|
||||
padding: EdgeInsets.all(1),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () => _nudge(-1),
|
||||
onPressed: () => nudgeScale(-1),
|
||||
),
|
||||
),
|
||||
Expanded(child: sliderControl),
|
||||
@@ -1508,7 +1302,7 @@ class _CustomScaleMenuControlsState extends State<_CustomScaleMenuControls> {
|
||||
padding: EdgeInsets.all(1),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _nudge(1),
|
||||
onPressed: () => nudgeScale(1),
|
||||
),
|
||||
),
|
||||
]),
|
||||
@@ -1526,6 +1320,7 @@ class _RectValueThumbShape extends SliderComponentShape {
|
||||
final double width;
|
||||
final double height;
|
||||
final double radius;
|
||||
final String unit;
|
||||
// Optional mapper to compute display value from normalized position [0,1]
|
||||
// If null, falls back to linear interpolation between min and max.
|
||||
final int Function(double normalized)? displayValueForNormalized;
|
||||
@@ -1537,6 +1332,7 @@ class _RectValueThumbShape extends SliderComponentShape {
|
||||
required this.height,
|
||||
required this.radius,
|
||||
this.displayValueForNormalized,
|
||||
this.unit = '%',
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1577,12 +1373,12 @@ class _RectValueThumbShape extends SliderComponentShape {
|
||||
final Paint paint = Paint()..color = fillColor;
|
||||
canvas.drawRRect(rrect, paint);
|
||||
|
||||
// Compute displayed percent from normalized slider value.
|
||||
final int percent = displayValueForNormalized != null
|
||||
// Compute displayed value from normalized slider value.
|
||||
final int displayValue = displayValueForNormalized != null
|
||||
? displayValueForNormalized!(value)
|
||||
: (min + value * (max - min)).round();
|
||||
final TextSpan span = TextSpan(
|
||||
text: '$percent%',
|
||||
text: '$displayValue$unit',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
@@ -1595,7 +1391,8 @@ class _RectValueThumbShape extends SliderComponentShape {
|
||||
textDirection: textDirection,
|
||||
);
|
||||
tp.layout(maxWidth: width - 4);
|
||||
tp.paint(canvas, Offset(center.dx - tp.width / 2, center.dy - tp.height / 2));
|
||||
tp.paint(
|
||||
canvas, Offset(center.dx - tp.width / 2, center.dy - tp.height / 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1941,7 +1738,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
ffi: ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [
|
||||
menuChildrenGetter: (_) => [
|
||||
keyboardMode(),
|
||||
localKeyboardType(),
|
||||
inputSource(),
|
||||
@@ -2206,7 +2003,7 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
menuChildrenGetter: (_) => [textChat(), voiceCall()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2262,7 +2059,7 @@ class _VoiceCallMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
menuChildrenGetter() {
|
||||
menuChildrenGetter(_IconSubmenuButtonState state) {
|
||||
final audioInput = AudioInput(
|
||||
builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
@@ -2368,7 +2165,12 @@ class _CloseMenu extends StatelessWidget {
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/close.svg',
|
||||
tooltip: 'Close',
|
||||
onPressed: () => closeConnection(id: id),
|
||||
onPressed: () async {
|
||||
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
|
||||
return;
|
||||
}
|
||||
closeConnection(id: id);
|
||||
},
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor,
|
||||
);
|
||||
@@ -2462,7 +2264,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
final Widget? icon;
|
||||
final Color color;
|
||||
final Color hoverColor;
|
||||
final List<Widget> Function() menuChildrenGetter;
|
||||
final List<Widget> Function(_IconSubmenuButtonState state) menuChildrenGetter;
|
||||
final MenuStyle? menuStyle;
|
||||
final FFI? ffi;
|
||||
final double? width;
|
||||
@@ -2487,6 +2289,11 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
bool hover = false;
|
||||
|
||||
@override // discard @protected
|
||||
void setState(VoidCallback fn) {
|
||||
super.setState(fn);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(widget.svg != null || widget.icon != null);
|
||||
@@ -2519,7 +2326,7 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
),
|
||||
child: icon))),
|
||||
menuChildren: widget
|
||||
.menuChildrenGetter()
|
||||
.menuChildrenGetter(this)
|
||||
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
|
||||
.toList()));
|
||||
return MenuBar(children: [
|
||||
@@ -2882,3 +2689,56 @@ Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class EdgeThicknessControl extends StatelessWidget {
|
||||
final double value;
|
||||
final ValueChanged<double>? onChanged;
|
||||
final ColorScheme? colorScheme;
|
||||
|
||||
const EdgeThicknessControl({
|
||||
Key? key,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
this.colorScheme,
|
||||
}) : super(key: key);
|
||||
|
||||
static const double kMin = 20;
|
||||
static const double kMax = 150;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = this.colorScheme ?? Theme.of(context).colorScheme;
|
||||
|
||||
final slider = SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: colorScheme.primary,
|
||||
thumbColor: colorScheme.primary,
|
||||
overlayColor: colorScheme.primary.withOpacity(0.1),
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
thumbShape: _RectValueThumbShape(
|
||||
min: EdgeThicknessControl.kMin,
|
||||
max: EdgeThicknessControl.kMax,
|
||||
width: 52,
|
||||
height: 24,
|
||||
radius: 4,
|
||||
unit: 'px',
|
||||
),
|
||||
),
|
||||
child: Semantics(
|
||||
value: value.toInt().toString(),
|
||||
child: Slider(
|
||||
value: value,
|
||||
min: EdgeThicknessControl.kMin,
|
||||
max: EdgeThicknessControl.kMax,
|
||||
divisions:
|
||||
(EdgeThicknessControl.kMax - EdgeThicknessControl.kMin).round(),
|
||||
semanticFormatterCallback: (double newValue) =>
|
||||
"${newValue.round()}px",
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return slider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,10 +405,15 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
}
|
||||
|
||||
_saveFrame({bool? flush}) async {
|
||||
if (tabType == DesktopTabType.main) {
|
||||
await saveWindowPosition(WindowType.Main, flush: flush);
|
||||
} else if (kWindowType != null && kWindowId != null) {
|
||||
await saveWindowPosition(kWindowType!, windowId: kWindowId, flush: flush);
|
||||
try {
|
||||
if (tabType == DesktopTabType.main) {
|
||||
await saveWindowPosition(WindowType.Main, flush: flush);
|
||||
} else if (kWindowType != null && kWindowId != null) {
|
||||
await saveWindowPosition(kWindowType!,
|
||||
windowId: kWindowId, flush: flush);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error saving window position: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,11 +1085,12 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
||||
child: Tooltip(
|
||||
message: widget.tabType == DesktopTabType.main
|
||||
? ''
|
||||
: translate(widget.label.value),
|
||||
message:
|
||||
widget.tabType == DesktopTabType.main ? '' : widget.label.value,
|
||||
child: Text(
|
||||
translate(widget.label.value),
|
||||
widget.tabType == DesktopTabType.main
|
||||
? translate(widget.label.value)
|
||||
: widget.label.value,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
|
||||
@@ -12,7 +12,11 @@ import '../../common/widgets/dialog.dart';
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
{Key? key,
|
||||
required this.id,
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
@@ -92,6 +96,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
WakelockPlus.disable();
|
||||
});
|
||||
model.jobController.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -112,8 +117,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
leading: Row(children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () =>
|
||||
clientClose(gFFI.sessionId, gFFI.dialogManager)),
|
||||
onPressed: () => clientClose(gFFI.sessionId, gFFI)),
|
||||
]),
|
||||
centerTitle: true,
|
||||
title: ToggleSwitch(
|
||||
@@ -424,6 +428,7 @@ class FileManagerView extends StatefulWidget {
|
||||
class _FileManagerViewState extends State<FileManagerView> {
|
||||
final _listScrollController = ScrollController();
|
||||
final _breadCrumbScroller = ScrollController();
|
||||
late final ascending = Rx<bool>(controller.sortAscending);
|
||||
|
||||
bool get isLocal => widget.controller.isLocal;
|
||||
FileController get controller => widget.controller;
|
||||
@@ -635,7 +640,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
onSelected: controller.changeSortStyle),
|
||||
onSelected: (sortBy) {
|
||||
// If selecting the same sort option, flip the order
|
||||
// If selecting a different sort option, use ascending order
|
||||
if (controller.sortBy.value == sortBy) {
|
||||
ascending.value = !controller.sortAscending;
|
||||
} else {
|
||||
ascending.value = true;
|
||||
}
|
||||
controller.changeSortStyle(sortBy,
|
||||
ascending: ascending.value);
|
||||
}),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
||||
@@ -25,6 +25,7 @@ import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../utils/image.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
import '../widgets/custom_scale_widget.dart';
|
||||
|
||||
final initText = '1' * 1024;
|
||||
|
||||
@@ -365,7 +366,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -483,7 +484,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -576,7 +577,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
color: MyTheme.canvasColor,
|
||||
child: Stack(children: () {
|
||||
final paints = [
|
||||
ImagePaint(),
|
||||
ImagePaint(ffiModel: gFFI.ffiModel),
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
@@ -634,7 +635,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
|
||||
Widget getBodyForDesktopWithListener() {
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
var paints = <Widget>[ImagePaint()];
|
||||
var paints = <Widget>[ImagePaint(ffiModel: ffiModel)];
|
||||
if (showCursorPaint) {
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
@@ -1054,11 +1055,20 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
}
|
||||
|
||||
class ImagePaint extends StatelessWidget {
|
||||
final FfiModel ffiModel;
|
||||
ImagePaint({Key? key, required this.ffiModel}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
var s = c.scale;
|
||||
if (ffiModel.isPeerLinux) {
|
||||
final displays = ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
s = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
final adjust = c.getAdjustY();
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
@@ -1129,6 +1139,14 @@ void showOptions(
|
||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||
final numColorSelected = Colors.white;
|
||||
final numColorUnselected = isDarkTheme ? Colors.grey : Colors.black87;
|
||||
// We can't use `Theme.of(context).primaryColor` here, the color is:
|
||||
// - light theme: 0xff2196f3 (Colors.blue)
|
||||
// - dark theme: 0xff212121 (the canvas color?)
|
||||
final numBgSelected =
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.6);
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
@@ -1142,13 +1160,12 @@ void showOptions(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).hintColor),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
color: i == cur
|
||||
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||
: null),
|
||||
color: i == cur ? numBgSelected : null),
|
||||
child: Center(
|
||||
child: Text((i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: i == cur ? Colors.white : Colors.black87,
|
||||
color:
|
||||
i == cur ? numColorSelected : numColorUnselected,
|
||||
fontWeight: FontWeight.bold))))));
|
||||
}
|
||||
displays.add(Padding(
|
||||
@@ -1201,6 +1218,10 @@ void showOptions(
|
||||
if (v != null) viewStyle.value = v;
|
||||
}
|
||||
: null)),
|
||||
// Show custom scale controls when custom view style is selected
|
||||
Obx(() => viewStyle.value == kRemoteViewStyleCustom
|
||||
? MobileCustomScaleControls(ffi: gFFI)
|
||||
: const SizedBox.shrink()),
|
||||
const Divider(color: MyTheme.border),
|
||||
for (var e in imageQualityRadios)
|
||||
Obx(() => getRadio<String>(
|
||||
|
||||
@@ -156,7 +156,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
try {
|
||||
final sc = ServerConfig.decode(data.substring(7));
|
||||
Timer(Duration(milliseconds: 60), () {
|
||||
showServerSettingsWithValue(sc, gFFI.dialogManager);
|
||||
showServerSettingsWithValue(sc, gFFI.dialogManager, null);
|
||||
});
|
||||
} catch (e) {
|
||||
showToast('Invalid QR code');
|
||||
|
||||
@@ -94,7 +94,11 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _hideWebSocket = false;
|
||||
var _enableTrustedDevices = false;
|
||||
var _enableUdpPunch = false;
|
||||
var _allowInsecureTlsFallback = false;
|
||||
var _disableUdp = false;
|
||||
var _enableIpv6Punch = false;
|
||||
var _isUsingPublicServer = false;
|
||||
var _allowAskForNoteAtEndOfConnection = false;
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
@@ -109,6 +113,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||
_allowWebSocket = mainGetBoolOptionSync(kOptionAllowWebSocket);
|
||||
_allowInsecureTlsFallback =
|
||||
mainGetBoolOptionSync(kOptionAllowInsecureTLSFallback);
|
||||
_disableUdp = bind.mainGetOptionSync(key: kOptionDisableUdp) == 'Y';
|
||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||
@@ -130,6 +137,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
||||
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||
_allowAskForNoteAtEndOfConnection =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -200,6 +209,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
update = true;
|
||||
_buildDate = buildDate;
|
||||
}
|
||||
|
||||
final isUsingPublicServer = await bind.mainIsUsingPublicServer();
|
||||
if (_isUsingPublicServer != isUsingPublicServer) {
|
||||
update = true;
|
||||
_isUsingPublicServer = isUsingPublicServer;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
setState(() {});
|
||||
}
|
||||
@@ -667,9 +683,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
showServerSettings(gFFI.dialogManager, (callback) async {
|
||||
_isUsingPublicServer = await bind.mainIsUsingPublicServer();
|
||||
setState(callback);
|
||||
});
|
||||
}),
|
||||
if (!isIOS && !_hideNetwork && !_hideProxy)
|
||||
if (!_hideNetwork && !_hideProxy)
|
||||
SettingsTile(
|
||||
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||
leading: Icon(Icons.network_ping),
|
||||
@@ -691,6 +710,38 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!_isUsingPublicServer)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Allow insecure TLS fallback')),
|
||||
initialValue: _allowInsecureTlsFallback,
|
||||
onToggle: isOptionFixed(kOptionAllowInsecureTLSFallback)
|
||||
? null
|
||||
: (v) async {
|
||||
await mainSetBoolOption(
|
||||
kOptionAllowInsecureTLSFallback, v);
|
||||
final newValue = mainGetBoolOptionSync(
|
||||
kOptionAllowInsecureTLSFallback);
|
||||
setState(() {
|
||||
_allowInsecureTlsFallback = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isAndroid && !outgoingOnly && !_isUsingPublicServer)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Disable UDP')),
|
||||
initialValue: _disableUdp,
|
||||
onToggle: isOptionFixed(kOptionDisableUdp)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionDisableUdp, value: v ? 'Y' : 'N');
|
||||
final newValue =
|
||||
bind.mainGetOptionSync(key: kOptionDisableUdp) == 'Y';
|
||||
setState(() {
|
||||
_disableUdp = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable UDP hole punching')),
|
||||
@@ -734,6 +785,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
onPressed: (context) {
|
||||
showThemeSettings(gFFI.dialogManager);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('note-at-conn-end-tip')),
|
||||
initialValue: _allowAskForNoteAtEndOfConnection,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, v);
|
||||
final newValue = mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection);
|
||||
setState(() {
|
||||
_allowAskForNoteAtEndOfConnection = newValue;
|
||||
});
|
||||
},
|
||||
)
|
||||
]),
|
||||
if (isAndroid)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -38,6 +39,8 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
? (GoogleFonts.robotoMono().fontFamily ?? 'monospace')
|
||||
: 'monospace';
|
||||
|
||||
SessionID get sessionId => _ffi.sessionId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -82,6 +85,16 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi);
|
||||
return false; // Prevent default back behavior
|
||||
},
|
||||
child: buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
|
||||
@@ -197,7 +197,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -310,7 +310,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
clientClose(sessionId, gFFI);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -590,6 +590,14 @@ void showOptions(
|
||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||
final numColorSelected = Colors.white;
|
||||
final numColorUnselected = isDarkTheme ? Colors.grey : Colors.black87;
|
||||
// We can't use `Theme.of(context).primaryColor` here, the color is:
|
||||
// - light theme: 0xff2196f3 (Colors.blue)
|
||||
// - dark theme: 0xff212121 (the canvas color?)
|
||||
final numBgSelected =
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.6);
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
@@ -603,13 +611,12 @@ void showOptions(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).hintColor),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
color: i == cur
|
||||
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||
: null),
|
||||
color: i == cur ? numBgSelected : null),
|
||||
child: Center(
|
||||
child: Text((i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: i == cur ? Colors.white : Colors.black87,
|
||||
color:
|
||||
i == cur ? numColorSelected : numColorUnselected,
|
||||
fontWeight: FontWeight.bold))))));
|
||||
}
|
||||
displays.add(Padding(
|
||||
|
||||
71
flutter/lib/mobile/widgets/custom_scale_widget.dart
Normal file
71
flutter/lib/mobile/widgets/custom_scale_widget.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
|
||||
|
||||
class MobileCustomScaleControls extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final ValueChanged<int>? onChanged;
|
||||
const MobileCustomScaleControls({super.key, required this.ffi, this.onChanged});
|
||||
|
||||
@override
|
||||
State<MobileCustomScaleControls> createState() => _MobileCustomScaleControlsState();
|
||||
}
|
||||
|
||||
class _MobileCustomScaleControlsState extends CustomScaleControls<MobileCustomScaleControls> {
|
||||
@override
|
||||
FFI get ffi => widget.ffi;
|
||||
|
||||
@override
|
||||
ValueChanged<int>? get onScaleChanged => widget.onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Smaller button size for mobile
|
||||
const smallBtnConstraints = BoxConstraints(minWidth: 32, minHeight: 32);
|
||||
|
||||
final sliderControl = Slider(
|
||||
value: scalePos,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: (CustomScaleControls.maxPercent - CustomScaleControls.minPercent).round(),
|
||||
label: '$scaleValue%',
|
||||
onChanged: onSliderChanged,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${translate("Scale custom")}: $scaleValue%',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 20,
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: translate('Decrease'),
|
||||
onPressed: () => nudgeScale(-1),
|
||||
),
|
||||
Expanded(child: sliderControl),
|
||||
IconButton(
|
||||
iconSize: 20,
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: smallBtnConstraints,
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: translate('Increase'),
|
||||
onPressed: () => nudgeScale(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -147,18 +147,22 @@ void setTemporaryPasswordLengthDialog(
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
void showServerSettings(OverlayDialogManager dialogManager,
|
||||
void Function(VoidCallback) setState) async {
|
||||
Map<String, dynamic> options = {};
|
||||
try {
|
||||
options = jsonDecode(await bind.mainGetOptions());
|
||||
} catch (e) {
|
||||
print("Invalid server config: $e");
|
||||
}
|
||||
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||
showServerSettingsWithValue(
|
||||
ServerConfig.fromOptions(options), dialogManager, setState);
|
||||
}
|
||||
|
||||
void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||
ServerConfig serverConfig,
|
||||
OverlayDialogManager dialogManager,
|
||||
void Function(VoidCallback)? upSetState) async {
|
||||
var isInProgress = false;
|
||||
final idCtrl = TextEditingController(text: serverConfig.idServer);
|
||||
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
|
||||
@@ -288,6 +292,7 @@ void showServerSettingsWithValue(
|
||||
if (await submit()) {
|
||||
close();
|
||||
showToast(translate('Successful'));
|
||||
upSetState?.call(() {});
|
||||
} else {
|
||||
showToast(translate('Failed'));
|
||||
}
|
||||
|
||||
@@ -1033,30 +1033,54 @@ class JobController {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
|
||||
void loadLastJob(Map<String, dynamic> evt) {
|
||||
Future<void> loadLastJob(Map<String, dynamic> evt) async {
|
||||
debugPrint("load last job: $evt");
|
||||
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
||||
// int id = int.parse(jobDetail['id']);
|
||||
String remote = jobDetail['remote'];
|
||||
String to = jobDetail['to'];
|
||||
bool showHidden = jobDetail['show_hidden'];
|
||||
int fileNum = jobDetail['file_num'];
|
||||
bool isRemote = jobDetail['is_remote'];
|
||||
final currJobId = JobController.jobID.next();
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
var jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
..isRemoteToLocal = isRemote
|
||||
..fileNum = fileNum
|
||||
..remote = remote
|
||||
..to = to
|
||||
..showHidden = showHidden
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
bind.sessionAddJob(
|
||||
bool isAutoStart = jobDetail['auto_start'] == true;
|
||||
int currJobId = -1;
|
||||
if (isAutoStart) {
|
||||
// Ensure jobDetail['id'] exists and is an int
|
||||
if (jobDetail.containsKey('id') &&
|
||||
jobDetail['id'] != null &&
|
||||
jobDetail['id'] is int) {
|
||||
currJobId = jobDetail['id'];
|
||||
}
|
||||
}
|
||||
if (currJobId < 0) {
|
||||
// If id is missing or invalid, disable auto-start and assign a new job id
|
||||
isAutoStart = false;
|
||||
currJobId = JobController.jobID.next();
|
||||
}
|
||||
|
||||
if (!isAutoStart) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
// Don't add to job table if not auto start on mobile.
|
||||
// Because mobile does not support job list view now.
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to job table if not auto start on desktop.
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
final jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
..isRemoteToLocal = isRemote
|
||||
..fileNum = fileNum
|
||||
..remote = remote
|
||||
..to = to
|
||||
..showHidden = showHidden
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
}
|
||||
|
||||
await bind.sessionAddJob(
|
||||
sessionId: sessionId,
|
||||
isRemote: isRemote,
|
||||
includeHidden: showHidden,
|
||||
@@ -1065,6 +1089,11 @@ class JobController {
|
||||
to: isRemote ? to : remote,
|
||||
fileNum: fileNum,
|
||||
);
|
||||
|
||||
if (isAutoStart) {
|
||||
await bind.sessionResumeJob(
|
||||
sessionId: sessionId, actId: currJobId, isRemote: isRemote);
|
||||
}
|
||||
}
|
||||
|
||||
void resumeJob(int jobId) {
|
||||
@@ -1095,6 +1124,11 @@ class JobController {
|
||||
}
|
||||
debugPrint("update folder files: $info");
|
||||
}
|
||||
|
||||
void clear() {
|
||||
jobTable.clear();
|
||||
jobResultListener.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class JobResultListener<T> {
|
||||
|
||||
@@ -42,8 +42,7 @@ class CanvasCoords {
|
||||
'scale': scale,
|
||||
'scrollX': scrollX,
|
||||
'scrollY': scrollY,
|
||||
'scrollStyle':
|
||||
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
|
||||
'scrollStyle': scrollStyle.toJson(),
|
||||
'size': {
|
||||
'w': size.width,
|
||||
'h': size.height,
|
||||
@@ -58,9 +57,7 @@ class CanvasCoords {
|
||||
model.scale = json['scale'];
|
||||
model.scrollX = json['scrollX'];
|
||||
model.scrollY = json['scrollY'];
|
||||
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
|
||||
? ScrollStyle.scrollauto
|
||||
: ScrollStyle.scrollbar;
|
||||
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.size = Size(json['size']['w'], json['size']['h']);
|
||||
return model;
|
||||
}
|
||||
@@ -375,6 +372,7 @@ class InputModel {
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||
int get trackpadSpeed => _trackpadSpeed;
|
||||
bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
||||
|
||||
InputModel(this.parent) {
|
||||
sessionId = parent.target!.sessionId;
|
||||
@@ -888,7 +886,7 @@ class InputModel {
|
||||
isPhysicalMouse.value = true;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,7 +1074,7 @@ class InputModel {
|
||||
_queryOtherWindowCoords = false;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1125,7 +1123,7 @@ class InputModel {
|
||||
void refreshMousePos() => handleMouse({
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
}, lastMousePos);
|
||||
}, lastMousePos, edgeScroll: useEdgeScroll);
|
||||
|
||||
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
|
||||
{
|
||||
@@ -1232,6 +1230,7 @@ class InputModel {
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
if (isViewCamera) return null;
|
||||
double x = offset.dx;
|
||||
@@ -1273,6 +1272,7 @@ class InputModel {
|
||||
onExit: onExit,
|
||||
buttons: evt['buttons'],
|
||||
moveCanvas: moveCanvas,
|
||||
edgeScroll: edgeScroll,
|
||||
);
|
||||
if (pos == null) {
|
||||
return null;
|
||||
@@ -1301,9 +1301,10 @@ class InputModel {
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final evtToPeer =
|
||||
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas);
|
||||
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
||||
if (evtToPeer != null) {
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
|
||||
@@ -1320,6 +1321,7 @@ class InputModel {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final ffiModel = parent.target!.ffiModel;
|
||||
CanvasCoords canvas =
|
||||
@@ -1348,8 +1350,16 @@ class InputModel {
|
||||
|
||||
y -= CanvasModel.topToEdge;
|
||||
x -= CanvasModel.leftToEdge;
|
||||
if (isMove && moveCanvas) {
|
||||
parent.target!.canvasModel.moveDesktopMouse(x, y);
|
||||
if (isMove) {
|
||||
final canvasModel = parent.target!.canvasModel;
|
||||
|
||||
if (edgeScroll) {
|
||||
canvasModel.edgeScrollMouse(x, y);
|
||||
} else if (moveCanvas) {
|
||||
canvasModel.moveDesktopMouse(x, y);
|
||||
}
|
||||
|
||||
canvasModel.updateLocalCursor(x, y);
|
||||
}
|
||||
|
||||
return _handlePointerDevicePos(
|
||||
@@ -1412,7 +1422,7 @@ class InputModel {
|
||||
var nearBottom = (canvas.size.height - y) < nearThr;
|
||||
final imageWidth = rect.width * canvas.scale;
|
||||
final imageHeight = rect.height * canvas.scale;
|
||||
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (canvas.scrollStyle != ScrollStyle.scrollauto) {
|
||||
x += imageWidth * canvas.scrollX;
|
||||
y += imageHeight * canvas.scrollY;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
@@ -29,6 +30,7 @@ import 'package:flutter_hbb/plugin/manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:image/image.dart' as img2;
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -36,6 +38,7 @@ import 'package:get/get.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:vector_math/vector_math.dart' show Vector2;
|
||||
|
||||
import '../common.dart';
|
||||
import '../utils/image.dart' as img;
|
||||
@@ -156,6 +159,8 @@ class FfiModel with ChangeNotifier {
|
||||
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
|
||||
bool get isPeerMobile => isPeerAndroid;
|
||||
|
||||
bool get isPeerLinux => _pi.platform == kPeerPlatformLinux;
|
||||
|
||||
bool get viewOnly => _viewOnly;
|
||||
bool get showMyCursor => _showMyCursor;
|
||||
|
||||
@@ -176,6 +181,9 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (isPeerLinux) {
|
||||
useDisplayScale = true;
|
||||
}
|
||||
int scale(int len, double s) {
|
||||
if (useDisplayScale) {
|
||||
return len.toDouble() ~/ s;
|
||||
@@ -931,11 +939,21 @@ class FfiModel with ChangeNotifier {
|
||||
/// Show a message box with [type], [title] and [text].
|
||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) {
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
{bool? hasCancel}) async {
|
||||
final showNoteEdit = parent.target != null &&
|
||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||
(title == "Connection Error" || type == "restarting") &&
|
||||
!hasRetry;
|
||||
if (showNoteEdit) {
|
||||
await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: parent.target!, type: type, title: title, text: text);
|
||||
closeConnection();
|
||||
} else {
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
}
|
||||
_timer?.cancel();
|
||||
if (hasRetry) {
|
||||
_timer = Timer(Duration(seconds: _reconnects), () {
|
||||
@@ -956,8 +974,30 @@ class FfiModel with ChangeNotifier {
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
|
||||
void showRelayHintDialog(SessionID sessionId, String type, String title,
|
||||
String text, OverlayDialogManager dialogManager, String peerId) {
|
||||
Future<void> showRelayHintDialog(
|
||||
SessionID sessionId,
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
OverlayDialogManager dialogManager,
|
||||
String peerId) async {
|
||||
var hint = "\n\n${translate('relay_hint_tip')}";
|
||||
if (text.contains("10054") || text.contains("104")) {
|
||||
hint = "";
|
||||
}
|
||||
final text2 = "${translate(text)}$hint";
|
||||
|
||||
if (parent.target != null &&
|
||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||
pi.isSet.isTrue) {
|
||||
if (await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: parent.target!, type: type, title: title, text: text2)) {
|
||||
return;
|
||||
}
|
||||
closeConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
|
||||
onClose() {
|
||||
closeConnection();
|
||||
@@ -966,13 +1006,10 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
final style =
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
|
||||
var hint = "\n\n${translate('relay_hint_tip')}";
|
||||
if (text.contains("10054") || text.contains("104")) {
|
||||
hint = "";
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, "${translate(text)}$hint"),
|
||||
content: msgboxContent(type, title, text2),
|
||||
actions: [
|
||||
dialogButton('Close', onPressed: onClose, isOutline: true),
|
||||
if (type == 'relay-hint')
|
||||
@@ -1044,28 +1081,108 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.length == 1) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display:
|
||||
pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: _rect!.width.toInt(),
|
||||
height: _rect!.height.toInt(),
|
||||
display: pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: displays[0].width,
|
||||
height: displays[0].height,
|
||||
);
|
||||
} else {
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display: i,
|
||||
width: displays[i].width.toInt(),
|
||||
height: displays[i].height.toInt(),
|
||||
width: displays[i].width,
|
||||
height: displays[i].height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _queryAuditGuid(String peerId) async {
|
||||
try {
|
||||
if (!mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection)) {
|
||||
return;
|
||||
}
|
||||
if (bind.sessionGetAuditGuid(sessionId: sessionId).isNotEmpty) {
|
||||
debugPrint('Get cached audit GUID');
|
||||
return;
|
||||
}
|
||||
final url = bind.sessionGetAuditServerSync(
|
||||
sessionId: sessionId, typ: "conn/active");
|
||||
if (url.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final initialConnSessionId =
|
||||
bind.sessionGetConnSessionId(sessionId: sessionId);
|
||||
final connType = switch (parent.target?.connType) {
|
||||
ConnType.defaultConn => 0,
|
||||
ConnType.fileTransfer => 1,
|
||||
ConnType.portForward => 2,
|
||||
ConnType.rdp => 2,
|
||||
ConnType.viewCamera => 3,
|
||||
ConnType.terminal => 4,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
const retryIntervals = [1, 1, 2, 2, 3, 3];
|
||||
|
||||
for (int attempt = 1; attempt <= retryIntervals.length; attempt++) {
|
||||
final currentConnSessionId =
|
||||
bind.sessionGetConnSessionId(sessionId: sessionId);
|
||||
if (currentConnSessionId != initialConnSessionId) {
|
||||
debugPrint('connSessionId changed, stopping audit GUID query');
|
||||
return;
|
||||
}
|
||||
|
||||
final fullUrl =
|
||||
'$url?id=$peerId&session_id=$currentConnSessionId&conn_type=$connType';
|
||||
|
||||
debugPrint(
|
||||
'Querying audit GUID, attempt $attempt/${retryIntervals.length}');
|
||||
try {
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse(fullUrl),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final guid = jsonDecode(response.body) as String?;
|
||||
if (guid != null && guid.isNotEmpty) {
|
||||
bind.sessionSetAuditGuid(sessionId: sessionId, guid: guid);
|
||||
debugPrint('Successfully retrieved audit GUID');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'Failed to query audit GUID. Status: ${response.statusCode}, Body: ${response.body}');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error querying audit GUID (attempt $attempt): $e');
|
||||
}
|
||||
|
||||
if (attempt < retryIntervals.length) {
|
||||
await Future.delayed(Duration(seconds: retryIntervals[attempt - 1]));
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Failed to retrieve audit GUID after ${retryIntervals.length} attempts');
|
||||
} catch (e) {
|
||||
debugPrint('Error in _queryAuditGuid: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
|
||||
_queryAuditGuid(peerId);
|
||||
|
||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
|
||||
@@ -1323,8 +1440,17 @@ class FfiModel with ChangeNotifier {
|
||||
d.cursorEmbedded = evt['cursor_embedded'] == 1;
|
||||
d.originalWidth = evt['original_width'] ?? kInvalidResolutionValue;
|
||||
d.originalHeight = evt['original_height'] ?? kInvalidResolutionValue;
|
||||
double v = (evt['scale']?.toDouble() ?? 100.0) / 100;
|
||||
d._scale = v > 1.0 ? v : 1.0;
|
||||
d._scale = 1.0;
|
||||
final scaledWidth = evt['scaled_width'];
|
||||
if (scaledWidth != null) {
|
||||
final sw = int.tryParse(scaledWidth.toString());
|
||||
if (sw != null && sw > 0 && d.width > 0) {
|
||||
d._scale = max(d.width.toDouble() / sw, 1.0);
|
||||
} else {
|
||||
debugPrint(
|
||||
"Invalid scaled_width ($scaledWidth) or width (${d.width}), using default scale 1.0");
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1665,6 +1791,7 @@ class ImageModel with ChangeNotifier {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
await parent.target?.canvasModel.updateViewStyle();
|
||||
await parent.target?.canvasModel.updateScrollStyle();
|
||||
await parent.target?.canvasModel.initializeEdgeScrollEdgeThickness();
|
||||
}
|
||||
if (parent.target != null) {
|
||||
await initializeCursorAndCanvas(parent.target!);
|
||||
@@ -1713,8 +1840,56 @@ class ImageModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
enum ScrollStyle {
|
||||
scrollbar,
|
||||
scrollauto,
|
||||
scrollbar(kRemoteScrollStyleBar),
|
||||
scrollauto(kRemoteScrollStyleAuto),
|
||||
scrolledge(kRemoteScrollStyleEdge);
|
||||
|
||||
const ScrollStyle(this.stringValue);
|
||||
|
||||
final String stringValue;
|
||||
|
||||
String toJson() {
|
||||
return name;
|
||||
}
|
||||
|
||||
static ScrollStyle fromJson(String json, [ScrollStyle? fallbackValue]) {
|
||||
switch (json) {
|
||||
case 'scrollbar':
|
||||
return scrollbar;
|
||||
case 'scrollauto':
|
||||
return scrollauto;
|
||||
case 'scrolledge':
|
||||
return scrolledge;
|
||||
}
|
||||
|
||||
if (fallbackValue != null) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
throw ArgumentError("Unknown ScrollStyle JSON value: '$json'");
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
static ScrollStyle fromString(String string, [ScrollStyle? fallbackValue]) {
|
||||
switch (string) {
|
||||
case kRemoteScrollStyleBar:
|
||||
return scrollbar;
|
||||
case kRemoteScrollStyleAuto:
|
||||
return scrollauto;
|
||||
case kRemoteScrollStyleEdge:
|
||||
return scrolledge;
|
||||
}
|
||||
|
||||
if (fallbackValue != null) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
throw ArgumentError("Unknown ScrollStyle string value: '$string'");
|
||||
}
|
||||
}
|
||||
|
||||
class ViewStyle {
|
||||
@@ -1789,6 +1964,60 @@ class ViewStyle {
|
||||
}
|
||||
}
|
||||
|
||||
enum EdgeScrollState {
|
||||
inactive,
|
||||
armed,
|
||||
active,
|
||||
}
|
||||
|
||||
class EdgeScrollFallbackState {
|
||||
final CanvasModel _owner;
|
||||
|
||||
late Ticker _ticker;
|
||||
|
||||
Duration _lastTotalElapsed = Duration.zero;
|
||||
bool _nextEventIsFirst = true;
|
||||
Vector2 _encroachment = Vector2.zero();
|
||||
|
||||
EdgeScrollFallbackState(this._owner, TickerProvider tickerProvider) {
|
||||
_ticker = tickerProvider.createTicker(emitTick);
|
||||
}
|
||||
|
||||
void setEncroachment(Vector2 encroachment) {
|
||||
_encroachment = encroachment;
|
||||
}
|
||||
|
||||
void emitTick(Duration totalElapsed) {
|
||||
if (_nextEventIsFirst) {
|
||||
_lastTotalElapsed = totalElapsed;
|
||||
_nextEventIsFirst = false;
|
||||
} else {
|
||||
final thisTickElapsed = totalElapsed - _lastTotalElapsed;
|
||||
|
||||
const double kFrameTime = 1000.0 / 60.0;
|
||||
const double kSpeedFactor = 0.1;
|
||||
|
||||
var delta = _encroachment *
|
||||
(kSpeedFactor * thisTickElapsed.inMilliseconds / kFrameTime);
|
||||
|
||||
_owner.performEdgeScroll(delta);
|
||||
|
||||
_lastTotalElapsed = totalElapsed;
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (!_ticker.isActive) {
|
||||
_nextEventIsFirst = true;
|
||||
_ticker.start();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_ticker.stop();
|
||||
}
|
||||
}
|
||||
|
||||
class CanvasModel with ChangeNotifier {
|
||||
// image offset of canvas
|
||||
double _x = 0;
|
||||
@@ -1810,6 +2039,15 @@ class CanvasModel with ChangeNotifier {
|
||||
// scroll offset y percent
|
||||
double _scrollY = 0.0;
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
// edge scroll mode: trigger scrolling when the cursor is close to the edge of the view
|
||||
int _edgeScrollEdgeThickness = 100;
|
||||
// tracks whether edge scroll should be active, prevents spurious
|
||||
// scrolling when the cursor enters the view from outside
|
||||
EdgeScrollState _edgeScrollState = EdgeScrollState.inactive;
|
||||
// fallback strategy for when Bump Mouse isn't available
|
||||
late EdgeScrollFallbackState _edgeScrollFallbackState;
|
||||
// to avoid hammering a non-functional Bump Mouse
|
||||
bool _bumpMouseIsWorking = true;
|
||||
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
|
||||
Timer? _timerMobileFocusCanvasCursor;
|
||||
@@ -1840,9 +2078,18 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
_resetScroll() => setScrollPercent(0.0, 0.0);
|
||||
|
||||
setScrollPercent(double x, double y) {
|
||||
_scrollX = x;
|
||||
_scrollY = y;
|
||||
void setScrollPercent(double x, double y) {
|
||||
_scrollX = x.isFinite ? x : 0.0;
|
||||
_scrollY = y.isFinite ? y : 0.0;
|
||||
}
|
||||
|
||||
void pushScrollPositionToUI(double scrollPixelX, double scrollPixelY) {
|
||||
if (_horizontal.hasClients) {
|
||||
_horizontal.jumpTo(scrollPixelX);
|
||||
}
|
||||
if (_vertical.hasClients) {
|
||||
_vertical.jumpTo(scrollPixelY);
|
||||
}
|
||||
}
|
||||
|
||||
ScrollController get scrollHorizontal => _horizontal;
|
||||
@@ -1957,30 +2204,47 @@ class CanvasModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
tryUpdateScrollStyle(Duration duration, String? style) async {
|
||||
if (_scrollStyle != ScrollStyle.scrollbar) return;
|
||||
if (_scrollStyle == ScrollStyle.scrollauto) return;
|
||||
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||
if (style != kRemoteViewStyleOriginal && style != kRemoteViewStyleCustom) {
|
||||
return;
|
||||
}
|
||||
|
||||
_resetScroll();
|
||||
|
||||
Future.delayed(duration, () async {
|
||||
updateScrollPercent();
|
||||
});
|
||||
}
|
||||
|
||||
updateScrollStyle() async {
|
||||
Future<void> updateScrollStyle() async {
|
||||
final style = await bind.sessionGetScrollStyle(sessionId: sessionId);
|
||||
if (style == kRemoteScrollStyleBar) {
|
||||
_scrollStyle = ScrollStyle.scrollbar;
|
||||
|
||||
_scrollStyle =
|
||||
style != null ? ScrollStyle.fromString(style) : ScrollStyle.scrollauto;
|
||||
|
||||
if (_scrollStyle != ScrollStyle.scrollauto) {
|
||||
_resetScroll();
|
||||
} else {
|
||||
_scrollStyle = ScrollStyle.scrollauto;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
update(double x, double y, double scale) {
|
||||
Future<void> initializeEdgeScrollEdgeThickness() async {
|
||||
final savedValue =
|
||||
await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId);
|
||||
|
||||
if (savedValue != null) {
|
||||
_edgeScrollEdgeThickness = savedValue;
|
||||
}
|
||||
}
|
||||
|
||||
void updateEdgeScrollEdgeThickness(int newThickness) {
|
||||
_edgeScrollEdgeThickness = newThickness;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void update(double x, double y, double scale) {
|
||||
_x = x;
|
||||
_y = y;
|
||||
_scale = scale;
|
||||
@@ -2007,7 +2271,33 @@ class CanvasModel with ChangeNotifier {
|
||||
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||
static double get tabBarHeight => stateGlobal.tabBarHeight;
|
||||
|
||||
moveDesktopMouse(double x, double y) {
|
||||
void activateLocalCursor() {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = false;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateLocalCursor(double x, double y) {
|
||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||
// Draw cursor if is not desktop.
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
parent.target!.cursorModel.moveLocal(x, y);
|
||||
} else {
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = false;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void moveDesktopMouse(double x, double y) {
|
||||
if (size.width == 0 || size.height == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -2036,24 +2326,128 @@ class CanvasModel with ChangeNotifier {
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||
// Draw cursor if is not desktop.
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
parent.target!.cursorModel.moveLocal(x, y);
|
||||
void initializeEdgeScrollFallback(TickerProvider tickerProvider) {
|
||||
_edgeScrollFallbackState = EdgeScrollFallbackState(this, tickerProvider);
|
||||
}
|
||||
|
||||
void disableEdgeScroll() {
|
||||
_edgeScrollState = EdgeScrollState.inactive;
|
||||
cancelEdgeScroll();
|
||||
}
|
||||
|
||||
void rearmEdgeScroll() {
|
||||
_edgeScrollState = EdgeScrollState.armed;
|
||||
}
|
||||
|
||||
void cancelEdgeScroll() {
|
||||
_edgeScrollFallbackState.stop();
|
||||
}
|
||||
|
||||
(Vector2, Vector2) getScrollInfo() {
|
||||
final scrollPixel = Vector2(
|
||||
_horizontal.hasClients ? _horizontal.position.pixels : 0,
|
||||
_vertical.hasClients ? _vertical.position.pixels : 0);
|
||||
|
||||
final max = Vector2(
|
||||
_horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0,
|
||||
_vertical.hasClients ? _vertical.position.maxScrollExtent : 0);
|
||||
|
||||
return (scrollPixel, max);
|
||||
}
|
||||
|
||||
void edgeScrollMouse(double x, double y) async {
|
||||
if ((_edgeScrollState == EdgeScrollState.inactive) ||
|
||||
(size.width == 0 || size.height == 0) ||
|
||||
!(_horizontal.hasClients || _vertical.hasClients)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_edgeScrollState == EdgeScrollState.armed) {
|
||||
// Edge scroll is armed to become active once the cursor
|
||||
// is observed within the rectangle interior to the
|
||||
// edge scroll regions. If the user has just moved the
|
||||
// cursor in from outside of the window, edge scrolling
|
||||
// doesn't happen yet.
|
||||
final clientArea = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
|
||||
final innerZone = clientArea.deflate(_edgeScrollEdgeThickness.toDouble());
|
||||
|
||||
if (innerZone.contains(Offset(x, y))) {
|
||||
_edgeScrollState = EdgeScrollState.active;
|
||||
} else {
|
||||
try {
|
||||
RemoteCursorMovedState.find(id).value = false;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
// Not yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var dxOffset = 0.0;
|
||||
var dyOffset = 0.0;
|
||||
|
||||
if (x < _edgeScrollEdgeThickness) {
|
||||
dxOffset = x - _edgeScrollEdgeThickness;
|
||||
} else if (x >= size.width - _edgeScrollEdgeThickness) {
|
||||
dxOffset = x - (size.width - _edgeScrollEdgeThickness);
|
||||
}
|
||||
|
||||
if (y < _edgeScrollEdgeThickness) {
|
||||
dyOffset = y - _edgeScrollEdgeThickness;
|
||||
} else if (y >= size.height - _edgeScrollEdgeThickness) {
|
||||
dyOffset = y - (size.height - _edgeScrollEdgeThickness);
|
||||
}
|
||||
|
||||
var encroachment = Vector2(dxOffset, dyOffset);
|
||||
|
||||
var (scrollPixel, max) = getScrollInfo();
|
||||
|
||||
encroachment.clamp(-scrollPixel, max - scrollPixel);
|
||||
|
||||
if (encroachment.length2 == 0) {
|
||||
_edgeScrollFallbackState.stop();
|
||||
} else {
|
||||
var bumpAmount = -encroachment;
|
||||
|
||||
// Round away from 0: this ensures that the mouse will be bumped clear of
|
||||
// whichever edge scroll zone(s) it is in
|
||||
bumpAmount.x += bumpAmount.x.sign * 0.5;
|
||||
bumpAmount.y += bumpAmount.y.sign * 0.5;
|
||||
|
||||
var bumpMouseSucceeded = _bumpMouseIsWorking &&
|
||||
(await rustDeskWinManager.call(WindowType.Main, kWindowBumpMouse,
|
||||
{"dx": bumpAmount.x.round(), "dy": bumpAmount.y.round()}))
|
||||
.result;
|
||||
|
||||
if (bumpMouseSucceeded) {
|
||||
performEdgeScroll(encroachment);
|
||||
} else {
|
||||
// If we can't BumpMouse, then we switch to slower scrolling with autorepeat
|
||||
|
||||
// Don't keep hammering BumpMouse if it's not working.
|
||||
_bumpMouseIsWorking = false;
|
||||
|
||||
// Keep scrolling as long as the user is overtop of an edge.
|
||||
_edgeScrollFallbackState.setEncroachment(encroachment);
|
||||
_edgeScrollFallbackState.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set scale(v) {
|
||||
_scale = v;
|
||||
void performEdgeScroll(Vector2 delta) {
|
||||
var (scrollPixel, max) = getScrollInfo();
|
||||
|
||||
scrollPixel += delta;
|
||||
|
||||
scrollPixel.clamp(Vector2.zero(), max);
|
||||
|
||||
var scrollPixelPercent = scrollPixel.clone();
|
||||
|
||||
scrollPixelPercent.divide(max);
|
||||
scrollPixelPercent.scale(100.0);
|
||||
|
||||
setScrollPercent(scrollPixelPercent.x, scrollPixelPercent.y);
|
||||
pushScrollPositionToUI(scrollPixel.x, scrollPixel.y);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -2590,9 +2984,10 @@ class CursorModel with ChangeNotifier {
|
||||
var cx = r.center.dx;
|
||||
var cy = r.center.dy;
|
||||
var tryMoveCanvasX = false;
|
||||
final displayRect = parent.target?.ffiModel.rect;
|
||||
if (dx > 0) {
|
||||
final maxCanvasCanMove = _displayOriginX +
|
||||
(parent.target?.imageModel.image!.width ?? 1280) -
|
||||
(displayRect?.width ?? 1280) -
|
||||
r.right.roundToDouble();
|
||||
tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0;
|
||||
if (tryMoveCanvasX) {
|
||||
@@ -2614,7 +3009,7 @@ class CursorModel with ChangeNotifier {
|
||||
var tryMoveCanvasY = false;
|
||||
if (dy > 0) {
|
||||
final mayCanvasCanMove = _displayOriginY +
|
||||
(parent.target?.imageModel.image!.height ?? 720) -
|
||||
(displayRect?.height ?? 720) -
|
||||
r.bottom.roundToDouble();
|
||||
tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0;
|
||||
if (tryMoveCanvasY) {
|
||||
@@ -3035,7 +3430,6 @@ class FFI {
|
||||
var version = '';
|
||||
var connType = ConnType.defaultConn;
|
||||
var closed = false;
|
||||
var auditNote = '';
|
||||
|
||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||
late final dialogManager = OverlayDialogManager();
|
||||
@@ -3126,7 +3520,6 @@ class FFI {
|
||||
List<int>? displays,
|
||||
}) {
|
||||
closed = false;
|
||||
auditNote = '';
|
||||
if (isMobile) mobileReset();
|
||||
assert(
|
||||
(!(isPortForward && isViewCamera)) &&
|
||||
@@ -3318,6 +3711,7 @@ class FFI {
|
||||
dialogManager.dismissAll();
|
||||
await canvasModel.updateViewStyle();
|
||||
await canvasModel.updateScrollStyle();
|
||||
await canvasModel.initializeEdgeScrollEdgeThickness();
|
||||
for (final cb in imageModel.callbacksOnFirstImage) {
|
||||
cb(id);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ class TerminalModel with ChangeNotifier {
|
||||
|
||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||
|
||||
Future<void> _handleInput(String data) async {
|
||||
// If we press the `Enter` button on Android,
|
||||
// `data` can be '\r' or '\n' when using different keyboards.
|
||||
@@ -68,6 +70,10 @@ class TerminalModel with ChangeNotifier {
|
||||
if (w > 0 && h > 0 && pw > 0 && ph > 0) {
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal resized to ${w}x$h (pixel: ${pw}x$ph)');
|
||||
|
||||
// This piece of code must be placed before the conditional check in order to initialize properly.
|
||||
onResizeExternal?.call(w, h, pw, ph);
|
||||
|
||||
if (_terminalOpened) {
|
||||
// Notify remote terminal of resize
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/platform_model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
export 'package:http/http.dart' show Response;
|
||||
|
||||
enum HttpMethod { get, post, put, delete }
|
||||
@@ -15,11 +17,19 @@ class HttpService {
|
||||
}) async {
|
||||
headers ??= {'Content-Type': 'application/json'};
|
||||
|
||||
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
|
||||
final isProxy = await bind.mainGetProxyStatus();
|
||||
// Use Rust HTTP implementation for non-web platforms for consistency.
|
||||
var useFlutterHttp = (isWeb || kIsWeb);
|
||||
if (!useFlutterHttp) {
|
||||
final enableFlutterHttpOnRust =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableFlutterHttpOnRust);
|
||||
// Use flutter http if:
|
||||
// Not `enableFlutterHttpOnRust` and no proxy is set
|
||||
useFlutterHttp =
|
||||
!(enableFlutterHttpOnRust || await bind.mainGetProxyStatus());
|
||||
}
|
||||
|
||||
if (!isProxy) {
|
||||
return await _pollFultterHttp(url, method, headers: headers, body: body);
|
||||
if (useFlutterHttp) {
|
||||
return await _pollFlutterHttp(url, method, headers: headers, body: body);
|
||||
}
|
||||
|
||||
String headersJson = jsonEncode(headers);
|
||||
@@ -34,7 +44,7 @@ class HttpService {
|
||||
return _parseHttpResponse(resJson);
|
||||
}
|
||||
|
||||
Future<http.Response> _pollFultterHttp(
|
||||
Future<http.Response> _pollFlutterHttp(
|
||||
Uri url,
|
||||
HttpMethod method, {
|
||||
Map<String, String>? headers,
|
||||
@@ -87,7 +97,8 @@ class HttpService {
|
||||
int statusCode = parsedJson['status_code'];
|
||||
return http.Response(body, statusCode, headers: headers);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to parse response: $e');
|
||||
print('Failed to parse response\n$responseJson\nError:\n$e');
|
||||
throw Exception('Failed to parse response.\n$responseJson');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +475,11 @@ class RustDeskMultiWindowManager {
|
||||
final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1;
|
||||
if (shouldSavePos) {
|
||||
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
try {
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to save window position of $wId, $e');
|
||||
}
|
||||
}
|
||||
try {
|
||||
await WindowController.fromWindowId(wId).setPreventClose(false);
|
||||
|
||||
@@ -13,8 +13,18 @@ class RdPlatformChannel {
|
||||
|
||||
static RdPlatformChannel get instance => _windowUtil;
|
||||
|
||||
final MethodChannel _osxMethodChannel =
|
||||
MethodChannel("org.rustdesk.rustdesk/macos");
|
||||
final MethodChannel _hostMethodChannel =
|
||||
MethodChannel("org.rustdesk.rustdesk/host");
|
||||
|
||||
/// Bump the position of the mouse cursor, if applicable
|
||||
Future<bool> bumpMouse({required int dx, required int dy}) async {
|
||||
// No debug output; this call is too chatty.
|
||||
|
||||
bool? result = await _hostMethodChannel
|
||||
.invokeMethod("bumpMouse", {"dx": dx, "dy": dy});
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
/// Change the theme of the system window
|
||||
Future<void> changeSystemWindowTheme(SystemWindowTheme theme) {
|
||||
@@ -23,13 +33,13 @@ class RdPlatformChannel {
|
||||
print(
|
||||
"[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}");
|
||||
}
|
||||
return _osxMethodChannel
|
||||
return _hostMethodChannel
|
||||
.invokeMethod("setWindowTheme", {"themeName": theme.name});
|
||||
}
|
||||
|
||||
/// Terminate .app manually.
|
||||
Future<void> terminate() {
|
||||
assert(isMacOS);
|
||||
return _osxMethodChannel.invokeMethod("terminate");
|
||||
return _hostMethodChannel.invokeMethod("terminate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,7 +812,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetAppNameSync({dynamic hint}) {
|
||||
return 'RustDesk';
|
||||
return js.context.callMethod('getByName', ['app-name']);
|
||||
}
|
||||
|
||||
String mainUriPrefixSync({dynamic hint}) {
|
||||
@@ -1609,23 +1609,28 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
bool isCustomClient({dynamic hint}) {
|
||||
return false;
|
||||
// is_custom_client() checks if app name is not "RustDesk"
|
||||
return mainGetAppNameSync(hint: hint) != "RustDesk";
|
||||
}
|
||||
|
||||
bool isDisableSettings({dynamic hint}) {
|
||||
return false;
|
||||
// Checks HARD_SETTINGS["disable-settings"] == "Y"
|
||||
return mainGetHardOption(key: "disable-settings", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableAb({dynamic hint}) {
|
||||
return false;
|
||||
// Checks HARD_SETTINGS["disable-ab"] == "Y"
|
||||
return mainGetHardOption(key: "disable-ab", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableGroupPanel({dynamic hint}) {
|
||||
return false;
|
||||
// Checks LocalConfig::get_option("disable-group-panel") == "Y"
|
||||
return mainGetLocalOption(key: "disable-group-panel", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableAccount({dynamic hint}) {
|
||||
return false;
|
||||
// Checks HARD_SETTINGS["disable-account"] == "Y"
|
||||
return mainGetHardOption(key: "disable-account", hint: hint) == "Y";
|
||||
}
|
||||
|
||||
bool isDisableInstallation({dynamic hint}) {
|
||||
@@ -1748,7 +1753,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetHardOption({required String key, dynamic hint}) {
|
||||
throw UnimplementedError("mainGetHardOption");
|
||||
return mainGetLocalOption(key: key, hint: hint);
|
||||
}
|
||||
|
||||
Future<void> mainCheckHwcodec({dynamic hint}) {
|
||||
@@ -1821,7 +1826,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetBuildinOption({required String key, dynamic hint}) {
|
||||
return '';
|
||||
return mainGetLocalOption(key: key, hint: hint);
|
||||
}
|
||||
|
||||
String installInstallOptions({dynamic hint}) {
|
||||
@@ -1979,5 +1984,41 @@ class RustdeskImpl {
|
||||
]));
|
||||
}
|
||||
|
||||
Future<int?> sessionGetEdgeScrollEdgeThickness(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
final thickness = js.context.callMethod(
|
||||
'getByName', ['option:session', 'edge-scroll-edge-thickness']);
|
||||
return Future(() => int.tryParse(thickness) ?? 100);
|
||||
}
|
||||
|
||||
Future<void> sessionSetEdgeScrollEdgeThickness(
|
||||
{required UuidValue sessionId, required int value, dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName',
|
||||
['option:session', 'edge-scroll-edge-thickness', value.toString()]));
|
||||
}
|
||||
|
||||
String sessionGetConnSessionId({required UuidValue sessionId, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['conn_session_id']);
|
||||
}
|
||||
|
||||
bool willSessionCloseCloseSession(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String sessionGetLastAuditNote({required UuidValue sessionId, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['last_audit_note']);
|
||||
}
|
||||
|
||||
Future<void> sessionSetAuditGuid(
|
||||
{required UuidValue sessionId, required String guid, dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['audit_guid', guid]));
|
||||
}
|
||||
|
||||
String sessionGetAuditGuid({required UuidValue sessionId, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['audit_guid']);
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"bump_mouse.cc"
|
||||
"bump_mouse_x11.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
|
||||
18
flutter/linux/bump_mouse.cc
Normal file
18
flutter/linux/bump_mouse.cc
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "bump_mouse.h"
|
||||
|
||||
#include "bump_mouse_x11.h"
|
||||
|
||||
#include <gdk/gdkx.h>
|
||||
|
||||
bool bump_mouse(int dx, int dy)
|
||||
{
|
||||
GdkDisplay *display = gdk_display_get_default();
|
||||
|
||||
if (GDK_IS_X11_DISPLAY(display)) {
|
||||
return bump_mouse_x11(dx, dy);
|
||||
}
|
||||
else {
|
||||
// Don't know how to support this.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
3
flutter/linux/bump_mouse.h
Normal file
3
flutter/linux/bump_mouse.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
bool bump_mouse(int dx, int dy);
|
||||
30
flutter/linux/bump_mouse_x11.cc
Normal file
30
flutter/linux/bump_mouse_x11.cc
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "bump_mouse.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <gdk/gdkx.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
bool bump_mouse_x11(int dx, int dy)
|
||||
{
|
||||
GdkDevice *mouse_device;
|
||||
|
||||
#if GTK_CHECK_VERSION(3, 20, 0)
|
||||
auto seat = gdk_display_get_default_seat(gdk_display_get_default());
|
||||
|
||||
mouse_device = gdk_seat_get_pointer(seat);
|
||||
#else
|
||||
auto devman = gdk_display_get_device_manager(gdk_display_get_default());
|
||||
|
||||
mouse_device = gdk_device_manager_get_client_pointer(devman);
|
||||
#endif
|
||||
|
||||
GdkScreen *screen;
|
||||
gint x, y;
|
||||
|
||||
gdk_device_get_position(mouse_device, &screen, &x, &y);
|
||||
gdk_device_warp(mouse_device, screen, x + dx, y + dy);
|
||||
|
||||
return true;
|
||||
}
|
||||
3
flutter/linux/bump_mouse_x11.h
Normal file
3
flutter/linux/bump_mouse_x11.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
bool bump_mouse_x11(int dx, int dy);
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include "bump_mouse.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
@@ -10,10 +12,13 @@
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
FlMethodChannel* host_channel;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data);
|
||||
|
||||
GtkWidget *find_gl_area(GtkWidget *widget);
|
||||
void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
|
||||
|
||||
@@ -24,10 +29,11 @@ GtkWidget *find_gl_area(GtkWidget *widget);
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
gtk_window_set_decorated(window, FALSE);
|
||||
// try setting icon for rustdesk, which uses the system cache
|
||||
// try setting icon for rustdesk, which uses the system cache
|
||||
GtkIconTheme* theme = gtk_icon_theme_get_default();
|
||||
gint icons[4] = {256, 128, 64, 32};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -87,6 +93,17 @@ static void my_application_activate(GApplication* application) {
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||
self->host_channel = fl_method_channel_new(
|
||||
fl_engine_get_binary_messenger(fl_view_get_engine(view)),
|
||||
"org.rustdesk.rustdesk/host",
|
||||
FL_METHOD_CODEC(codec));
|
||||
fl_method_channel_set_method_call_handler(
|
||||
self->host_channel,
|
||||
host_channel_call_handler,
|
||||
self,
|
||||
nullptr);
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
@@ -113,6 +130,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
g_clear_object(&self->host_channel);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
@@ -131,6 +149,61 @@ MyApplication* my_application_new() {
|
||||
nullptr));
|
||||
}
|
||||
|
||||
void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data)
|
||||
{
|
||||
if (strcmp(fl_method_call_get_name(method_call), "bumpMouse") == 0) {
|
||||
FlValue *args = fl_method_call_get_args(method_call);
|
||||
|
||||
FlValue *dxValue = nullptr;
|
||||
FlValue *dyValue = nullptr;
|
||||
|
||||
switch (fl_value_get_type(args))
|
||||
{
|
||||
case FL_VALUE_TYPE_MAP:
|
||||
{
|
||||
dxValue = fl_value_lookup_string(args, "dx");
|
||||
dyValue = fl_value_lookup_string(args, "dy");
|
||||
|
||||
break;
|
||||
}
|
||||
case FL_VALUE_TYPE_LIST:
|
||||
{
|
||||
int listSize = fl_value_get_length(args);
|
||||
|
||||
dxValue = (listSize >= 1) ? fl_value_get_list_value(args, 0) : nullptr;
|
||||
dyValue = (listSize >= 2) ? fl_value_get_list_value(args, 1) : nullptr;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
int dx = 0, dy = 0;
|
||||
|
||||
if (dxValue && (fl_value_get_type(dxValue) == FL_VALUE_TYPE_INT)) {
|
||||
dx = fl_value_get_int(dxValue);
|
||||
}
|
||||
|
||||
if (dyValue && (fl_value_get_type(dyValue) == FL_VALUE_TYPE_INT)) {
|
||||
dy = fl_value_get_int(dyValue);
|
||||
}
|
||||
|
||||
bool result = bump_mouse(dx, dy);
|
||||
|
||||
FlValue *result_value = fl_value_new_bool(result);
|
||||
|
||||
GError *error = nullptr;
|
||||
|
||||
if (!fl_method_call_respond_success(method_call, result_value, &error)) {
|
||||
g_warning("Failed to send Flutter Platform Channel response: %s", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
fl_value_unref(result_value);
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget *find_gl_area(GtkWidget *widget)
|
||||
{
|
||||
if (GTK_IS_GL_AREA(widget)) {
|
||||
@@ -160,7 +233,7 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view)
|
||||
GtkWidget *gl_area = NULL;
|
||||
|
||||
printf("Try setting transparent\n");
|
||||
|
||||
|
||||
gl_area = find_gl_area(GTK_WIDGET(view));
|
||||
if (gl_area != NULL) {
|
||||
gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE);
|
||||
|
||||
@@ -29,7 +29,7 @@ class MainFlutterWindow: NSWindow {
|
||||
// register self method handler
|
||||
let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin")
|
||||
setMethodHandler(registrar: registrar)
|
||||
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
||||
@@ -50,22 +50,22 @@ class MainFlutterWindow: NSWindow {
|
||||
WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin"))
|
||||
TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin"))
|
||||
}
|
||||
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
|
||||
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
|
||||
super.order(place, relativeTo: otherWin)
|
||||
hiddenWindowAtLaunch()
|
||||
}
|
||||
|
||||
|
||||
/// Override window theme.
|
||||
public func setWindowInterfaceMode(window: NSWindow, themeName: String) {
|
||||
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
||||
}
|
||||
|
||||
|
||||
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger)
|
||||
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
||||
channel.setMethodCallHandler({
|
||||
(call, result) -> Void in
|
||||
switch call.method {
|
||||
@@ -99,6 +99,58 @@ class MainFlutterWindow: NSWindow {
|
||||
result(granted)
|
||||
})
|
||||
break
|
||||
case "bumpMouse":
|
||||
var dx = 0
|
||||
var dy = 0
|
||||
|
||||
if let argMap = call.arguments as? [String: Any] {
|
||||
dx = (argMap["dx"] as? Int) ?? 0
|
||||
dy = (argMap["dy"] as? Int) ?? 0
|
||||
}
|
||||
else if let argList = call.arguments as? [Any] {
|
||||
dx = argList.count >= 1 ? (argList[0] as? Int) ?? 0 : 0
|
||||
dy = argList.count >= 2 ? (argList[1] as? Int) ?? 0 : 0
|
||||
}
|
||||
|
||||
var mouseLoc: CGPoint
|
||||
|
||||
if let dummyEvent = CGEvent(source: nil) { // can this ever fail?
|
||||
mouseLoc = dummyEvent.location
|
||||
}
|
||||
else if let screenFrame = NSScreen.screens.first?.frame {
|
||||
// NeXTStep: Origin is lower-left of primary screen, positive is up
|
||||
// Cocoa Core Graphics: Origin is upper-left of primary screen, positive is down
|
||||
let nsMouseLoc = NSEvent.mouseLocation
|
||||
|
||||
mouseLoc = CGPoint(
|
||||
x: nsMouseLoc.x,
|
||||
y: NSHeight(screenFrame) - nsMouseLoc.y)
|
||||
}
|
||||
else {
|
||||
result(false)
|
||||
break
|
||||
}
|
||||
|
||||
let newLoc = CGPoint(x: mouseLoc.x + CGFloat(dx), y: mouseLoc.y + CGFloat(dy))
|
||||
|
||||
CGDisplayMoveCursorToPoint(0, newLoc)
|
||||
|
||||
// By default, Cocoa suppresses mouse events briefly after a call to warp the
|
||||
// cursor to a new location. This is good if you want to draw the user's
|
||||
// attention to the fact that the mouse is now in a particular location, but
|
||||
// it's bad in this case; we get called as part of the handling of edge
|
||||
// scrolling, which means the mouse is typically still in motion, and we want
|
||||
// the cursor to keep moving smoothly uninterrupted.
|
||||
//
|
||||
// This function's main action is to toggle whether the mouse cursor is
|
||||
// associated with the mouse position, but setting it to true when it's
|
||||
// already true has the side-effect of cancelling this motion suppression.
|
||||
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
||||
|
||||
result(true)
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.4.3+60
|
||||
version: 1.4.4+62
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -109,6 +109,7 @@ dependencies:
|
||||
xterm: 4.0.0
|
||||
sqflite: 2.2.0
|
||||
google_fonts: ^6.2.1
|
||||
vector_math: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <desktop_multi_window/desktop_multi_window_plugin.h>
|
||||
#include <texture_rgba_renderer/texture_rgba_renderer_plugin_c_api.h>
|
||||
#include <flutter_gpu_texture_renderer/flutter_gpu_texture_renderer_plugin_c_api.h>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter/event_channel.h>
|
||||
#include <flutter/event_sink.h>
|
||||
#include <flutter/event_stream_handler_functions.h>
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
|
||||
#include "win32_desktop.h"
|
||||
|
||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||
: project_(project) {}
|
||||
|
||||
@@ -29,6 +40,48 @@ bool FlutterWindow::OnCreate() {
|
||||
return false;
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
|
||||
flutter::MethodChannel<> channel(
|
||||
flutter_controller_->engine()->messenger(),
|
||||
"org.rustdesk.rustdesk/host",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
channel.SetMethodCallHandler(
|
||||
[](const flutter::MethodCall<>& call, std::unique_ptr<flutter::MethodResult<>> result) {
|
||||
if (call.method_name() == "bumpMouse") {
|
||||
auto arguments = call.arguments();
|
||||
|
||||
int dx = 0, dy = 0;
|
||||
|
||||
if (std::holds_alternative<flutter::EncodableMap>(*arguments)) {
|
||||
auto argsMap = std::get<flutter::EncodableMap>(*arguments);
|
||||
|
||||
auto dxIt = argsMap.find(flutter::EncodableValue("dx"));
|
||||
auto dyIt = argsMap.find(flutter::EncodableValue("dy"));
|
||||
|
||||
if ((dxIt != argsMap.end()) && std::holds_alternative<int>(dxIt->second)) {
|
||||
dx = std::get<int>(dxIt->second);
|
||||
}
|
||||
if ((dyIt != argsMap.end()) && std::holds_alternative<int>(dyIt->second)) {
|
||||
dy = std::get<int>(dyIt->second);
|
||||
}
|
||||
} else if (std::holds_alternative<flutter::EncodableList>(*arguments)) {
|
||||
auto argsList = std::get<flutter::EncodableList>(*arguments);
|
||||
|
||||
if ((argsList.size() >= 1) && std::holds_alternative<int>(argsList[0])) {
|
||||
dx = std::get<int>(argsList[0]);
|
||||
}
|
||||
if ((argsList.size() >= 2) && std::holds_alternative<int>(argsList[1])) {
|
||||
dy = std::get<int>(argsList[1]);
|
||||
}
|
||||
}
|
||||
|
||||
bool succeeded = Win32Desktop::BumpMouse(dx, dy);
|
||||
|
||||
result->Success(succeeded);
|
||||
}
|
||||
});
|
||||
|
||||
DesktopMultiWindowSetWindowCreatedCallback([](void *controller) {
|
||||
auto *flutter_view_controller =
|
||||
reinterpret_cast<flutter::FlutterViewController *>(controller);
|
||||
|
||||
@@ -66,4 +66,17 @@ namespace Win32Desktop
|
||||
size.width = std::min(size.width, workarea_bottom_right.x - origin.x);
|
||||
size.height = std::min(size.height, workarea_bottom_right.y - origin.y);
|
||||
}
|
||||
|
||||
bool BumpMouse(int dx, int dy)
|
||||
{
|
||||
POINT pos;
|
||||
|
||||
if (GetCursorPos(&pos))
|
||||
{
|
||||
SetCursorPos(pos.x + dx, pos.y + dy);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Win32Desktop
|
||||
{
|
||||
void GetWorkArea(Win32Window::Point& origin, Win32Window::Size& size);
|
||||
void FitToWorkArea(Win32Window::Point& origin, Win32Window::Size& size);
|
||||
bool BumpMouse(int dx, int dy);
|
||||
}
|
||||
|
||||
#endif // RUNNER_WIN32_DESKTOP_H_
|
||||
|
||||
Submodule libs/hbb_common updated: 5ed0afde08...a86eda749e
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use hbb_common::{bail, ResultType};
|
||||
use std::{io, ptr::null_mut};
|
||||
use winapi::{
|
||||
|
||||
@@ -10,7 +10,7 @@ authors = ["Ram <quadrupleslap@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||
mediacodec = ["ndk"]
|
||||
linux-pkg-config = ["dep:pkg-config"]
|
||||
hwcodec = ["dep:hwcodec"]
|
||||
@@ -57,6 +57,7 @@ tracing = { version = "0.1", optional = true }
|
||||
gstreamer = { version = "0.16", optional = true }
|
||||
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
||||
gstreamer-video = { version = "0.16", optional = true }
|
||||
zbus = { version = "3.15", optional = true }
|
||||
|
||||
[dependencies.hwcodec]
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
|
||||
@@ -227,24 +227,12 @@ fn ffmpeg() {
|
||||
*/
|
||||
|
||||
fn main() {
|
||||
// in this crate, these are also valid configurations
|
||||
println!("cargo:rustc-check-cfg=cfg(dxgi,quartz,x11)");
|
||||
|
||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
|
||||
// We check if is macos, because macos uses rust 1.8.1.
|
||||
// `cargo::rustc-check-cfg` is new with Cargo 1.80.
|
||||
// No need to run `cargo version` to get the version here, because:
|
||||
// The following lines are used to suppress the lint warnings.
|
||||
// warning: unexpected `cfg` condition name: `quartz`
|
||||
if cfg!(target_os = "macos") {
|
||||
if target_os != "ios" {
|
||||
println!("cargo::rustc-check-cfg=cfg(android)");
|
||||
println!("cargo::rustc-check-cfg=cfg(dxgi)");
|
||||
println!("cargo::rustc-check-cfg=cfg(quartz)");
|
||||
println!("cargo::rustc-check-cfg=cfg(x11)");
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^ new with Cargo 1.80
|
||||
}
|
||||
}
|
||||
|
||||
// note: all link symbol names in x86 (32-bit) are prefixed wth "_".
|
||||
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
|
||||
// please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc",
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::time::{Duration, Instant};
|
||||
lazy_static! {
|
||||
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
|
||||
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
|
||||
static ref APPLICATION_CONTEXT: RwLock<Option<GlobalRef>> = RwLock::new(None);
|
||||
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
|
||||
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
|
||||
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
|
||||
@@ -462,6 +463,23 @@ fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) {
|
||||
*lock = true;
|
||||
}
|
||||
|
||||
fn try_init_rustls_platform_verifier(env: &mut JNIEnv, context_jobject: *mut c_void) {
|
||||
use hbb_common::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED as INITIALIZED;
|
||||
use std::sync::atomic::Ordering;
|
||||
let initialized = INITIALIZED.load(Ordering::Relaxed);
|
||||
if !initialized {
|
||||
let ctx_for_rustls = unsafe { JObject::from_raw(context_jobject as jni::sys::jobject) };
|
||||
if let Err(e) =
|
||||
hbb_common::rustls_platform_verifier::android::init_hosted(env, ctx_for_rustls)
|
||||
{
|
||||
log::error!("Failed to initialize rustls-platform-verifier: {:?}", e);
|
||||
} else {
|
||||
INITIALIZED.store(true, Ordering::Relaxed);
|
||||
log::info!("rustls-platform-verifier initialized successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init
|
||||
#[no_mangle]
|
||||
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
|
||||
@@ -471,3 +489,23 @@ pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) ->
|
||||
}
|
||||
jni::JNIVersion::V6.into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_ffi_FFI_onAppStart(mut env: JNIEnv, _class: JClass, ctx: JObject) {
|
||||
if ctx.is_null() {
|
||||
log::error!("application context is null");
|
||||
return;
|
||||
}
|
||||
if APPLICATION_CONTEXT.read().unwrap().is_some() {
|
||||
log::info!("application context already initialized");
|
||||
return;
|
||||
}
|
||||
if let Ok(jvm) = env.get_java_vm() {
|
||||
if let Ok(context) = env.new_global_ref(ctx) {
|
||||
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
|
||||
let context_jobject = context.as_obj().as_raw() as *mut c_void;
|
||||
*APPLICATION_CONTEXT.write().unwrap() = Some(context);
|
||||
try_init_rustls_platform_verifier(&mut env, context_jobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ impl EncoderApi for AomEncoder {
|
||||
}
|
||||
|
||||
impl AomEncoder {
|
||||
pub fn encode(&mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
pub fn encode<'a>(&'a mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames<'a>> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
@@ -461,7 +461,7 @@ impl AomDecoder {
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
|
||||
pub fn decode(&mut self, data: &[u8]) -> Result<DecodeFrames> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result<DecodeFrames<'a>> {
|
||||
call_aom!(aom_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
@@ -476,7 +476,7 @@ impl AomDecoder {
|
||||
}
|
||||
|
||||
/// Notify the decoder to return any pending frame
|
||||
pub fn flush(&mut self) -> Result<DecodeFrames> {
|
||||
pub fn flush<'a>(&'a mut self) -> Result<DecodeFrames<'a>> {
|
||||
call_aom!(aom_codec_decode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
|
||||
@@ -268,12 +268,12 @@ impl TraitCapturer for CameraCapturer {
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_gdi(&self) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn set_gdi(&mut self) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
|
||||
@@ -364,7 +364,7 @@ impl HwRamDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<HwRamDecoderImage>> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType<Vec<HwRamDecoderImage<'a>>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| HwRamDecoderImage { frame: f }).collect()),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
@@ -687,7 +687,7 @@ pub fn check_available_hwcodec() -> String {
|
||||
height: 720,
|
||||
pixfmt: DEFAULT_PIXFMT,
|
||||
align: HW_STRIDE_ALIGN as _,
|
||||
kbs: 0,
|
||||
kbs: 1000,
|
||||
fps: DEFAULT_FPS,
|
||||
gop: DEFAULT_GOP,
|
||||
quality: DEFAULT_HW_QUALITY,
|
||||
|
||||
@@ -88,6 +88,27 @@ impl Display {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
match self {
|
||||
Display::X11(_d) => 1.0,
|
||||
Display::WAYLAND(d) => d.scale(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.width(),
|
||||
Display::WAYLAND(d) => d.logical_width(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.height(),
|
||||
Display::WAYLAND(d) => d.logical_height(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
match self {
|
||||
Display::X11(d) => d.origin(),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
impl Default for vpx_codec_enc_cfg {
|
||||
fn default() -> Self {
|
||||
|
||||
@@ -231,7 +231,7 @@ impl EncoderApi for VpxEncoder {
|
||||
}
|
||||
|
||||
impl VpxEncoder {
|
||||
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
pub fn encode<'a>(&'a mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames<'a>> {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
@@ -268,7 +268,7 @@ impl VpxEncoder {
|
||||
}
|
||||
|
||||
/// Notify the encoder to return any pending packets
|
||||
pub fn flush(&mut self) -> Result<EncodeFrames> {
|
||||
pub fn flush<'a>(&'a mut self) -> Result<EncodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_encode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
@@ -473,7 +473,7 @@ impl VpxDecoder {
|
||||
/// The `data` slice is sent to the decoder
|
||||
///
|
||||
/// It matches a call to `vpx_codec_decode`.
|
||||
pub fn decode(&mut self, data: &[u8]) -> Result<DecodeFrames> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result<DecodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
@@ -489,7 +489,7 @@ impl VpxDecoder {
|
||||
}
|
||||
|
||||
/// Notify the decoder to return any pending frame
|
||||
pub fn flush(&mut self) -> Result<DecodeFrames> {
|
||||
pub fn flush<'a>(&'a mut self) -> Result<DecodeFrames<'a>> {
|
||||
call_vpx!(vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
ptr::null(),
|
||||
|
||||
@@ -367,7 +367,7 @@ impl VRamDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<VRamDecoderImage>> {
|
||||
pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType<Vec<VRamDecoderImage<'a>>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| VRamDecoderImage { frame: f }).collect()),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
|
||||
@@ -8,7 +8,6 @@ use super::x11::PixelBuffer;
|
||||
|
||||
pub struct Capturer(Display, Box<dyn Recorder>, Vec<u8>);
|
||||
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MAP_ERR: RwLock<Option<fn(err: String)-> io::Error>> = Default::default();
|
||||
}
|
||||
@@ -61,7 +60,7 @@ impl TraitCapturer for Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display(pipewire::PipeWireCapturable);
|
||||
pub struct Display(pub(crate) pipewire::PipeWireCapturable);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
@@ -81,11 +80,35 @@ impl Display {
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.size.0
|
||||
self.physical_width()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.size.1
|
||||
self.physical_height()
|
||||
}
|
||||
|
||||
pub fn physical_width(&self) -> usize {
|
||||
self.0.physical_size.0
|
||||
}
|
||||
|
||||
pub fn physical_height(&self) -> usize {
|
||||
self.0.physical_size.1
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
self.0.logical_size.0
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
self.0.logical_size.1
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
if self.logical_width() == 0 {
|
||||
1.0
|
||||
} else {
|
||||
self.physical_width() as f64 / self.logical_width() as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
@@ -97,7 +120,7 @@ impl Display {
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
false
|
||||
self.0.primary
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// logic from webrtc -- https://github.com/shiguredo/libwebrtc/blob/main/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use lazy_static;
|
||||
use std::{
|
||||
ffi::CString,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod capturable;
|
||||
pub mod pipewire;
|
||||
pub mod display;
|
||||
mod screencast_portal;
|
||||
mod request_portal;
|
||||
pub mod remote_desktop_portal;
|
||||
|
||||
256
libs/scrap/src/wayland/display.rs
Normal file
256
libs/scrap/src/wayland/display.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use hbb_common::regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
process::{Command, Output, Stdio},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use hbb_common::platform::linux::{get_wayland_displays, WaylandDisplayInfo};
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAYS: Mutex<Option<Arc<Displays>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
pub struct Displays {
|
||||
pub primary: usize,
|
||||
pub displays: Vec<WaylandDisplayInfo>,
|
||||
}
|
||||
|
||||
// We need this helper to run commands with a timeout, as some commands may hang.
|
||||
// `kscreen-doctor -o` is known to hang when:
|
||||
// 1. On Archlinux, Both GNOME and KDE Plasma are installed.
|
||||
// 2. Run this command in a GNOME session.
|
||||
fn run_with_timeout(
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
timeout: Duration,
|
||||
label: &str,
|
||||
) -> Option<Output> {
|
||||
let mut child = Command::new(program)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
break;
|
||||
}
|
||||
if start.elapsed() >= timeout {
|
||||
warn!("{} command timed out after {:?}", label, timeout);
|
||||
if let Err(e) = child.kill() {
|
||||
warn!("Failed to kill child process for '{}': {}", label, e);
|
||||
}
|
||||
if let Err(e) = child.wait() {
|
||||
warn!("Failed to wait for child process for '{}': {}", label, e);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
}
|
||||
|
||||
match child.wait_with_output() {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
warn!("{} command failed with status: {}", label, output.status);
|
||||
return None;
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// There are some limitations with xrandr method:
|
||||
// 1. It only works when XWayland is running.
|
||||
// 2. The distro may not have xrandr installed by default.
|
||||
// 3. xrandr may not report "primary" in its output. eg. openSUSE Leap 15.6 KDE Plasma.
|
||||
fn try_xrandr_primary() -> Option<String> {
|
||||
let output = Command::new("xrandr").output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if line.contains("primary") && line.contains("connected") {
|
||||
if let Some(name) = line.split_whitespace().next() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn try_kscreen_primary() -> Option<String> {
|
||||
if !hbb_common::platform::linux::is_kde_session() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let output = run_with_timeout(
|
||||
"kscreen-doctor",
|
||||
&["-o"],
|
||||
COMMAND_TIMEOUT,
|
||||
"kscreen-doctor -o",
|
||||
)?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Remove ANSI color codes
|
||||
let re_ansi = Regex::new(r"\x1b\[[0-9;]*m").ok()?;
|
||||
let clean_text = re_ansi.replace_all(&text, "");
|
||||
|
||||
// Split the text into blocks, each starting with "Output:".
|
||||
// The first element of the split will be empty, so we skip it.
|
||||
for block in clean_text.split("Output:").skip(1) {
|
||||
// Check if this block describes the primary monitor.
|
||||
if block.contains("priority 1") {
|
||||
// The monitor name is the second piece of text in the block, after the ID.
|
||||
// e.g., " 1 eDP-1 enabled..." -> "eDP-1"
|
||||
if let Some(name) = block.split_whitespace().nth(1) {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn try_gdbus_primary() -> Option<String> {
|
||||
let output = run_with_timeout(
|
||||
"gdbus",
|
||||
&[
|
||||
"call",
|
||||
"--session",
|
||||
"--dest",
|
||||
"org.gnome.Mutter.DisplayConfig",
|
||||
"--object-path",
|
||||
"/org/gnome/Mutter/DisplayConfig",
|
||||
"--method",
|
||||
"org.gnome.Mutter.DisplayConfig.GetCurrentState",
|
||||
],
|
||||
COMMAND_TIMEOUT,
|
||||
"gdbus DisplayConfig.GetCurrentState",
|
||||
)?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Match logical monitor entries with primary=true
|
||||
// Pattern: (x, y, scale, transform, true, [('connector-name', ...), ...], ...)
|
||||
// Use regex to find entries where 5th field is true, then extract connector name
|
||||
// Example matched text: "(0, 0, 1.5, 0, true, [('HDMI-1', 'MHH', 'Monitor', '0x00000000')], ...)"
|
||||
let re = Regex::new(r"\([^()]*,\s*true,\s*\[\('([^']+)'").ok()?;
|
||||
|
||||
if let Some(captures) = re.captures(&text) {
|
||||
return captures.get(1).map(|m| m.as_str().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_primary_monitor() -> Option<String> {
|
||||
try_xrandr_primary()
|
||||
.or_else(try_kscreen_primary)
|
||||
.or_else(try_gdbus_primary)
|
||||
}
|
||||
|
||||
pub fn get_displays() -> Arc<Displays> {
|
||||
let mut lock = DISPLAYS.lock().unwrap();
|
||||
match lock.as_ref() {
|
||||
Some(displays) => displays.clone(),
|
||||
None => match get_wayland_displays() {
|
||||
Ok(displays) => {
|
||||
let mut primary_index = None;
|
||||
if let Some(name) = get_primary_monitor() {
|
||||
for (i, display) in displays.iter().enumerate() {
|
||||
if display.name == name {
|
||||
primary_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
if primary_index.is_none() {
|
||||
for (i, display) in displays.iter().enumerate() {
|
||||
if display.x == 0 && display.y == 0 {
|
||||
primary_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let displays = Arc::new(Displays {
|
||||
primary: primary_index.unwrap_or(0),
|
||||
displays,
|
||||
});
|
||||
*lock = Some(displays.clone());
|
||||
displays
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
Arc::new(Displays {
|
||||
primary: 0,
|
||||
displays: Vec::new(),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear_wayland_displays_cache() {
|
||||
let _ = DISPLAYS.lock().unwrap().take();
|
||||
}
|
||||
|
||||
// Return (min_x, max_x, min_y, max_y)
|
||||
pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
let wayland_displays = get_displays();
|
||||
let displays = &wayland_displays.displays;
|
||||
if displays.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// For compatibility, if only one display, we use the physical size for `uinput`.
|
||||
// Otherwise, we use the logical size for `uinput`.
|
||||
if displays.len() == 1 {
|
||||
let d = &displays[0];
|
||||
return Some((d.x, d.x + d.width, d.y, d.y + d.height));
|
||||
}
|
||||
|
||||
let mut min_x = i32::MAX;
|
||||
let mut min_y = i32::MAX;
|
||||
let mut max_x = i32::MIN;
|
||||
let mut max_y = i32::MIN;
|
||||
for d in displays.iter() {
|
||||
min_x = min_x.min(d.x);
|
||||
min_y = min_y.min(d.y);
|
||||
let size = if let Some(logical_size) = d.logical_size {
|
||||
logical_size
|
||||
} else {
|
||||
// When `logical_size` is None, we cannot obtain the correct desktop rectangle.
|
||||
// This may occur if the Wayland compositor does not provide logical size information,
|
||||
// or if display information is incomplete. We fall back to physical size, which provides
|
||||
// usable dimensions, but may not always be correct depending on compositor behavior.
|
||||
warn!(
|
||||
"Display at ({}, {}) is missing logical_size; falling back to physical size ({}, {}).",
|
||||
d.x, d.y, d.width, d.height
|
||||
);
|
||||
(d.width, d.height)
|
||||
};
|
||||
max_x = max_x.max(d.x + size.0);
|
||||
max_y = max_y.max(d.y + size.1);
|
||||
}
|
||||
Some((min_x, max_x, min_y, max_y))
|
||||
}
|
||||
@@ -2,9 +2,12 @@ use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::process::Command;
|
||||
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, trace, warn};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use dbus::{
|
||||
arg::{OwnedFd, PropMap, RefArg, Variant},
|
||||
@@ -17,23 +20,58 @@ use gstreamer as gst;
|
||||
use gstreamer::prelude::*;
|
||||
use gstreamer_app::AppSink;
|
||||
|
||||
use hbb_common::config;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use hbb_common::{bail, config, platform::linux::CMD_SH, serde_json, tokio, ResultType};
|
||||
|
||||
use super::capturable::PixelProvider;
|
||||
use super::capturable::{Capturable, Recorder};
|
||||
use super::display::{clear_wayland_displays_cache, get_displays, Displays};
|
||||
use super::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal;
|
||||
use super::request_portal::OrgFreedesktopPortalRequestResponse;
|
||||
use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_portal;
|
||||
use hbb_common::platform::linux::CMD_SH;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref RDP_SESSION_INFO: Mutex<Option<RdpSessionInfo>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
// For KDE Plasma only, because GNOME provides position info.
|
||||
struct PipewireDisplayOffsetCache {
|
||||
// We need to compare the displays, because:
|
||||
// 1. On Archlinux KDE Plasma
|
||||
// 2. One display, and connect, remember share choice.
|
||||
// 3. Plug in another monitor.
|
||||
// 4. The portal will reuse the restore token, no new share choice dialog, but the share screen is different.
|
||||
// The controlling side will see the new monitor.
|
||||
// All displays as one string for easy comparison
|
||||
// name1-x1-y1-width1-height1;name2-x2-y2-width2-height2;...
|
||||
display_key: String,
|
||||
restore_token: String,
|
||||
offsets: Vec<(i32, i32)>,
|
||||
}
|
||||
|
||||
// KDE Plasma may not provide position info
|
||||
static HAS_POSITION_ATTR: AtomicBool = AtomicBool::new(false);
|
||||
static IS_SERVER_RUNNING: AtomicU8 = AtomicU8::new(0); // 0: uninitialized, 1:true, 2: false
|
||||
|
||||
impl PipewireDisplayOffsetCache {
|
||||
fn displays_to_key(displays: &Arc<Displays>) -> String {
|
||||
displays
|
||||
.displays
|
||||
.iter()
|
||||
.map(|d| format!("{}-{}-{}-{}-{}", d.name, d.x, d.y, d.width, d.height))
|
||||
.collect::<Vec<String>>()
|
||||
.join(";")
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn close_session() {
|
||||
let _ = RDP_SESSION_INFO.lock().unwrap().take();
|
||||
clear_wayland_displays_cache();
|
||||
HAS_POSITION_ATTR.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -52,6 +90,8 @@ pub fn try_close_session() {
|
||||
}
|
||||
if close {
|
||||
*rdp_info = None;
|
||||
clear_wayland_displays_cache();
|
||||
HAS_POSITION_ATTR.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +115,10 @@ impl PwStreamInfo {
|
||||
pub fn get_size(&self) -> (usize, usize) {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> (i32, i32) {
|
||||
self.position
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -108,8 +152,10 @@ pub struct PipeWireCapturable {
|
||||
fd: OwnedFd,
|
||||
path: u64,
|
||||
source_type: u64,
|
||||
pub primary: bool,
|
||||
pub position: (i32, i32),
|
||||
pub size: (usize, usize),
|
||||
pub logical_size: (usize, usize),
|
||||
pub physical_size: (usize, usize),
|
||||
}
|
||||
|
||||
impl PipeWireCapturable {
|
||||
@@ -117,27 +163,31 @@ impl PipeWireCapturable {
|
||||
conn: Arc<SyncConnection>,
|
||||
fd: OwnedFd,
|
||||
resolution: Arc<Mutex<Option<(usize, usize)>>>,
|
||||
stream: PwStreamInfo,
|
||||
stream: &PwStreamInfo,
|
||||
) -> Self {
|
||||
// alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling
|
||||
// https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244
|
||||
let size = get_res(Self {
|
||||
let physical_size = get_res(Self {
|
||||
dbus_conn: conn.clone(),
|
||||
fd: fd.clone(),
|
||||
path: stream.path,
|
||||
source_type: stream.source_type,
|
||||
primary: false,
|
||||
position: stream.position,
|
||||
size: stream.size,
|
||||
logical_size: stream.size,
|
||||
physical_size: (0, 0),
|
||||
})
|
||||
.unwrap_or(stream.size);
|
||||
*resolution.lock().unwrap() = Some(size);
|
||||
*resolution.lock().unwrap() = Some(physical_size);
|
||||
Self {
|
||||
dbus_conn: conn,
|
||||
fd,
|
||||
path: stream.path,
|
||||
source_type: stream.source_type,
|
||||
primary: false,
|
||||
position: stream.position,
|
||||
size,
|
||||
logical_size: stream.size,
|
||||
physical_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +264,7 @@ pub struct PipeWireRecorder {
|
||||
}
|
||||
|
||||
impl PipeWireRecorder {
|
||||
pub fn new(capturable: PipeWireCapturable) -> Result<Self, Box<dyn Error>> {
|
||||
pub fn new(capturable: PipeWireCapturable) -> ResultType<Self> {
|
||||
let pipeline = gst::Pipeline::new(None);
|
||||
|
||||
let src = gst::ElementFactory::make("pipewiresrc", None)?;
|
||||
@@ -247,7 +297,40 @@ impl PipeWireRecorder {
|
||||
));
|
||||
appsink.set_caps(Some(&caps));
|
||||
|
||||
// [Workaround]
|
||||
// Crash may occur if there are multiple pipelines started at the same time.
|
||||
// `pipeline.get_state()` can significantly reduce the probability of crashes,
|
||||
// but cannot completely resolve this issue.
|
||||
// Adding a short sleep period can also reduce the probability of crashes.
|
||||
debug!(
|
||||
"[gstreamer] Setting pipeline {} to PLAYING state...",
|
||||
capturable.fd.as_raw_fd()
|
||||
);
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
// If `is_server_running()` is false, it means using remote_desktop_portal,
|
||||
// which does not use multiple streams, so no need to wait for state change.
|
||||
if is_server_running() {
|
||||
// Wait for the state change to actually complete before proceeding.
|
||||
// The 2000ms timeout for pipeline state change was chosen based on empirical testing.
|
||||
let state_change = pipeline.get_state(gst::ClockTime::from_mseconds(2000));
|
||||
match state_change {
|
||||
(Ok(_), gst::State::Playing, _) => {
|
||||
debug!(
|
||||
"[gstreamer] Pipeline {} state confirmed as PLAYING.",
|
||||
capturable.fd.as_raw_fd()
|
||||
);
|
||||
}
|
||||
(result, state, pending) => {
|
||||
warn!(
|
||||
"[gstreamer] Pipeline {} state change incomplete: result={:?}, state={:?}, pending={:?}",
|
||||
capturable.fd.as_raw_fd(), result, state, pending
|
||||
);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
pipeline,
|
||||
appsink,
|
||||
@@ -366,6 +449,8 @@ impl Drop for PipeWireRecorder {
|
||||
if let Err(err) = self.pipeline.set_state(gst::State::Null) {
|
||||
warn!("Failed to stop GStreamer pipeline: {}.", err);
|
||||
}
|
||||
// Wait for state change to complete to avoid races during PipeWire teardown.
|
||||
let _ = self.pipeline.get_state(gst::ClockTime::from_mseconds(2000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,18 +481,18 @@ where
|
||||
0 => {}
|
||||
1 => {
|
||||
warn!("DBus response: User cancelled interaction.");
|
||||
failure_out.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
failure_out.store(true, Ordering::SeqCst);
|
||||
return true;
|
||||
}
|
||||
c => {
|
||||
warn!("DBus response: Unknown error, code: {}.", c);
|
||||
failure_out.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
failure_out.store(true, Ordering::SeqCst);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Err(err) = f(r, c, m) {
|
||||
warn!("Error requesting screen capture via dbus: {}", err);
|
||||
failure_out.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
failure_out.store(true, Ordering::SeqCst);
|
||||
}
|
||||
true
|
||||
})
|
||||
@@ -488,6 +573,7 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec<P
|
||||
if v.len() == 2 {
|
||||
info.position.0 = v[0] as _;
|
||||
info.position.1 = v[1] as _;
|
||||
HAS_POSITION_ATTR.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,6 +588,7 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec<P
|
||||
static mut INIT: bool = false;
|
||||
const RESTORE_TOKEN: &str = "restore_token";
|
||||
const RESTORE_TOKEN_CONF_KEY: &str = "wayland-restore-token";
|
||||
const PIPEWIRE_DISPLAY_OFFSET_CONF_KEY: &str = "wayland-pipewire-display-offset";
|
||||
|
||||
pub fn get_available_cursor_modes() -> Result<u32, dbus::Error> {
|
||||
let conn = SyncConnection::new_session()?;
|
||||
@@ -510,16 +597,15 @@ pub fn get_available_cursor_modes() -> Result<u32, dbus::Error> {
|
||||
}
|
||||
|
||||
// mostly inspired by https://gitlab.gnome.org/-/snippets/39
|
||||
pub fn request_remote_desktop() -> Result<
|
||||
(
|
||||
SyncConnection,
|
||||
OwnedFd,
|
||||
Vec<PwStreamInfo>,
|
||||
dbus::Path<'static>,
|
||||
bool,
|
||||
),
|
||||
Box<dyn Error>,
|
||||
> {
|
||||
pub fn request_remote_desktop(
|
||||
capture_cursor: bool,
|
||||
) -> ResultType<(
|
||||
SyncConnection,
|
||||
OwnedFd,
|
||||
Vec<PwStreamInfo>,
|
||||
dbus::Path<'static>,
|
||||
bool,
|
||||
)> {
|
||||
unsafe {
|
||||
if !INIT {
|
||||
gstreamer::init()?;
|
||||
@@ -574,6 +660,7 @@ pub fn request_remote_desktop() -> Result<
|
||||
session.clone(),
|
||||
failure.clone(),
|
||||
is_support_restore_token,
|
||||
capture_cursor,
|
||||
),
|
||||
failure_res.clone(),
|
||||
)?;
|
||||
@@ -586,7 +673,7 @@ pub fn request_remote_desktop() -> Result<
|
||||
break;
|
||||
}
|
||||
|
||||
if failure_res.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
if failure_res.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -607,9 +694,7 @@ pub fn request_remote_desktop() -> Result<
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Box::new(DBusError(
|
||||
"Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.".into()
|
||||
)))
|
||||
bail!("Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.")
|
||||
}
|
||||
|
||||
fn on_create_session_response(
|
||||
@@ -618,6 +703,7 @@ fn on_create_session_response(
|
||||
session: Arc<Mutex<Option<dbus::Path<'static>>>>,
|
||||
failure: Arc<AtomicBool>,
|
||||
is_support_restore_token: bool,
|
||||
capture_cursor: bool,
|
||||
) -> impl Fn(
|
||||
OrgFreedesktopPortalRequestResponse,
|
||||
&SyncConnection,
|
||||
@@ -666,6 +752,14 @@ fn on_create_session_response(
|
||||
}
|
||||
args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32)));
|
||||
|
||||
if capture_cursor {
|
||||
get_available_cursor_modes().ok().map(|modes| {
|
||||
if modes & 0x2 != 0 {
|
||||
args.insert("cursor_mode".to_string(), Variant(Box::new(2u32)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let path = portal.select_sources(ses.clone(), args)?;
|
||||
handle_response(
|
||||
c,
|
||||
@@ -838,7 +932,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
};
|
||||
|
||||
if rdp_connection.is_none() {
|
||||
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
|
||||
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?;
|
||||
let conn = Arc::new(conn);
|
||||
|
||||
let rdp_info = RdpSessionInfo {
|
||||
@@ -852,7 +946,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
*rdp_connection = Some(rdp_info);
|
||||
}
|
||||
|
||||
let rdp_info = match rdp_connection.as_ref() {
|
||||
let rdp_info = match rdp_connection.as_mut() {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return Err(Box::new(DBusError("RDP response is None.".into())));
|
||||
@@ -861,8 +955,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
|
||||
Ok(rdp_info
|
||||
.streams
|
||||
.clone()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
PipeWireCapturable::new(
|
||||
rdp_info.conn.clone(),
|
||||
@@ -883,7 +976,12 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
//
|
||||
// `screencast_portal` supports restore_token and persist_mode if the version is greater than or equal to 4.
|
||||
// `remote_desktop_portal` does not support restore_token and persist_mode.
|
||||
fn is_server_running() -> bool {
|
||||
pub(crate) fn is_server_running() -> bool {
|
||||
let v = IS_SERVER_RUNNING.load(Ordering::SeqCst);
|
||||
if v > 0 {
|
||||
return v == 1;
|
||||
}
|
||||
|
||||
let app_name = config::APP_NAME.read().unwrap().clone().to_lowercase();
|
||||
let output = match Command::new(CMD_SH.as_str())
|
||||
.arg("-c")
|
||||
@@ -898,5 +996,533 @@ fn is_server_running() -> bool {
|
||||
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
let is_running = output_str.contains(&format!("{} --server", app_name));
|
||||
IS_SERVER_RUNNING.store(if is_running { 1 } else { 2 }, Ordering::SeqCst);
|
||||
is_running
|
||||
}
|
||||
|
||||
// The logical size reported by portal may be different from the size reported by `get_displays()`.
|
||||
// So we need to use the workaround here.
|
||||
// 1. openSUSE, KDE Plasma
|
||||
// 2. Kubuntu 24.04 TLS, after running `sudo apt install plasma-workspace-wayland`
|
||||
// Maybe it's a bug, and we can remove this workaround in the future.
|
||||
pub fn try_fix_logical_size(shared_displays: &mut Vec<crate::Display>) {
|
||||
if !is_server_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
let wayland_displays = get_displays();
|
||||
if wayland_displays.displays.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for sd in shared_displays.iter_mut() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &mut d.0;
|
||||
for wd in wayland_displays.displays.iter() {
|
||||
if capturable.position.0 == wd.x && capturable.position.1 == wd.y {
|
||||
if let Some(logical_size) = wd.logical_size {
|
||||
if capturable.physical_size.0 != wd.width as usize
|
||||
|| capturable.physical_size.1 != wd.height as usize
|
||||
{
|
||||
// If "Full Workspace" is selected in the portal dialog,
|
||||
// the physical size reported by portal may not match the display info.
|
||||
debug!(
|
||||
"Physical size of capturable ({:?}) does not match display info: ({:?}) - ({:?}). Skipping logical size fix.",
|
||||
capturable.position,
|
||||
capturable.physical_size,
|
||||
(wd.width as usize, wd.height as usize)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if capturable.logical_size.0 != logical_size.0 as usize
|
||||
|| capturable.logical_size.1 != logical_size.1 as usize
|
||||
{
|
||||
warn!(
|
||||
"Fixing logical size of capturable from {:?} to {:?} based on display info {:?}.",
|
||||
capturable.logical_size,
|
||||
logical_size,
|
||||
wd
|
||||
);
|
||||
capturable.logical_size =
|
||||
(logical_size.0 as usize, logical_size.1 as usize);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_displays(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
) -> ResultType<()> {
|
||||
if !is_server_running() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut rdp_connection = RDP_SESSION_INFO.lock().unwrap();
|
||||
let rdp_info = match rdp_connection.as_mut() {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
// Unreachable
|
||||
bail!("RDP session info is None when filling display positions.");
|
||||
}
|
||||
};
|
||||
|
||||
let all_displays = get_displays();
|
||||
if !HAS_POSITION_ATTR.load(Ordering::SeqCst) {
|
||||
if all_displays.displays.len() > 1 {
|
||||
debug!("Multiple Wayland displays detected, adjusting stream positions accordingly.");
|
||||
try_fill_positions(
|
||||
mouse_move_to,
|
||||
get_cursor_pos,
|
||||
&all_displays,
|
||||
shared_displays,
|
||||
&mut rdp_info.streams,
|
||||
)?;
|
||||
}
|
||||
HAS_POSITION_ATTR.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if all_displays.displays.len() > 1 {
|
||||
sort_streams(&all_displays, shared_displays, &mut rdp_info.streams);
|
||||
}
|
||||
|
||||
shared_displays.iter_mut().next().map(|d| {
|
||||
if let crate::Display::WAYLAND(d) = d {
|
||||
d.0.primary = true;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_fill_positions(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
) -> ResultType<()> {
|
||||
let pipewire_display_offset = config::LocalConfig::get_option(PIPEWIRE_DISPLAY_OFFSET_CONF_KEY);
|
||||
if !pipewire_display_offset.is_empty() {
|
||||
if try_fill_positions_from_cache(
|
||||
pipewire_display_offset,
|
||||
displays,
|
||||
shared_displays,
|
||||
streams,
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
config::LocalConfig::set_option(PIPEWIRE_DISPLAY_OFFSET_CONF_KEY.to_owned(), "".to_owned());
|
||||
}
|
||||
|
||||
let mut multi_matched_indices = Vec::new();
|
||||
for (i, sd) in shared_displays.iter_mut().enumerate() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &mut d.0;
|
||||
let mut match_count = 0;
|
||||
for wd in displays.displays.iter() {
|
||||
if capturable.physical_size.0 == wd.width as usize
|
||||
&& capturable.physical_size.1 == wd.height as usize
|
||||
{
|
||||
capturable.position = (wd.x, wd.y);
|
||||
if let Some(pw_stream) = streams.get_mut(i) {
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
}
|
||||
match_count += 1;
|
||||
}
|
||||
}
|
||||
if match_count == 0 {
|
||||
warn!(
|
||||
"No matching display found for capturable with size {:?}.",
|
||||
capturable.physical_size
|
||||
);
|
||||
} else if match_count > 1 {
|
||||
multi_matched_indices.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !multi_matched_indices.is_empty() {
|
||||
fill_multi_matched_positions(
|
||||
mouse_move_to,
|
||||
get_cursor_pos,
|
||||
displays,
|
||||
shared_displays,
|
||||
streams,
|
||||
multi_matched_indices,
|
||||
)?;
|
||||
}
|
||||
|
||||
save_positions_to_cache(displays, shared_displays);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_fill_positions_from_cache(
|
||||
cache_str: String,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
) -> bool {
|
||||
let Ok(cache) = serde_json::from_str::<PipewireDisplayOffsetCache>(&cache_str) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if cache.offsets.len() != shared_displays.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let display_key = PipewireDisplayOffsetCache::displays_to_key(displays);
|
||||
if cache.display_key != display_key {
|
||||
return false;
|
||||
}
|
||||
|
||||
let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY);
|
||||
if cache.restore_token != restore_token {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i, sd) in shared_displays.iter_mut().enumerate() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &mut d.0;
|
||||
if let Some((x_off, y_off)) = cache.offsets.get(i) {
|
||||
capturable.position = (*x_off, *y_off);
|
||||
if let Some(pw_stream) = streams.get_mut(i) {
|
||||
pw_stream.position = (*x_off, *y_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn save_positions_to_cache(displays: &Arc<Displays>, shared_displays: &Vec<crate::Display>) {
|
||||
let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY);
|
||||
if restore_token.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut offsets = Vec::new();
|
||||
for sd in shared_displays.iter() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &d.0;
|
||||
offsets.push((capturable.position.0, capturable.position.1));
|
||||
}
|
||||
}
|
||||
|
||||
let display_key = PipewireDisplayOffsetCache::displays_to_key(displays);
|
||||
let cache = PipewireDisplayOffsetCache {
|
||||
display_key,
|
||||
restore_token,
|
||||
offsets,
|
||||
};
|
||||
|
||||
if let Ok(s) = serde_json::to_string(&cache) {
|
||||
config::LocalConfig::set_option(PIPEWIRE_DISPLAY_OFFSET_CONF_KEY.to_owned(), s);
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_left_up_corner(w: usize, d1: &[u8], d2: &[u8]) -> bool {
|
||||
if w == 0 {
|
||||
return false;
|
||||
}
|
||||
if d1.len() != d2.len() {
|
||||
return false;
|
||||
}
|
||||
let bpp = 4; // BGR0/RGB0
|
||||
let stride = w.saturating_mul(bpp);
|
||||
if stride == 0 || d1.len() < stride || d2.len() < stride {
|
||||
return false;
|
||||
}
|
||||
let h = d1.len() / stride;
|
||||
if h == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let roi_w = std::cmp::min(36, w);
|
||||
let roi_h = std::cmp::min(36, h);
|
||||
let mut diff_px = 0usize;
|
||||
let total_px = roi_w * roi_h;
|
||||
// Minimum number of differing pixels required to consider images different.
|
||||
const MIN_DIFF_PIXELS: usize = 8;
|
||||
// Divisor for threshold calculation: allows up to 1/8 of ROI pixels to differ before returning true.
|
||||
const DIFF_THRESHOLD_DIVISOR: usize = 8;
|
||||
let threshold = std::cmp::max(MIN_DIFF_PIXELS, total_px / DIFF_THRESHOLD_DIVISOR);
|
||||
|
||||
for y in 0..roi_h {
|
||||
let row_off = y * stride;
|
||||
for x in 0..roi_w {
|
||||
let i = row_off + x * bpp;
|
||||
let a = &d1[i..i + bpp];
|
||||
let b = &d2[i..i + bpp];
|
||||
if a != b {
|
||||
diff_px += 1;
|
||||
if diff_px >= threshold {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn fill_multi_matched_positions(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
multi_matched_indices: Vec<usize>,
|
||||
) -> ResultType<()> {
|
||||
debug!(
|
||||
"Multiple capturables ({:?}) match the same display size, attempting to disambiguate positions.",
|
||||
&multi_matched_indices);
|
||||
if multi_matched_indices.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let is_support_embeded_cursor = get_available_cursor_modes()
|
||||
.ok()
|
||||
.map(|modes| modes & 0x2 != 0)
|
||||
.unwrap_or(false);
|
||||
if is_support_embeded_cursor {
|
||||
fill_multi_matched_positions_cursor(
|
||||
mouse_move_to,
|
||||
get_cursor_pos,
|
||||
displays,
|
||||
shared_displays,
|
||||
streams,
|
||||
multi_matched_indices,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mouse_move_to_(
|
||||
mouse_move_to: &impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
x: i32,
|
||||
y: i32,
|
||||
) {
|
||||
const MOVE_MOUSE_TIMEOUT: Duration = Duration::from_millis(150);
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < MOVE_MOUSE_TIMEOUT {
|
||||
mouse_move_to(x, y);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
if let Some((x1, y1)) = get_cursor_pos() {
|
||||
if x1 == x && y1 == y {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
warn!(
|
||||
"Failed to move mouse to ({}, {}) within timeout: {:?}.",
|
||||
x, y, &MOVE_MOUSE_TIMEOUT
|
||||
);
|
||||
}
|
||||
|
||||
fn fill_multi_matched_positions_cursor(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
multi_matched_indices: Vec<usize>,
|
||||
) -> ResultType<()> {
|
||||
// This creates a new remote desktop session for cursor-based position detection.
|
||||
// The session is temporary, used only for disambiguation, and is dropped after detection completes.
|
||||
let (conn, fd, streams_with_cursor, _session, _is_support_restore_token) =
|
||||
request_remote_desktop(true)?;
|
||||
let conn = Arc::new(conn);
|
||||
|
||||
let mut matched_indices = Vec::new();
|
||||
const CAPTURE_TIMEOUT_MS: u64 = 1_000;
|
||||
for idx in multi_matched_indices {
|
||||
match (
|
||||
shared_displays.get_mut(idx),
|
||||
streams.get_mut(idx),
|
||||
streams_with_cursor.get(idx),
|
||||
) {
|
||||
(Some(crate::Display::WAYLAND(d)), Some(pw_stream), Some(pw_stream_with_cursor)) => {
|
||||
// Check if only one display matches the size
|
||||
let mut match_count = 0;
|
||||
for (i, wd) in displays.displays.iter().enumerate() {
|
||||
if matched_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
if d.0.physical_size.0 == wd.width as usize
|
||||
&& d.0.physical_size.1 == wd.height as usize
|
||||
{
|
||||
match_count += 1;
|
||||
}
|
||||
}
|
||||
if match_count == 0 {
|
||||
error!(
|
||||
"No matching display found for capturable with size {:?}.",
|
||||
d.0.physical_size
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if match_count == 1 {
|
||||
for (i, wd) in displays.displays.iter().enumerate() {
|
||||
if matched_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
if d.0.physical_size.0 == wd.width as usize
|
||||
&& d.0.physical_size.1 == wd.height as usize
|
||||
{
|
||||
d.0.position = (wd.x, wd.y);
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
matched_indices.push(i);
|
||||
debug!(
|
||||
"Disambiguated position for capturable with size {:?} to ({}, {}).",
|
||||
d.0.physical_size, wd.x, wd.y
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move the mouse to a neutral position first,
|
||||
// to avoid interference from previous position.
|
||||
mouse_move_to_(&mouse_move_to, get_cursor_pos, 300, 300);
|
||||
|
||||
let mut rec = PipeWireRecorder::new(PipeWireCapturable {
|
||||
dbus_conn: conn.clone(),
|
||||
fd: fd.clone(),
|
||||
path: pw_stream_with_cursor.path,
|
||||
source_type: pw_stream_with_cursor.source_type,
|
||||
primary: false,
|
||||
position: pw_stream_with_cursor.position,
|
||||
logical_size: pw_stream_with_cursor.size,
|
||||
physical_size: (0, 0),
|
||||
})?;
|
||||
// Take first frame and copy owned buffer to avoid borrow across second capture
|
||||
let (is_bgr, w, first_buf): (bool, usize, Vec<u8>) =
|
||||
match rec.capture(CAPTURE_TIMEOUT_MS) {
|
||||
Ok(PixelProvider::BGR0(w, _, data1)) => (true, w, data1.to_vec()),
|
||||
Ok(PixelProvider::RGB0(w, _, data1)) => (false, w, data1.to_vec()),
|
||||
Ok(_) => {
|
||||
error!("Unexpected pixel format on first capture.");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to capture screen for position disambiguation: {}",
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let matched_len = matched_indices.len();
|
||||
for (i, wd) in displays.displays.iter().enumerate() {
|
||||
if matched_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if wd.width as usize == d.0.physical_size.0
|
||||
&& wd.height as usize == d.0.physical_size.1
|
||||
{
|
||||
mouse_move_to_(&mouse_move_to, get_cursor_pos, wd.x + 8, wd.y + 8);
|
||||
rec.saved_raw_data.clear();
|
||||
match rec.capture(CAPTURE_TIMEOUT_MS) {
|
||||
Ok(PixelProvider::BGR0(_, _, data2)) if is_bgr => {
|
||||
if compare_left_up_corner(w, &first_buf, data2) {
|
||||
d.0.position = (wd.x, wd.y);
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
matched_indices.push(i);
|
||||
debug!(
|
||||
"Disambiguated position for capturable with size {:?} to ({}, {}).",
|
||||
d.0.physical_size, wd.x, wd.y
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(PixelProvider::RGB0(_, _, data2)) if !is_bgr => {
|
||||
if compare_left_up_corner(w, &first_buf, data2) {
|
||||
d.0.position = (wd.x, wd.y);
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
matched_indices.push(i);
|
||||
debug!(
|
||||
"Disambiguated position for capturable with size {:?} to ({}, {}).",
|
||||
d.0.physical_size, wd.x, wd.y
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// unreachable
|
||||
error!("Pixel format changed between captures, cannot disambiguate position.");
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to capture screen for position disambiguation: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if matched_len == matched_indices.len() {
|
||||
error!(
|
||||
"Failed to disambiguate position for capturable with size {:?}.",
|
||||
d.0.physical_size
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sort_streams(
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
) {
|
||||
if streams.is_empty() {
|
||||
// unreachable
|
||||
error!("No streams available to sort.");
|
||||
return;
|
||||
}
|
||||
|
||||
// put the main display first, then the rest by the order of displays
|
||||
let mut display_order: Vec<(i32, i32)> = Vec::new();
|
||||
if let Some(d) = displays.displays.get(displays.primary) {
|
||||
display_order.push((d.x, d.y));
|
||||
}
|
||||
for (i, d) in displays.displays.iter().enumerate() {
|
||||
if i != displays.primary {
|
||||
display_order.push((d.x, d.y));
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_streams = Vec::new();
|
||||
let mut sorted_shared_displays = Vec::new();
|
||||
// Move matching items in order without cloning
|
||||
for (x, y) in display_order.into_iter() {
|
||||
for i in 0..streams.len() {
|
||||
if streams[i].position.0 == x && streams[i].position.1 == y {
|
||||
sorted_streams.push(streams.remove(i));
|
||||
// shared_displays.len() must be equal to streams.len()
|
||||
// But we still check the length to avoid panic
|
||||
if shared_displays.len() > i {
|
||||
sorted_shared_displays.push(shared_displays.remove(i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
*streams = sorted_streams;
|
||||
*shared_displays = sorted_shared_displays;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.4.3
|
||||
pkgver=1.4.4
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
46
res/ab.py
46
res/ab.py
@@ -39,7 +39,14 @@ def view_shared_abs(url, token, name=None):
|
||||
while True:
|
||||
filtered_params["current"] = current
|
||||
response = requests.get(f"{url}/api/ab/shared/profiles", headers=headers, params=filtered_params)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
|
||||
data = response_json.get("data", [])
|
||||
abs.extend(data)
|
||||
@@ -84,7 +91,14 @@ def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None):
|
||||
while True:
|
||||
filtered_params["current"] = current
|
||||
response = requests.get(f"{url}/api/ab/peers", headers=headers, params=filtered_params)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
|
||||
data = response_json.get("data", [])
|
||||
peers.extend(data)
|
||||
@@ -103,11 +117,6 @@ def view_ab_tags(url, token, ab_guid):
|
||||
response = requests.get(f"{url}/api/ab/tags/{ab_guid}", headers=headers)
|
||||
response_json = check_response(response)
|
||||
|
||||
# Handle error responses
|
||||
if isinstance(response_json, tuple) and response_json[0] == "Failed":
|
||||
print(f"Error: {response_json[1]} - {response_json[2]}")
|
||||
return []
|
||||
|
||||
# Format color values as hex
|
||||
if response_json:
|
||||
for tag in response_json:
|
||||
@@ -122,14 +131,18 @@ def view_ab_tags(url, token, ab_guid):
|
||||
|
||||
def check_response(response):
|
||||
"""Check API response and return result"""
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
response_json = response.json()
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
else:
|
||||
return "Failed", response.status_code, response.text
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
|
||||
|
||||
def add_peer(url, token, ab_guid, peer_id, alias=None, note=None, tags=None, password=None):
|
||||
@@ -395,7 +408,14 @@ def view_ab_rules(url, token, ab_guid):
|
||||
while True:
|
||||
params["current"] = current
|
||||
response = requests.get(f"{url}/api/ab/rules", headers=headers, params=params)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
|
||||
data = response_json.get("data", [])
|
||||
rules.extend(data)
|
||||
|
||||
@@ -149,14 +149,18 @@ def enhance_audit_data(data, audit_type):
|
||||
|
||||
def check_response(response):
|
||||
"""Check API response and return result"""
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
response_json = response.json()
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
else:
|
||||
return "Failed", response.status_code, response.text
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
|
||||
|
||||
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
|
||||
@@ -216,7 +220,7 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
|
||||
string_params[k] = v
|
||||
|
||||
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
|
||||
response_json = response.json()
|
||||
response_json = check_response(response)
|
||||
|
||||
# Enhance the data with readable formats
|
||||
data = enhance_audit_data(response_json.get("data", []), endpoint)
|
||||
|
||||
274
res/device_group.py
Executable file
274
res/device_group.py
Executable file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def check_response(response):
|
||||
"""
|
||||
Check API response and handle errors.
|
||||
|
||||
Two error cases:
|
||||
1. Status code is not 200 -> exit with error
|
||||
2. Response contains {"error": "xxx"} -> exit with error
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code}: {response.text}")
|
||||
exit(1)
|
||||
|
||||
# Check for {"error": "xxx"} in response
|
||||
if response.text and response.text.strip():
|
||||
try:
|
||||
json_data = response.json()
|
||||
if isinstance(json_data, dict) and "error" in json_data:
|
||||
print(f"Error: {json_data['error']}")
|
||||
exit(1)
|
||||
return json_data
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def headers_with(token):
|
||||
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
# ---------- Device Group APIs ----------
|
||||
|
||||
def list_groups(url, token, name=None, page_size=50):
|
||||
headers = headers_with(token)
|
||||
params = {"pageSize": page_size}
|
||||
if name:
|
||||
params["name"] = name
|
||||
data, current = [], 1
|
||||
while True:
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/device-groups", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
print(f"Error: HTTP {r.status_code} - {r.text}")
|
||||
exit(1)
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
print(f"Error: {res['error']}")
|
||||
exit(1)
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
break
|
||||
return data
|
||||
|
||||
|
||||
def get_group_by_name(url, token, name):
|
||||
groups = list_groups(url, token, name)
|
||||
for g in groups:
|
||||
if str(g.get("name")) == name:
|
||||
return g
|
||||
return None
|
||||
|
||||
|
||||
def create_group(url, token, name, note=None, accessed_from=None):
|
||||
headers = headers_with(token)
|
||||
payload = {"name": name}
|
||||
if note:
|
||||
payload["note"] = note
|
||||
if accessed_from:
|
||||
payload["allowed_incomings"] = accessed_from
|
||||
r = requests.post(f"{url}/api/device-groups", headers=headers, json=payload)
|
||||
return check_response(r)
|
||||
|
||||
|
||||
def update_group(url, token, name, new_name=None, note=None, accessed_from=None):
|
||||
headers = headers_with(token)
|
||||
g = get_group_by_name(url, token, name)
|
||||
if not g:
|
||||
print(f"Error: Group '{name}' not found")
|
||||
exit(1)
|
||||
guid = g.get("guid")
|
||||
payload = {}
|
||||
if new_name is not None:
|
||||
payload["name"] = new_name
|
||||
if note is not None:
|
||||
payload["note"] = note
|
||||
if accessed_from is not None:
|
||||
payload["allowed_incomings"] = accessed_from
|
||||
r = requests.patch(f"{url}/api/device-groups/{guid}", headers=headers, json=payload)
|
||||
check_response(r)
|
||||
return "Success"
|
||||
|
||||
|
||||
def delete_groups(url, token, names):
|
||||
headers = headers_with(token)
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
for n in names:
|
||||
g = get_group_by_name(url, token, n)
|
||||
if not g:
|
||||
print(f"Error: Group '{n}' not found")
|
||||
exit(1)
|
||||
guid = g.get("guid")
|
||||
r = requests.delete(f"{url}/api/device-groups/{guid}", headers=headers)
|
||||
check_response(r)
|
||||
return "Success"
|
||||
|
||||
|
||||
# ---------- Device group assign APIs (name -> guid) ----------
|
||||
|
||||
def view_devices(url, token, group_name=None, id=None, device_name=None,
|
||||
user_name=None, device_username=None, page_size=50):
|
||||
"""View devices in a device group with filters"""
|
||||
headers = headers_with(token)
|
||||
|
||||
# Separate exact match and fuzzy match params
|
||||
params = {}
|
||||
fuzzy_params = {
|
||||
"id": id,
|
||||
"device_name": device_name,
|
||||
"user_name": user_name,
|
||||
"device_username": device_username,
|
||||
}
|
||||
|
||||
# Add device_group_name without wildcard (exact match)
|
||||
if group_name:
|
||||
params["device_group_name"] = group_name
|
||||
|
||||
# Add wildcard for fuzzy search to other params
|
||||
for k, v in fuzzy_params.items():
|
||||
if v is not None:
|
||||
params[k] = "%" + v + "%" if (v != "-" and "%" not in v) else v
|
||||
|
||||
params["pageSize"] = page_size
|
||||
|
||||
data, current = [], 1
|
||||
while True:
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/devices", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
return check_response(r)
|
||||
res = r.json()
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
break
|
||||
return data
|
||||
|
||||
|
||||
def add_devices(url, token, group_name, device_ids):
|
||||
headers = headers_with(token)
|
||||
g = get_group_by_name(url, token, group_name)
|
||||
if not g:
|
||||
return f"Group '{group_name}' not found"
|
||||
guid = g.get("guid")
|
||||
payload = device_ids if isinstance(device_ids, list) else [device_ids]
|
||||
r = requests.post(f"{url}/api/device-groups/{guid}", headers=headers, json=payload)
|
||||
return check_response(r)
|
||||
|
||||
|
||||
def remove_devices(url, token, group_name, device_ids):
|
||||
headers = headers_with(token)
|
||||
g = get_group_by_name(url, token, group_name)
|
||||
if not g:
|
||||
return f"Group '{group_name}' not found"
|
||||
guid = g.get("guid")
|
||||
payload = device_ids if isinstance(device_ids, list) else [device_ids]
|
||||
r = requests.delete(f"{url}/api/device-groups/{guid}/devices", headers=headers, json=payload)
|
||||
return check_response(r)
|
||||
|
||||
|
||||
def parse_rules(s):
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
v = json.loads(s)
|
||||
if isinstance(v, list):
|
||||
# expect list of {"type": number, "name": string}
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Device Group manager")
|
||||
parser.add_argument("command", choices=[
|
||||
"view", "add", "update", "delete",
|
||||
"view-devices", "add-devices", "remove-devices"
|
||||
], help=(
|
||||
"Command to execute. "
|
||||
"[view/add/update/delete/add-devices/remove-devices: require Device Group Permission] "
|
||||
"[view-devices: require Device Permission]"
|
||||
))
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--token", required=True)
|
||||
|
||||
parser.add_argument("--name", help="Device group name (exact match)")
|
||||
parser.add_argument("--new-name", help="New device group name (for update)")
|
||||
parser.add_argument("--note", help="Note")
|
||||
|
||||
parser.add_argument("--accessed-from", help="JSON array: '[{\"type\":0|2,\"name\":\"...\"}]' (0=User Group, 2=User)")
|
||||
|
||||
parser.add_argument("--ids", help="Comma separated device IDs for add-devices/remove-devices")
|
||||
|
||||
# Filters for view-devices command
|
||||
parser.add_argument("--id", help="Device ID filter (for view-devices)")
|
||||
parser.add_argument("--device-name", help="Device name filter (for view-devices)")
|
||||
parser.add_argument("--user-name", help="User name filter (owner of device, for view-devices)")
|
||||
parser.add_argument("--device-username", help="Device username filter (logged in user on device, for view-devices)")
|
||||
|
||||
args = parser.parse_args()
|
||||
while args.url.endswith("/"): args.url = args.url[:-1]
|
||||
|
||||
if args.command == "view":
|
||||
res = list_groups(args.url, args.token, args.name)
|
||||
print(json.dumps(res, indent=2))
|
||||
elif args.command == "add":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
print(create_group(
|
||||
args.url, args.token, args.name, args.note,
|
||||
parse_rules(args.accessed_from)
|
||||
))
|
||||
elif args.command == "update":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
print(update_group(
|
||||
args.url, args.token, args.name, args.new_name, args.note,
|
||||
parse_rules(args.accessed_from)
|
||||
))
|
||||
elif args.command == "delete":
|
||||
if not args.name:
|
||||
print("Error: --name is required (supports comma separated)")
|
||||
exit(1)
|
||||
names = [x.strip() for x in args.name.split(",") if x.strip()]
|
||||
print(delete_groups(args.url, args.token, names))
|
||||
elif args.command == "view-devices":
|
||||
res = view_devices(
|
||||
args.url,
|
||||
args.token,
|
||||
group_name=args.name,
|
||||
id=args.id,
|
||||
device_name=args.device_name,
|
||||
user_name=args.user_name,
|
||||
device_username=args.device_username
|
||||
)
|
||||
print(json.dumps(res, indent=2))
|
||||
elif args.command in ("add-devices", "remove-devices"):
|
||||
if not args.name or not args.ids:
|
||||
print("Error: --name and --ids are required for add/remove devices")
|
||||
exit(1)
|
||||
ids = [x.strip() for x in args.ids.split(",") if x.strip()]
|
||||
if args.command == "add-devices":
|
||||
print(add_devices(args.url, args.token, args.name, ids))
|
||||
else:
|
||||
print(remove_devices(args.url, args.token, args.name, ids))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -39,7 +39,14 @@ def view(
|
||||
while True:
|
||||
params["current"] = current
|
||||
response = requests.get(f"{url}/api/devices", headers=headers, params=params)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
|
||||
data = response_json.get("data", [])
|
||||
|
||||
@@ -62,14 +69,18 @@ def view(
|
||||
|
||||
|
||||
def check(response):
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
response_json = response.json()
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
else:
|
||||
return "Failed", response.status_code, response.text
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
|
||||
|
||||
def disable(url, token, guid, id):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.3
|
||||
Version: 1.4.4
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.3
|
||||
Version: 1.4.4
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.3
|
||||
Version: 1.4.4
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
301
res/strategies.py
Executable file
301
res/strategies.py
Executable file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def check_response(response):
|
||||
"""
|
||||
Check API response and handle errors.
|
||||
|
||||
Two error cases:
|
||||
1. Status code is not 200 -> exit with error
|
||||
2. Response contains {"error": "xxx"} -> exit with error
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code}: {response.text}")
|
||||
exit(1)
|
||||
|
||||
# Check for {"error": "xxx"} in response
|
||||
if response.text and response.text.strip():
|
||||
try:
|
||||
json_data = response.json()
|
||||
if isinstance(json_data, dict) and "error" in json_data:
|
||||
print(f"Error: {json_data['error']}")
|
||||
exit(1)
|
||||
return json_data
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def headers_with(token):
|
||||
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
# ---------- Strategies APIs ----------
|
||||
|
||||
def list_strategies(url, token):
|
||||
"""List all strategies"""
|
||||
headers = headers_with(token)
|
||||
r = requests.get(f"{url}/api/strategies", headers=headers)
|
||||
return check_response(r)
|
||||
|
||||
|
||||
def get_strategy_by_guid(url, token, guid):
|
||||
"""Get strategy by GUID"""
|
||||
headers = headers_with(token)
|
||||
r = requests.get(f"{url}/api/strategies/{guid}", headers=headers)
|
||||
return check_response(r)
|
||||
|
||||
|
||||
def get_strategy_by_name(url, token, name):
|
||||
"""Get strategy by name"""
|
||||
strategies = list_strategies(url, token)
|
||||
if not strategies:
|
||||
return None
|
||||
for s in strategies:
|
||||
if str(s.get("name")) == name:
|
||||
return s
|
||||
return None
|
||||
|
||||
|
||||
def enable_strategy(url, token, name):
|
||||
"""Enable a strategy"""
|
||||
headers = headers_with(token)
|
||||
strategy = get_strategy_by_name(url, token, name)
|
||||
if not strategy:
|
||||
print(f"Error: Strategy '{name}' not found")
|
||||
exit(1)
|
||||
guid = strategy.get("guid")
|
||||
r = requests.put(f"{url}/api/strategies/{guid}/status", headers=headers, json=True)
|
||||
check_response(r)
|
||||
return "Success"
|
||||
|
||||
|
||||
def disable_strategy(url, token, name):
|
||||
"""Disable a strategy"""
|
||||
headers = headers_with(token)
|
||||
strategy = get_strategy_by_name(url, token, name)
|
||||
if not strategy:
|
||||
print(f"Error: Strategy '{name}' not found")
|
||||
exit(1)
|
||||
guid = strategy.get("guid")
|
||||
r = requests.put(f"{url}/api/strategies/{guid}/status", headers=headers, json=False)
|
||||
check_response(r)
|
||||
return "Success"
|
||||
|
||||
|
||||
def get_device_guid_by_id(url, token, device_id):
|
||||
"""Get device GUID by device ID (exact match)"""
|
||||
headers = headers_with(token)
|
||||
params = {"id": device_id, "pageSize": 50}
|
||||
r = requests.get(f"{url}/api/devices", headers=headers, params=params)
|
||||
res = check_response(r)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
devices_data = res.get("data", []) if isinstance(res, dict) else res
|
||||
for d in devices_data:
|
||||
if d.get("id") == device_id:
|
||||
return d.get("guid")
|
||||
return None
|
||||
|
||||
|
||||
def get_user_guid_by_name(url, token, name):
|
||||
"""Get user GUID by exact name match"""
|
||||
headers = headers_with(token)
|
||||
params = {"name": name, "pageSize": 50}
|
||||
r = requests.get(f"{url}/api/users", headers=headers, params=params)
|
||||
res = check_response(r)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
users_data = res.get("data", []) if isinstance(res, dict) else res
|
||||
for u in users_data:
|
||||
if u.get("name") == name:
|
||||
return u.get("guid")
|
||||
return None
|
||||
|
||||
|
||||
def get_device_group_guid_by_name(url, token, name):
|
||||
"""Get device group GUID by exact name match"""
|
||||
headers = headers_with(token)
|
||||
params = {"pageSize": 50, "name": name}
|
||||
r = requests.get(f"{url}/api/device-groups", headers=headers, params=params)
|
||||
res = check_response(r)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
groups_data = res.get("data", []) if isinstance(res, dict) else res
|
||||
for g in groups_data:
|
||||
if g.get("name") == name:
|
||||
return g.get("guid")
|
||||
return None
|
||||
|
||||
|
||||
def assign_strategy(url, token, strategy_name, peers=None, users=None, device_groups=None):
|
||||
"""
|
||||
Assign strategy to peers, users, or device groups
|
||||
|
||||
Args:
|
||||
strategy_name: Name of the strategy (or None to unassign)
|
||||
peers: List of device IDs or GUIDs
|
||||
users: List of user names or GUIDs
|
||||
device_groups: List of device group names or GUIDs
|
||||
"""
|
||||
headers = headers_with(token)
|
||||
|
||||
# Get strategy GUID if strategy_name is provided
|
||||
strategy_guid = None
|
||||
if strategy_name:
|
||||
strategy = get_strategy_by_name(url, token, strategy_name)
|
||||
if not strategy:
|
||||
print(f"Error: Strategy '{strategy_name}' not found")
|
||||
exit(1)
|
||||
strategy_guid = strategy.get("guid")
|
||||
|
||||
# Convert device IDs to GUIDs
|
||||
peer_guids = []
|
||||
if peers:
|
||||
for peer in peers:
|
||||
# Check if it's already a GUID format
|
||||
if len(peer) == 36 and peer.count('-') == 4:
|
||||
peer_guids.append(peer)
|
||||
else:
|
||||
# Treat as device ID, look it up
|
||||
guid = get_device_guid_by_id(url, token, peer)
|
||||
if not guid:
|
||||
print(f"Error: Device '{peer}' not found")
|
||||
exit(1)
|
||||
peer_guids.append(guid)
|
||||
|
||||
# Convert user names to GUIDs
|
||||
user_guids = []
|
||||
if users:
|
||||
for user in users:
|
||||
# Check if it's already a GUID format
|
||||
if len(user) == 36 and user.count('-') == 4:
|
||||
user_guids.append(user)
|
||||
else:
|
||||
# Treat as username, look it up
|
||||
guid = get_user_guid_by_name(url, token, user)
|
||||
if not guid:
|
||||
print(f"Error: User '{user}' not found")
|
||||
exit(1)
|
||||
user_guids.append(guid)
|
||||
|
||||
# Convert device group names to GUIDs
|
||||
device_group_guids = []
|
||||
if device_groups:
|
||||
for dg in device_groups:
|
||||
# Check if it's already a GUID format
|
||||
if len(dg) == 36 and dg.count('-') == 4:
|
||||
device_group_guids.append(dg)
|
||||
else:
|
||||
# Treat as device group name, look it up
|
||||
guid = get_device_group_guid_by_name(url, token, dg)
|
||||
if not guid:
|
||||
print(f"Error: Device group '{dg}' not found")
|
||||
exit(1)
|
||||
device_group_guids.append(guid)
|
||||
|
||||
# Build payload
|
||||
payload = {}
|
||||
if strategy_guid:
|
||||
payload["strategy"] = strategy_guid
|
||||
|
||||
payload["peers"] = peer_guids
|
||||
payload["users"] = user_guids
|
||||
payload["groups"] = device_group_guids
|
||||
|
||||
r = requests.post(f"{url}/api/strategies/assign", headers=headers, json=payload)
|
||||
check_response(r)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Strategy manager")
|
||||
parser.add_argument("command", choices=[
|
||||
"list", "view", "enable", "disable", "assign", "unassign"
|
||||
])
|
||||
parser.add_argument("--url", required=True, help="Server URL")
|
||||
parser.add_argument("--token", required=True, help="API token")
|
||||
|
||||
parser.add_argument("--name", help="Strategy name (for view/enable/disable/assign commands)")
|
||||
parser.add_argument("--guid", help="Strategy GUID (for view command, alternative to --name)")
|
||||
|
||||
# For assign/unassign commands
|
||||
parser.add_argument("--peers", help="Comma separated device IDs or GUIDs (requires Device Permission:r)")
|
||||
parser.add_argument("--users", help="Comma separated user names or GUIDs (requires User Permission:r)")
|
||||
parser.add_argument("--device-groups", help="Comma separated device group names or GUIDs (requires Device Group Permission:r)")
|
||||
|
||||
args = parser.parse_args()
|
||||
while args.url.endswith("/"): args.url = args.url[:-1]
|
||||
|
||||
if args.command == "list":
|
||||
res = list_strategies(args.url, args.token)
|
||||
print(json.dumps(res, indent=2))
|
||||
|
||||
elif args.command == "view":
|
||||
if args.guid:
|
||||
res = get_strategy_by_guid(args.url, args.token, args.guid)
|
||||
print(json.dumps(res, indent=2))
|
||||
elif args.name:
|
||||
strategy = get_strategy_by_name(args.url, args.token, args.name)
|
||||
if not strategy:
|
||||
print(f"Error: Strategy '{args.name}' not found")
|
||||
exit(1)
|
||||
# Get full details by GUID
|
||||
guid = strategy.get("guid")
|
||||
res = get_strategy_by_guid(args.url, args.token, guid)
|
||||
print(json.dumps(res, indent=2))
|
||||
else:
|
||||
print("Error: --name or --guid is required for view command")
|
||||
exit(1)
|
||||
|
||||
elif args.command == "enable":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
print(enable_strategy(args.url, args.token, args.name))
|
||||
|
||||
elif args.command == "disable":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
print(disable_strategy(args.url, args.token, args.name))
|
||||
|
||||
elif args.command == "assign":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
if not args.peers and not args.users and not args.device_groups:
|
||||
print("Error: at least one of --peers, --users, or --device-groups is required")
|
||||
exit(1)
|
||||
|
||||
peers = [x.strip() for x in args.peers.split(",") if x.strip()] if args.peers else None
|
||||
users = [x.strip() for x in args.users.split(",") if x.strip()] if args.users else None
|
||||
device_groups = [x.strip() for x in args.device_groups.split(",") if x.strip()] if args.device_groups else None
|
||||
|
||||
assign_strategy(args.url, args.token, args.name, peers=peers, users=users, device_groups=device_groups)
|
||||
count = (len(peers) if peers else 0) + (len(users) if users else 0) + (len(device_groups) if device_groups else 0)
|
||||
print(f"Success: Assigned strategy '{args.name}' to {count} target(s)")
|
||||
|
||||
elif args.command == "unassign":
|
||||
if not args.peers and not args.users and not args.device_groups:
|
||||
print("Error: at least one of --peers, --users, or --device-groups is required")
|
||||
exit(1)
|
||||
|
||||
peers = [x.strip() for x in args.peers.split(",") if x.strip()] if args.peers else None
|
||||
users = [x.strip() for x in args.users.split(",") if x.strip()] if args.users else None
|
||||
device_groups = [x.strip() for x in args.device_groups.split(",") if x.strip()] if args.device_groups else None
|
||||
|
||||
assign_strategy(args.url, args.token, None, peers=peers, users=users, device_groups=device_groups)
|
||||
count = (len(peers) if peers else 0) + (len(users) if users else 0) + (len(device_groups) if device_groups else 0)
|
||||
print(f"Success: Unassigned strategy from {count} target(s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
302
res/user_group.py
Executable file
302
res/user_group.py
Executable file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def check_response(response):
|
||||
"""
|
||||
Check API response and handle errors.
|
||||
|
||||
Two error cases:
|
||||
1. Status code is not 200 -> exit with error
|
||||
2. Response contains {"error": "xxx"} -> exit with error
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code}: {response.text}")
|
||||
exit(1)
|
||||
|
||||
# Check for {"error": "xxx"} in response
|
||||
if response.text and response.text.strip():
|
||||
try:
|
||||
json_data = response.json()
|
||||
if isinstance(json_data, dict) and "error" in json_data:
|
||||
print(f"Error: {json_data['error']}")
|
||||
exit(1)
|
||||
return json_data
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def headers_with(token):
|
||||
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
# ---------- User Group APIs ----------
|
||||
|
||||
def list_groups(url, token, name=None, page_size=50):
|
||||
headers = headers_with(token)
|
||||
params = {"pageSize": page_size}
|
||||
if name:
|
||||
params["name"] = name
|
||||
data, current = [], 1
|
||||
while True:
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/user-groups", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
print(f"Error: HTTP {r.status_code} - {r.text}")
|
||||
exit(1)
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
print(f"Error: {res['error']}")
|
||||
exit(1)
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
break
|
||||
return data
|
||||
|
||||
|
||||
def get_group_by_name(url, token, name):
|
||||
groups = list_groups(url, token, name)
|
||||
for g in groups:
|
||||
if str(g.get("name")) == name:
|
||||
return g
|
||||
return None
|
||||
|
||||
|
||||
def create_group(url, token, name, note=None, accessed_from=None, access_to=None):
|
||||
headers = headers_with(token)
|
||||
payload = {"name": name}
|
||||
if note:
|
||||
payload["note"] = note
|
||||
if accessed_from:
|
||||
payload["allowed_incomings"] = accessed_from
|
||||
if access_to:
|
||||
payload["allowed_outgoings"] = access_to
|
||||
r = requests.post(f"{url}/api/user-groups", headers=headers, json=payload)
|
||||
return check_response(r)
|
||||
|
||||
|
||||
def update_group(url, token, name, new_name=None, note=None, accessed_from=None, access_to=None):
|
||||
headers = headers_with(token)
|
||||
g = get_group_by_name(url, token, name)
|
||||
if not g:
|
||||
print(f"Error: Group '{name}' not found")
|
||||
exit(1)
|
||||
guid = g.get("guid")
|
||||
payload = {}
|
||||
if new_name is not None:
|
||||
payload["name"] = new_name
|
||||
if note is not None:
|
||||
payload["note"] = note
|
||||
if accessed_from is not None:
|
||||
payload["allowed_incomings"] = accessed_from
|
||||
if access_to is not None:
|
||||
payload["allowed_outgoings"] = access_to
|
||||
r = requests.patch(f"{url}/api/user-groups/{guid}", headers=headers, json=payload)
|
||||
check_response(r)
|
||||
return "Success"
|
||||
|
||||
|
||||
def delete_groups(url, token, names):
|
||||
headers = headers_with(token)
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
for n in names:
|
||||
g = get_group_by_name(url, token, n)
|
||||
if not g:
|
||||
print(f"Error: Group '{n}' not found")
|
||||
exit(1)
|
||||
guid = g.get("guid")
|
||||
r = requests.delete(f"{url}/api/user-groups/{guid}", headers=headers)
|
||||
check_response(r)
|
||||
return "Success"
|
||||
|
||||
|
||||
# ---------- User management in group ----------
|
||||
|
||||
def view_users(url, token, group_name=None, name=None, page_size=50):
|
||||
"""View users in a user group with filters"""
|
||||
headers = headers_with(token)
|
||||
|
||||
# Separate exact match and fuzzy match params
|
||||
params = {}
|
||||
fuzzy_params = {
|
||||
"name": name,
|
||||
}
|
||||
|
||||
# Add group_name without wildcard (exact match)
|
||||
if group_name:
|
||||
params["group_name"] = group_name
|
||||
|
||||
# Add wildcard for fuzzy search to other params
|
||||
for k, v in fuzzy_params.items():
|
||||
if v is not None:
|
||||
params[k] = "%" + v + "%" if (v != "-" and "%" not in v) else v
|
||||
|
||||
params["pageSize"] = page_size
|
||||
|
||||
data, current = [], 1
|
||||
while True:
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/users", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
return check_response(r)
|
||||
res = r.json()
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
break
|
||||
return data
|
||||
|
||||
|
||||
def add_users(url, token, group_name, user_names):
|
||||
"""Add users to a user group"""
|
||||
headers = headers_with(token)
|
||||
if isinstance(user_names, str):
|
||||
user_names = [user_names]
|
||||
|
||||
# Get the user group guid
|
||||
g = get_group_by_name(url, token, group_name)
|
||||
if not g:
|
||||
print(f"Error: Group '{group_name}' not found")
|
||||
exit(1)
|
||||
guid = g.get("guid")
|
||||
|
||||
# Get user GUIDs
|
||||
user_guids = []
|
||||
errors = []
|
||||
|
||||
for user_name in user_names:
|
||||
# Get user by exact name match
|
||||
params = {"name": user_name, "pageSize": 50}
|
||||
r = requests.get(f"{url}/api/users", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
errors.append(f"{user_name}: HTTP {r.status_code}")
|
||||
continue
|
||||
|
||||
users_data = r.json()
|
||||
users_list = users_data.get("data", [])
|
||||
user = None
|
||||
for u in users_list:
|
||||
if u.get("name") == user_name:
|
||||
user = u
|
||||
break
|
||||
|
||||
if not user:
|
||||
errors.append(f"{user_name}: User not found")
|
||||
continue
|
||||
|
||||
user_guids.append(user["guid"])
|
||||
|
||||
if not user_guids:
|
||||
msg = "Error: No valid users found"
|
||||
if errors:
|
||||
msg += ". " + "; ".join(errors)
|
||||
print(msg)
|
||||
exit(1)
|
||||
|
||||
# Add users to group using POST /api/user-groups/:guid
|
||||
r = requests.post(f"{url}/api/user-groups/{guid}", headers=headers, json=user_guids)
|
||||
check_response(r)
|
||||
|
||||
success_msg = f"Success: Added {len(user_guids)} user(s) to group '{group_name}'"
|
||||
if errors:
|
||||
return success_msg + " (with errors: " + "; ".join(errors) + ")"
|
||||
return success_msg
|
||||
|
||||
|
||||
def parse_rules(s):
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
v = json.loads(s)
|
||||
if isinstance(v, list):
|
||||
# expect list of {"type": number, "name": string}
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="User Group manager")
|
||||
parser.add_argument("command", choices=[
|
||||
"view", "add", "update", "delete",
|
||||
"view-users", "add-users"
|
||||
], help=(
|
||||
"Command to execute. "
|
||||
"[view/add/update/delete/add-users: require User Group Permission] "
|
||||
"[view-users: require User Permission]"
|
||||
))
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--token", required=True)
|
||||
|
||||
parser.add_argument("--name", help="User group name (exact match)")
|
||||
parser.add_argument("--new-name", help="New user group name (for update)")
|
||||
parser.add_argument("--note", help="Note")
|
||||
|
||||
parser.add_argument("--accessed-from", help="JSON array: '[{\"type\":0|2,\"name\":\"...\"}]' (0=User Group, 2=User)")
|
||||
parser.add_argument("--access-to", help="JSON array: '[{\"type\":0|1,\"name\":\"...\"}]' (0=User Group, 1=Device Group)")
|
||||
|
||||
parser.add_argument("--users", help="Comma separated usernames for add-users")
|
||||
|
||||
# Filters for view-users command
|
||||
parser.add_argument("--user-name", help="User name filter (for view-users, supports fuzzy search)")
|
||||
|
||||
args = parser.parse_args()
|
||||
while args.url.endswith("/"): args.url = args.url[:-1]
|
||||
|
||||
if args.command == "view":
|
||||
res = list_groups(args.url, args.token, args.name)
|
||||
print(json.dumps(res, indent=2))
|
||||
elif args.command == "add":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
print(create_group(
|
||||
args.url, args.token, args.name, args.note,
|
||||
parse_rules(args.accessed_from),
|
||||
parse_rules(args.access_to)
|
||||
))
|
||||
elif args.command == "update":
|
||||
if not args.name:
|
||||
print("Error: --name is required")
|
||||
exit(1)
|
||||
print(update_group(
|
||||
args.url, args.token, args.name, args.new_name, args.note,
|
||||
parse_rules(args.accessed_from),
|
||||
parse_rules(args.access_to)
|
||||
))
|
||||
elif args.command == "delete":
|
||||
if not args.name:
|
||||
print("Error: --name is required (supports comma separated)")
|
||||
exit(1)
|
||||
names = [x.strip() for x in args.name.split(",") if x.strip()]
|
||||
print(delete_groups(args.url, args.token, names))
|
||||
elif args.command == "view-users":
|
||||
res = view_users(
|
||||
args.url,
|
||||
args.token,
|
||||
group_name=args.name,
|
||||
name=args.user_name
|
||||
)
|
||||
print(json.dumps(res, indent=2))
|
||||
elif args.command == "add-users":
|
||||
if not args.name or not args.users:
|
||||
print("Error: --name and --users are required")
|
||||
exit(1)
|
||||
users = [x.strip() for x in args.users.split(",") if x.strip()]
|
||||
print(add_users(args.url, args.token, args.name, users))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
231
res/users.py
231
res/users.py
@@ -5,6 +5,28 @@ import argparse
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def check_response(response):
|
||||
"""
|
||||
Check API response and handle errors properly.
|
||||
Exit with code 1 if there's an error.
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code}: {response.text}")
|
||||
exit(1)
|
||||
|
||||
if response.text and response.text.strip():
|
||||
try:
|
||||
json_data = response.json()
|
||||
if isinstance(json_data, dict) and "error" in json_data:
|
||||
print(f"Error: {json_data['error']}")
|
||||
exit(1)
|
||||
return json_data
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def view(
|
||||
url,
|
||||
token,
|
||||
@@ -32,7 +54,14 @@ def view(
|
||||
while True:
|
||||
params["current"] = current
|
||||
response = requests.get(f"{url}/api/users", headers=headers, params=params)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||
exit(1)
|
||||
|
||||
response_json = response.json()
|
||||
if "error" in response_json:
|
||||
print(f"Error: {response_json['error']}")
|
||||
exit(1)
|
||||
|
||||
data = response_json.get("data", [])
|
||||
users.extend(data)
|
||||
@@ -45,43 +74,122 @@ def view(
|
||||
return users
|
||||
|
||||
|
||||
def check(response):
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
response_json = response.json()
|
||||
return response_json
|
||||
except ValueError:
|
||||
return response.text or "Success"
|
||||
else:
|
||||
return "Failed", response.status_code, response.text
|
||||
|
||||
|
||||
def disable(url, token, guid, name):
|
||||
print("Disable", name)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.post(f"{url}/api/users/{guid}/disable", headers=headers)
|
||||
return check(response)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def enable(url, token, guid, name):
|
||||
print("Enable", name)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.post(f"{url}/api/users/{guid}/enable", headers=headers)
|
||||
return check(response)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def delete(url, token, guid, name):
|
||||
def delete_user(url, token, guid, name):
|
||||
print("Delete", name)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.delete(f"{url}/api/users/{guid}", headers=headers)
|
||||
return check(response)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def new_user(url, token, name, password, group_name=None, email=None, note=None):
|
||||
"""Create a new user"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"name": name,
|
||||
"password": password,
|
||||
}
|
||||
if group_name:
|
||||
payload["group_name"] = group_name
|
||||
if email:
|
||||
payload["email"] = email
|
||||
if note:
|
||||
payload["note"] = note
|
||||
response = requests.post(f"{url}/api/users", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def invite_user(url, token, email, name, group_name=None, note=None):
|
||||
"""Invite a user by email"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"email": email,
|
||||
"name": name,
|
||||
}
|
||||
if group_name:
|
||||
payload["group_name"] = group_name
|
||||
if note:
|
||||
payload["note"] = note
|
||||
response = requests.post(f"{url}/api/users/invite", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def enable_2fa_enforce(url, token, user_guids, base_url):
|
||||
"""Enable 2FA enforcement for users"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
|
||||
"enforce": True,
|
||||
"url": base_url
|
||||
}
|
||||
response = requests.put(f"{url}/api/users/tfa/totp/enforce", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def disable_2fa_enforce(url, token, user_guids, base_url=""):
|
||||
"""Disable 2FA enforcement for users"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
|
||||
"enforce": False,
|
||||
"url": base_url
|
||||
}
|
||||
response = requests.put(f"{url}/api/users/tfa/totp/enforce", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def disable_email_verification(url, token, user_guids):
|
||||
"""Disable email login verification for users"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
|
||||
"type": "email"
|
||||
}
|
||||
response = requests.put(f"{url}/api/users/disable_login_verification", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def reset_2fa(url, token, user_guids):
|
||||
"""Reset 2FA for users"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
|
||||
"type": "2fa"
|
||||
}
|
||||
response = requests.put(f"{url}/api/users/disable_login_verification", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def force_logout(url, token, user_guids):
|
||||
"""Force logout users"""
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
|
||||
}
|
||||
response = requests.post(f"{url}/api/users/force-logout", headers=headers, json=payload)
|
||||
check_response(response)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="User manager")
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=["view", "disable", "enable", "delete"],
|
||||
choices=["view", "disable", "enable", "delete", "new", "invite",
|
||||
"enable-2fa-enforce", "disable-2fa-enforce",
|
||||
"disable-email-verification", "reset-2fa", "force-logout"],
|
||||
help="Command to execute",
|
||||
)
|
||||
parser.add_argument("--url", required=True, help="URL of the API")
|
||||
@@ -89,12 +197,32 @@ def main():
|
||||
"--token", required=True, help="Bearer token for authentication"
|
||||
)
|
||||
parser.add_argument("--name", help="User name")
|
||||
parser.add_argument("--group_name", help="Group name")
|
||||
parser.add_argument("--group_name", help="Group name (for filtering in view, or for new/invite command)")
|
||||
parser.add_argument("--password", help="User password (for new command)")
|
||||
parser.add_argument("--email", help="User email (for invite command)")
|
||||
parser.add_argument("--note", help="User note (for new/invite command)")
|
||||
parser.add_argument("--web-console-url", help="Web console URL (for 2FA enforce commands)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
while args.url.endswith("/"): args.url = args.url[:-1]
|
||||
|
||||
if args.command == "new":
|
||||
if not args.name or not args.password or not args.group_name:
|
||||
print("Error: --name and --password and --group_name are required for new command")
|
||||
exit(1)
|
||||
new_user(args.url, args.token, args.name, args.password, args.group_name, args.email, args.note)
|
||||
print("Success: User created")
|
||||
return
|
||||
|
||||
if args.command == "invite":
|
||||
if not args.email or not args.name or not args.group_name:
|
||||
print("Error: --email and --name and --group_name are required for invite command")
|
||||
exit(1)
|
||||
invite_user(args.url, args.token, args.email, args.name, args.group_name, args.note)
|
||||
print("Success: Invitation sent")
|
||||
return
|
||||
|
||||
users = view(
|
||||
args.url,
|
||||
args.token,
|
||||
@@ -103,20 +231,61 @@ def main():
|
||||
)
|
||||
|
||||
if args.command == "view":
|
||||
for user in users:
|
||||
print(user)
|
||||
elif args.command == "disable":
|
||||
for user in users:
|
||||
response = disable(args.url, args.token, user["guid"], user["name"])
|
||||
print(response)
|
||||
elif args.command == "enable":
|
||||
for user in users:
|
||||
response = enable(args.url, args.token, user["guid"], user["name"])
|
||||
print(response)
|
||||
elif args.command == "delete":
|
||||
for user in users:
|
||||
response = delete(args.url, args.token, user["guid"], user["name"])
|
||||
print(response)
|
||||
if len(users) == 0:
|
||||
print("Found 0 users")
|
||||
else:
|
||||
for user in users:
|
||||
print(user)
|
||||
elif args.command in ["disable", "enable", "delete", "enable-2fa-enforce",
|
||||
"disable-2fa-enforce", "disable-email-verification", "reset-2fa", "force-logout"]:
|
||||
if len(users) == 0:
|
||||
print("Found 0 users")
|
||||
return
|
||||
|
||||
# Check if we need user confirmation for multiple users
|
||||
if len(users) > 1:
|
||||
print(f"Found {len(users)} users. Do you want to proceed with {args.command} operation on the users? (Y/N)")
|
||||
confirmation = input("Type 'Y' to confirm: ").strip()
|
||||
if confirmation.upper() != 'Y':
|
||||
print("Operation cancelled.")
|
||||
return
|
||||
|
||||
if args.command == "disable":
|
||||
for user in users:
|
||||
disable(args.url, args.token, user["guid"], user["name"])
|
||||
print("Success")
|
||||
elif args.command == "enable":
|
||||
for user in users:
|
||||
enable(args.url, args.token, user["guid"], user["name"])
|
||||
print("Success")
|
||||
elif args.command == "delete":
|
||||
for user in users:
|
||||
delete_user(args.url, args.token, user["guid"], user["name"])
|
||||
print("Success")
|
||||
elif args.command == "enable-2fa-enforce":
|
||||
if not args.web_console_url:
|
||||
print("Error: --web-console-url is required for enable-2fa-enforce")
|
||||
exit(1)
|
||||
user_guids = [user["guid"] for user in users]
|
||||
enable_2fa_enforce(args.url, args.token, user_guids, args.web_console_url)
|
||||
print(f"Success: Enabled 2FA enforcement for {len(users)} user(s)")
|
||||
elif args.command == "disable-2fa-enforce":
|
||||
user_guids = [user["guid"] for user in users]
|
||||
web_url = args.web_console_url or ""
|
||||
disable_2fa_enforce(args.url, args.token, user_guids, web_url)
|
||||
print(f"Success: Disabled 2FA enforcement for {len(users)} user(s)")
|
||||
elif args.command == "disable-email-verification":
|
||||
user_guids = [user["guid"] for user in users]
|
||||
disable_email_verification(args.url, args.token, user_guids)
|
||||
print(f"Success: Disabled email verification for {len(users)} user(s)")
|
||||
elif args.command == "reset-2fa":
|
||||
user_guids = [user["guid"] for user in users]
|
||||
reset_2fa(args.url, args.token, user_guids)
|
||||
print(f"Success: Reset 2FA for {len(users)} user(s)")
|
||||
elif args.command == "force-logout":
|
||||
user_guids = [user["guid"] for user in users]
|
||||
force_logout(args.url, args.token, user_guids)
|
||||
print(f"Success: Force logout for {len(users)} user(s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: RustDesk <support@rustdesk.com>
|
||||
Date: Fri, 1 Nov 2025 08:00:00 +0000
|
||||
Subject: [PATCH] Fix CVBufferCopyAttachments crash on macOS Big Sur
|
||||
|
||||
Use weak linking for CVBufferCopyAttachments to avoid symbol resolution
|
||||
crash on macOS < 12. The function will be NULL on older systems and the
|
||||
code will fall back to the deprecated CVBufferGetAttachments.
|
||||
|
||||
This fixes a crash on macOS Big Sur (11.x) where CVBufferCopyAttachments
|
||||
is not available. The runtime check with __builtin_available is not enough
|
||||
because the symbol is still resolved at load time, causing a dyld error.
|
||||
|
||||
Fixes: https://github.com/rustdesk/rustdesk/issues/13377
|
||||
---
|
||||
libavutil/hwcontext_videotoolbox.c | 21 ++++++++++++++++++++-
|
||||
1 file changed, 20 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/libavutil/hwcontext_videotoolbox.c b/libavutil/hwcontext_videotoolbox.c
|
||||
index 0000000000..1111111111 100644
|
||||
--- a/libavutil/hwcontext_videotoolbox.c
|
||||
+++ b/libavutil/hwcontext_videotoolbox.c
|
||||
@@ -33,6 +33,25 @@
|
||||
#include "pixfmt.h"
|
||||
#include "pixdesc.h"
|
||||
|
||||
+// Weak import CVBufferCopyAttachments to support macOS < 12
|
||||
+// The runtime check with __builtin_available is not enough because
|
||||
+// the symbol is still resolved at load time, causing dyld errors on Big Sur.
|
||||
+// With weak_import, the function pointer will be NULL on older systems.
|
||||
+#if TARGET_OS_OSX && defined(__MAC_12_0) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_12_0
|
||||
+extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode)
|
||||
+ __attribute__((weak_import));
|
||||
+#endif
|
||||
+#if TARGET_OS_IOS && defined(__IPHONE_15_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0
|
||||
+extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode)
|
||||
+ __attribute__((weak_import));
|
||||
+#endif
|
||||
+#if TARGET_OS_TV && defined(__TVOS_15_0) && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0
|
||||
+extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode)
|
||||
+ __attribute__((weak_import));
|
||||
+#endif
|
||||
+
|
||||
+// End of weak import section
|
||||
+
|
||||
typedef struct VTFramesContext {
|
||||
/**
|
||||
* The public AVVTFramesContext. See hwcontext_videotoolbox.h for it.
|
||||
@@ -547,7 +566,7 @@ static CFDictionaryRef vt_cv_buffer_copy_attachments(CVBufferRef buffer,
|
||||
(TARGET_OS_TV && defined(__TVOS_15_0) && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0)
|
||||
// On recent enough versions, just use the respective API
|
||||
if (__builtin_available(macOS 12.0, iOS 15.0, tvOS 15.0, *))
|
||||
- return CVBufferCopyAttachments(buffer, attachment_mode);
|
||||
+ if (CVBufferCopyAttachments != NULL) return CVBufferCopyAttachments(buffer, attachment_mode);
|
||||
#endif
|
||||
|
||||
// Check that the target is lower than macOS 12 / iOS 15 / tvOS 15
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -27,6 +27,7 @@ vcpkg_from_github(
|
||||
patch/0009-fix-nvenc-reconfigure-blur.patch
|
||||
patch/0010.disable-loading-DLLs-from-app-dir.patch
|
||||
patch/0011-android-mediacodec-encode-align-64.patch
|
||||
patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch
|
||||
)
|
||||
|
||||
if(SOURCE_PATH MATCHES " ")
|
||||
|
||||
@@ -1976,13 +1976,24 @@ impl LoginConfigHandler {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The view style to be saved.
|
||||
/// * `value` - The scroll style to be saved.
|
||||
pub fn save_scroll_style(&mut self, value: String) {
|
||||
let mut config = self.load_config();
|
||||
config.scroll_style = value;
|
||||
self.save_config(config);
|
||||
}
|
||||
|
||||
/// Save edge scroll edge thickness to the current config.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The edge thickness to be saved.
|
||||
pub fn save_edge_scroll_edge_thickness(&mut self, value: i32) {
|
||||
let mut config = self.load_config();
|
||||
config.edge_scroll_edge_thickness = value;
|
||||
self.save_config(config);
|
||||
}
|
||||
|
||||
/// Set a ui config of flutter for handler's [`PeerConfig`].
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -1755,6 +1755,13 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
thread.video_sender.send(MediaData::Reset).ok();
|
||||
}
|
||||
|
||||
let mut scale = 1.0;
|
||||
if let Some(pi) = &self.handler.lc.read().unwrap().peer_info {
|
||||
if let Some(d) = pi.displays.get(s.display as usize) {
|
||||
scale = d.scale;
|
||||
}
|
||||
}
|
||||
|
||||
if s.width > 0 && s.height > 0 {
|
||||
self.handler.set_display(
|
||||
s.x,
|
||||
@@ -1762,6 +1769,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
s.width,
|
||||
s.height,
|
||||
s.cursor_embedded,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user