Compare commits
89 Commits
fdroid-ver
...
1.2.5
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -38,10 +38,8 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
|||||||
2
.github/workflows/build-macos-arm64.yml
vendored
2
.github/workflows/build-macos-arm64.yml
vendored
@@ -18,7 +18,7 @@ env:
|
|||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.03.25
|
# vcpkg version: 2024.03.25
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||||
VERSION: "1.2.4"
|
VERSION: "1.2.5"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
|||||||
13
.github/workflows/flutter-build.yml
vendored
13
.github/workflows/flutter-build.yml
vendored
@@ -22,7 +22,7 @@ env:
|
|||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.03.25
|
# vcpkg version: 2024.03.25
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||||
VERSION: "1.2.4"
|
VERSION: "1.2.5"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -548,13 +548,14 @@ jobs:
|
|||||||
- {
|
- {
|
||||||
target: x86_64-apple-darwin,
|
target: x86_64-apple-darwin,
|
||||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
extra-build-args: "--disable-flutter-texture-render",
|
extra-build-args: "",
|
||||||
arch: x86_64,
|
arch: x86_64,
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-apple-darwin,
|
target: aarch64-apple-darwin,
|
||||||
os: macos-latest,
|
os: macos-latest,
|
||||||
extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
|
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
|
||||||
|
extra-build-args: "",
|
||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
@@ -783,7 +784,6 @@ jobs:
|
|||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
@@ -797,7 +797,6 @@ jobs:
|
|||||||
libxdo-dev \
|
libxdo-dev \
|
||||||
libxfixes-dev \
|
libxfixes-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openjdk-11-jdk-headless \
|
openjdk-11-jdk-headless \
|
||||||
@@ -1073,7 +1072,6 @@ jobs:
|
|||||||
libappindicator3-dev \
|
libappindicator3-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
@@ -1087,7 +1085,6 @@ jobs:
|
|||||||
libxdo-dev \
|
libxdo-dev \
|
||||||
libxfixes-dev \
|
libxfixes-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@@ -1128,7 +1125,7 @@ jobs:
|
|||||||
export JOBS=""
|
export JOBS=""
|
||||||
fi
|
fi
|
||||||
echo $JOBS
|
echo $JOBS
|
||||||
cargo build --lib $JOBS --features hwcodec,flutter,flutter_texture_render --release
|
cargo build --lib $JOBS --features hwcodec,flutter --release
|
||||||
rm -rf target/release/deps target/release/build
|
rm -rf target/release/deps target/release/build
|
||||||
rm -rf ~/.cargo
|
rm -rf ~/.cargo
|
||||||
|
|
||||||
|
|||||||
20
.github/workflows/flutter-tag.yml
vendored
20
.github/workflows/flutter-tag.yml
vendored
@@ -16,23 +16,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload-artifact: true
|
upload-artifact: true
|
||||||
upload-tag: ${{ github.ref_name }}
|
upload-tag: ${{ github.ref_name }}
|
||||||
|
|
||||||
update-fdroid-version-file:
|
|
||||||
name: Publish RustDesk version file for F-Droid updater
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Generate RustDesk version file
|
|
||||||
run: |
|
|
||||||
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
|
|
||||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
|
|
||||||
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
|
||||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Publish RustDesk version file
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: "fdroid-version"
|
|
||||||
files: |
|
|
||||||
./rustdesk-version.txt
|
|
||||||
|
|||||||
2
.github/workflows/history.yml
vendored
2
.github/workflows/history.yml
vendored
@@ -8,7 +8,7 @@ env:
|
|||||||
TAG_NAME: "tmp"
|
TAG_NAME: "tmp"
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VERSION: "1.2.4"
|
VERSION: "1.2.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-for-history-windows:
|
build-for-history-windows:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -52,3 +52,5 @@ lib/generated_bridge.dart
|
|||||||
examples/**/target/
|
examples/**/target/
|
||||||
# ===
|
# ===
|
||||||
vcpkg_installed
|
vcpkg_installed
|
||||||
|
flutter/lib/generated_plugin_registrant.dart
|
||||||
|
libsciter.dylib
|
||||||
|
|||||||
196
Cargo.lock
generated
196
Cargo.lock
generated
@@ -763,9 +763,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.5.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
]
|
]
|
||||||
@@ -1101,7 +1101,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "confy"
|
name = "confy"
|
||||||
version = "0.4.0-2"
|
version = "0.4.0-2"
|
||||||
source = "git+https://github.com/open-trade/confy#7855cd3c32b1a60b44e5076ee8f6b4131da10350"
|
source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"directories-next",
|
"directories-next",
|
||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
@@ -2092,7 +2092,7 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"spin 0.9.8",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2903,7 +2903,6 @@ dependencies = [
|
|||||||
"osascript",
|
"osascript",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"protobuf-codegen",
|
"protobuf-codegen",
|
||||||
"quinn",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -2918,7 +2917,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls 0.26.0",
|
"tokio-rustls 0.26.0",
|
||||||
"tokio-socks 0.5.1-2",
|
"tokio-socks 0.5.2",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
"url",
|
"url",
|
||||||
@@ -3038,8 +3037,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.4.9"
|
version = "0.4.15"
|
||||||
source = "git+https://github.com/21pages/hwcodec#7a52282267cb6aadbcd74c132bd4ecd43ab6f505"
|
source = "git+https://github.com/21pages/hwcodec#1d504ee590c15472813fecc22cee4b8149b2b8cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -3764,15 +3763,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miow"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mouce"
|
name = "mouce"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4394,12 +4384,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "parity-tokio-ipc"
|
name = "parity-tokio-ipc"
|
||||||
version = "0.7.3-3"
|
version = "0.7.3-3"
|
||||||
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#f2d1fcf8fb002335d9a62bec308559d40698694d"
|
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#e8448ade10d6d972d0b2307646424b36ab202ff5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"miow",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"tokio",
|
"tokio",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
@@ -4822,56 +4811,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e8b432585672228923edbbf64b8b12c14e1112f62e88737655b4a083dbcd78e"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.20.9",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"webpki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.9.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94b0b33c13a79f669c85defaf4c275dc86a0c0372807d0ca3d78e0bb87274863"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"ring 0.16.20",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.20.9",
|
|
||||||
"rustls-native-certs 0.6.3",
|
|
||||||
"slab",
|
|
||||||
"thiserror",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"webpki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"quinn-proto",
|
|
||||||
"socket2 0.4.10",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.42.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "0.6.13"
|
version = "0.6.13"
|
||||||
@@ -5230,21 +5169,6 @@ dependencies = [
|
|||||||
"winreg 0.50.0",
|
"winreg 0.50.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ring"
|
|
||||||
version = "0.16.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"spin 0.5.2",
|
|
||||||
"untrusted 0.7.1",
|
|
||||||
"web-sys",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.5"
|
version = "0.17.5"
|
||||||
@@ -5254,8 +5178,8 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"libc",
|
"libc",
|
||||||
"spin 0.9.8",
|
"spin",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5365,7 +5289,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -5462,7 +5386,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -5513,17 +5437,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls"
|
|
||||||
version = "0.20.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
|
|
||||||
dependencies = [
|
|
||||||
"ring 0.16.20",
|
|
||||||
"sct",
|
|
||||||
"webpki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.21.10"
|
version = "0.21.10"
|
||||||
@@ -5531,7 +5444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"rustls-webpki 0.101.7",
|
"rustls-webpki 0.101.7",
|
||||||
"sct",
|
"sct",
|
||||||
]
|
]
|
||||||
@@ -5544,7 +5457,7 @@ checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki 0.102.2",
|
"rustls-webpki 0.102.2",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -5634,8 +5547,8 @@ version = "0.101.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5644,9 +5557,9 @@ version = "0.102.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
|
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5755,8 +5668,8 @@ version = "0.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6015,12 +5928,6 @@ dependencies = [
|
|||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -6452,9 +6359,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.36.0"
|
version = "1.37.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -6513,8 +6420,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-socks"
|
name = "tokio-socks"
|
||||||
version = "0.5.1-2"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/open-trade/tokio-socks#14a5c2564fa20a2765ea53d03c573ee2b7e20421"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"futures-util",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-socks"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "git+https://github.com/rustdesk-org/tokio-socks#51037c93f8be34196fd2b6de9f674e8dfae3d01e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"either",
|
"either",
|
||||||
@@ -6527,18 +6446,6 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-socks"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
"futures-util",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -6849,12 +6756,6 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "untrusted"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -7188,16 +7089,6 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webpki"
|
|
||||||
version = "0.22.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
|
|
||||||
dependencies = [
|
|
||||||
"ring 0.17.5",
|
|
||||||
"untrusted 0.9.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.25.4"
|
version = "0.25.4"
|
||||||
@@ -7435,21 +7326,6 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.42.2",
|
|
||||||
"windows_aarch64_msvc 0.42.2",
|
|
||||||
"windows_i686_gnu 0.42.2",
|
|
||||||
"windows_i686_msvc 0.42.2",
|
|
||||||
"windows_x86_64_gnu 0.42.2",
|
|
||||||
"windows_x86_64_gnullvm 0.42.2",
|
|
||||||
"windows_x86_64_msvc 0.42.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -19,7 +19,6 @@ path = "src/naming.rs"
|
|||||||
[features]
|
[features]
|
||||||
inline = []
|
inline = []
|
||||||
cli = []
|
cli = []
|
||||||
flutter_texture_render = []
|
|
||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ RUN apt update -y && \
|
|||||||
libxcb-shape0-dev \
|
libxcb-shape0-dev \
|
||||||
libxcb-xfixes0-dev \
|
libxcb-xfixes0-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
make \
|
make \
|
||||||
cmake \
|
cmake \
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.4
|
version: 1.2.5
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.4
|
version: 1.2.5
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
4
build.py
4
build.py
@@ -111,8 +111,6 @@ def make_parser():
|
|||||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
||||||
parser.add_argument('--flutter', action='store_true',
|
parser.add_argument('--flutter', action='store_true',
|
||||||
help='Build flutter package', default=False)
|
help='Build flutter package', default=False)
|
||||||
parser.add_argument('--disable-flutter-texture-render', action='store_true',
|
|
||||||
help='Build flutter package', default=False)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--hwcodec',
|
'--hwcodec',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@@ -278,8 +276,6 @@ def get_features(args):
|
|||||||
features.append('vram')
|
features.append('vram')
|
||||||
if args.flutter:
|
if args.flutter:
|
||||||
features.append('flutter')
|
features.append('flutter')
|
||||||
if not args.disable_flutter_texture_render:
|
|
||||||
features.append('flutter_texture_render')
|
|
||||||
if args.unix_file_copy_paste:
|
if args.unix_file_copy_paste:
|
||||||
features.append('unix-file-copy-paste')
|
features.append('unix-file-copy-paste')
|
||||||
print("features:", features)
|
print("features:", features)
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import ffi.FFI
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.projection.MediaProjection
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||||
|
const val AUDIO_SAMPLE_RATE = 48000
|
||||||
|
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||||
|
|
||||||
|
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||||
|
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
||||||
|
|
||||||
|
private var audioRecorder: AudioRecord? = null
|
||||||
|
private var audioReader: AudioReader? = null
|
||||||
|
private var minBufferSize = 0
|
||||||
|
private var audioRecordStat = false
|
||||||
|
private var audioThread: Thread? = null
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = AudioRecord.Builder()
|
||||||
|
.setAudioFormat(
|
||||||
|
AudioFormat.Builder()
|
||||||
|
.setEncoding(AUDIO_ENCODING)
|
||||||
|
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||||
|
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||||
|
);
|
||||||
|
if (inVoiceCall) {
|
||||||
|
builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
|
||||||
|
} else {
|
||||||
|
mediaProjection?.let {
|
||||||
|
var apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();
|
||||||
|
builder.setAudioPlaybackCaptureConfig(apcc);
|
||||||
|
} ?: let {
|
||||||
|
Log.d(logTag, "createAudioRecorder failed, mediaProjection null")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioRecorder = builder.build()
|
||||||
|
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun checkAudioReader() {
|
||||||
|
if (audioReader != null && minBufferSize != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// read f32 to byte , length * 4
|
||||||
|
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
|
AUDIO_SAMPLE_RATE,
|
||||||
|
AUDIO_CHANNEL_MASK,
|
||||||
|
AUDIO_ENCODING
|
||||||
|
)
|
||||||
|
if (minBufferSize == 0) {
|
||||||
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioReader = AudioReader(minBufferSize, 4)
|
||||||
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun startAudioRecorder() {
|
||||||
|
checkAudioReader()
|
||||||
|
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
|
try {
|
||||||
|
FFI.setFrameRawEnable("audio", true)
|
||||||
|
audioRecorder!!.startRecording()
|
||||||
|
audioRecordStat = true
|
||||||
|
audioThread = thread {
|
||||||
|
while (audioRecordStat) {
|
||||||
|
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||||
|
FFI.onAudioFrameUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// let's release here rather than onDestroy to avoid threading issue
|
||||||
|
audioRecorder?.release()
|
||||||
|
audioRecorder = null
|
||||||
|
minBufferSize = 0
|
||||||
|
FFI.setFrameRawEnable("audio", false)
|
||||||
|
Log.d(logTag, "Exit audio thread")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isVideoStart() || isAudioStart()) {
|
||||||
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isVideoStart()) {
|
||||||
|
switchOutVoiceCall(mediaProjection)
|
||||||
|
}
|
||||||
|
tryReleaseAudio()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
audioRecorder?.let {
|
||||||
|
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
audioThread = null
|
||||||
|
|
||||||
|
if (!createAudioRecorder(true, mediaProjection)) {
|
||||||
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startAudioRecorder()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
audioRecorder?.let {
|
||||||
|
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
|
||||||
|
if (!createAudioRecorder(false, mediaProjection)) {
|
||||||
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startAudioRecorder()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryReleaseAudio() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isAudioStart() || isVideoStart()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
audioThread = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
Log.d(logTag, "destroy audio record handle")
|
||||||
|
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ package com.carriez.flutter_hbb
|
|||||||
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ffi.FFI
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -15,10 +17,20 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.media.MediaCodecInfo
|
||||||
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||||
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||||
|
import android.media.MediaCodecList
|
||||||
|
import android.media.MediaFormat
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
@@ -30,6 +42,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val logTag = "mMainActivity"
|
private val logTag = "mMainActivity"
|
||||||
private var mainService: MainService? = null
|
private var mainService: MainService? = null
|
||||||
|
|
||||||
|
private var isAudioStart = false
|
||||||
|
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
if (MainService.isReady) {
|
if (MainService.isReady) {
|
||||||
@@ -42,6 +57,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
channelTag
|
channelTag
|
||||||
)
|
)
|
||||||
initFlutterChannel(flutterMethodChannel!!)
|
initFlutterChannel(flutterMethodChannel!!)
|
||||||
|
thread { setCodecInfo() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -217,10 +233,145 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"on_voice_call_started" -> {
|
||||||
|
onVoiceCallStarted()
|
||||||
|
}
|
||||||
|
"on_voice_call_closed" -> {
|
||||||
|
onVoiceCallClosed()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
result.error("-1", "No such method", null)
|
result.error("-1", "No such method", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setCodecInfo() {
|
||||||
|
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||||
|
val codecs = codecList.codecInfos
|
||||||
|
val codecArray = JSONArray()
|
||||||
|
|
||||||
|
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val m = windowManager.maximumWindowMetrics
|
||||||
|
w = m.bounds.width()
|
||||||
|
h = m.bounds.height()
|
||||||
|
} else {
|
||||||
|
val dm = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||||
|
w = dm.widthPixels
|
||||||
|
h = dm.heightPixels
|
||||||
|
}
|
||||||
|
val align = 64
|
||||||
|
w = (w + align - 1) / align * align
|
||||||
|
h = (h + align - 1) / align * align
|
||||||
|
codecs.forEach { codec ->
|
||||||
|
val codecObject = JSONObject()
|
||||||
|
codecObject.put("name", codec.name)
|
||||||
|
codecObject.put("is_encoder", codec.isEncoder)
|
||||||
|
var hw: Boolean? = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
hw = codec.isHardwareAccelerated
|
||||||
|
} else {
|
||||||
|
// https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29
|
||||||
|
// https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229
|
||||||
|
if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) {
|
||||||
|
hw = false
|
||||||
|
} else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) {
|
||||||
|
hw = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hw != true) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
codecObject.put("hw", hw)
|
||||||
|
var mime_type = ""
|
||||||
|
codec.supportedTypes.forEach { type ->
|
||||||
|
if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01"
|
||||||
|
mime_type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mime_type.isNotEmpty()) {
|
||||||
|
codecObject.put("mime_type", mime_type)
|
||||||
|
val caps = codec.getCapabilitiesForType(mime_type)
|
||||||
|
if (codec.isEncoder) {
|
||||||
|
// Encoder‘s max_height and max_width are interchangeable
|
||||||
|
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower)
|
||||||
|
codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper)
|
||||||
|
codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower)
|
||||||
|
codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper)
|
||||||
|
val surface = caps.colorFormats.contains(COLOR_FormatSurface);
|
||||||
|
codecObject.put("surface", surface)
|
||||||
|
val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar)
|
||||||
|
codecObject.put("nv12", nv12)
|
||||||
|
if (!(nv12 || surface)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000)
|
||||||
|
codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000)
|
||||||
|
if (!codec.isEncoder) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!codec.isEncoder) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
codecArray.put(codecObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val result = JSONObject()
|
||||||
|
result.put("version", Build.VERSION.SDK_INT)
|
||||||
|
result.put("w", w)
|
||||||
|
result.put("h", h)
|
||||||
|
result.put("codecs", codecArray)
|
||||||
|
FFI.setCodecInfo(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVoiceCallStarted() {
|
||||||
|
var ok = false
|
||||||
|
mainService?.let {
|
||||||
|
ok = it.onVoiceCallStarted()
|
||||||
|
} ?: let {
|
||||||
|
isAudioStart = true
|
||||||
|
ok = audioRecordHandle.onVoiceCallStarted(null)
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
// Rarely happens, So we just add log and msgbox here.
|
||||||
|
Log.e(logTag, "onVoiceCallStarted fail")
|
||||||
|
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to start voice call."))
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "onVoiceCallStarted success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVoiceCallClosed() {
|
||||||
|
var ok = false
|
||||||
|
mainService?.let {
|
||||||
|
ok = it.onVoiceCallClosed()
|
||||||
|
} ?: let {
|
||||||
|
isAudioStart = false
|
||||||
|
ok = audioRecordHandle.onVoiceCallClosed(null)
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
// Rarely happens, So we just add log and msgbox here.
|
||||||
|
Log.e(logTag, "onVoiceCallClosed fail")
|
||||||
|
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to stop voice call."))
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "onVoiceCallClosed success")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,14 +55,11 @@ const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
|||||||
|
|
||||||
// video const
|
// video const
|
||||||
|
|
||||||
|
const val MAX_SCREEN_SIZE = 1200
|
||||||
|
|
||||||
const val VIDEO_KEY_BIT_RATE = 1024_000
|
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||||
const val VIDEO_KEY_FRAME_RATE = 30
|
const val VIDEO_KEY_FRAME_RATE = 30
|
||||||
|
|
||||||
// audio const
|
|
||||||
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
|
||||||
const val AUDIO_SAMPLE_RATE = 48000
|
|
||||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
|
||||||
|
|
||||||
class MainService : Service() {
|
class MainService : Service() {
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@@ -138,10 +135,51 @@ class MainService : Service() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"update_voice_call_state" -> {
|
||||||
|
try {
|
||||||
|
val jsonObject = JSONObject(arg1)
|
||||||
|
val id = jsonObject["id"] as Int
|
||||||
|
val username = jsonObject["name"] as String
|
||||||
|
val peerId = jsonObject["peer_id"] as String
|
||||||
|
val inVoiceCall = jsonObject["in_voice_call"] as Boolean
|
||||||
|
val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean
|
||||||
|
if (!inVoiceCall) {
|
||||||
|
if (incomingVoiceCall) {
|
||||||
|
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||||
|
} else {
|
||||||
|
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
||||||
|
Log.e(logTag, "switchOutVoiceCall fail")
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to switch out voice call."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
||||||
|
Log.e(logTag, "switchToVoiceCall fail")
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to switch to voice call."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
"stop_capture" -> {
|
"stop_capture" -> {
|
||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
stopCapture()
|
stopCapture()
|
||||||
}
|
}
|
||||||
|
"is_hardware_codec" -> {
|
||||||
|
val isHwCodec = arg1.toBoolean()
|
||||||
|
if (isHardwareCodec != isHwCodec) {
|
||||||
|
isHardwareCodec = isHwCodec
|
||||||
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,10 +199,13 @@ class MainService : Service() {
|
|||||||
companion object {
|
companion object {
|
||||||
private var _isReady = false // media permission ready status
|
private var _isReady = false // media permission ready status
|
||||||
private var _isStart = false // screen capture start status
|
private var _isStart = false // screen capture start status
|
||||||
|
private var _isAudioStart = false // audio capture start status
|
||||||
val isReady: Boolean
|
val isReady: Boolean
|
||||||
get() = _isReady
|
get() = _isReady
|
||||||
val isStart: Boolean
|
val isStart: Boolean
|
||||||
get() = _isStart
|
get() = _isStart
|
||||||
|
val isAudioStart: Boolean
|
||||||
|
get() = _isAudioStart
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logTag = "LOG_SERVICE"
|
private val logTag = "LOG_SERVICE"
|
||||||
@@ -182,10 +223,7 @@ class MainService : Service() {
|
|||||||
private var virtualDisplay: VirtualDisplay? = null
|
private var virtualDisplay: VirtualDisplay? = null
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
private var audioRecorder: AudioRecord? = null
|
private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
|
||||||
private var audioReader: AudioReader? = null
|
|
||||||
private var minBufferSize = 0
|
|
||||||
private var audioRecordStat = false
|
|
||||||
|
|
||||||
// notification
|
// notification
|
||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
@@ -217,6 +255,7 @@ class MainService : Service() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isHardwareCodec: Boolean? = null;
|
||||||
private fun updateScreenInfo(orientation: Int) {
|
private fun updateScreenInfo(orientation: Int) {
|
||||||
var w: Int
|
var w: Int
|
||||||
var h: Int
|
var h: Int
|
||||||
@@ -249,6 +288,12 @@ class MainService : Service() {
|
|||||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||||
var scale = 1
|
var scale = 1
|
||||||
if (w != 0 && h != 0) {
|
if (w != 0 && h != 0) {
|
||||||
|
if (isHardwareCodec == false && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||||
|
scale = 2
|
||||||
|
w /= scale
|
||||||
|
h /= scale
|
||||||
|
dpi /= scale
|
||||||
|
}
|
||||||
if (SCREEN_INFO.width != w) {
|
if (SCREEN_INFO.width != w) {
|
||||||
SCREEN_INFO.width = w
|
SCREEN_INFO.width = w
|
||||||
SCREEN_INFO.height = h
|
SCREEN_INFO.height = h
|
||||||
@@ -349,6 +394,14 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallStarted(): Boolean {
|
||||||
|
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallClosed(): Boolean {
|
||||||
|
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||||
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
@@ -369,12 +422,16 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
startAudioRecorder()
|
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
||||||
|
Log.d(logTag, "createAudioRecorder fail")
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "audio recorder start")
|
||||||
|
audioRecordHandle.startAudioRecorder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
FFI.setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
FFI.setFrameRawEnable("audio",true)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +439,6 @@ class MainService : Service() {
|
|||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
FFI.setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
FFI.setFrameRawEnable("audio",false)
|
|
||||||
_isStart = false
|
_isStart = false
|
||||||
// release video
|
// release video
|
||||||
if (reuseVirtualDisplay) {
|
if (reuseVirtualDisplay) {
|
||||||
@@ -411,12 +467,14 @@ class MainService : Service() {
|
|||||||
surface?.release()
|
surface?.release()
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
audioRecordStat = false
|
_isAudioStart = false
|
||||||
|
audioRecordHandle.tryReleaseAudio()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
Log.d(logTag, "destroy service")
|
Log.d(logTag, "destroy service")
|
||||||
_isReady = false
|
_isReady = false
|
||||||
|
_isAudioStart = false
|
||||||
|
|
||||||
stopCapture()
|
stopCapture()
|
||||||
|
|
||||||
@@ -514,7 +572,6 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createMediaCodec() {
|
private fun createMediaCodec() {
|
||||||
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
||||||
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
||||||
@@ -534,80 +591,6 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun startAudioRecorder() {
|
|
||||||
checkAudioRecorder()
|
|
||||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
|
||||||
try {
|
|
||||||
audioRecorder!!.startRecording()
|
|
||||||
audioRecordStat = true
|
|
||||||
thread {
|
|
||||||
while (audioRecordStat) {
|
|
||||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
|
||||||
FFI.onAudioFrameUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// let's release here rather than onDestroy to avoid threading issue
|
|
||||||
audioRecorder?.release()
|
|
||||||
audioRecorder = null
|
|
||||||
minBufferSize = 0
|
|
||||||
Log.d(logTag, "Exit audio thread")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(logTag, "startAudioRecorder fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun checkAudioRecorder() {
|
|
||||||
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// read f32 to byte , length * 4
|
|
||||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
|
||||||
AUDIO_SAMPLE_RATE,
|
|
||||||
AUDIO_CHANNEL_MASK,
|
|
||||||
AUDIO_ENCODING
|
|
||||||
)
|
|
||||||
if (minBufferSize == 0) {
|
|
||||||
Log.d(logTag, "get min buffer size fail!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
audioReader = AudioReader(minBufferSize, 4)
|
|
||||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
mediaProjection?.let {
|
|
||||||
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.RECORD_AUDIO
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
audioRecorder = AudioRecord.Builder()
|
|
||||||
.setAudioFormat(
|
|
||||||
AudioFormat.Builder()
|
|
||||||
.setEncoding(AUDIO_ENCODING)
|
|
||||||
.setSampleRate(AUDIO_SAMPLE_RATE)
|
|
||||||
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
|
||||||
)
|
|
||||||
.setAudioPlaybackCaptureConfig(apcc)
|
|
||||||
.setBufferSizeInBytes(minBufferSize).build()
|
|
||||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(logTag, "createAudioRecorder fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNotification() {
|
private fun initNotification() {
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -692,6 +675,21 @@ class MainService : Service() {
|
|||||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun voiceCallRequestNotification(
|
||||||
|
clientID: Int,
|
||||||
|
type: String,
|
||||||
|
username: String,
|
||||||
|
peerId: String
|
||||||
|
) {
|
||||||
|
val notification = notificationBuilder
|
||||||
|
.setOngoing(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setContentTitle(translate("Do you accept?"))
|
||||||
|
.setContentText("$type:$username-$peerId")
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getClientNotifyID(clientID: Int): Int {
|
private fun getClientNotifyID(clientID: Int): Int {
|
||||||
return clientID + NOTIFY_ID_OFFSET
|
return clientID + NOTIFY_ID_OFFSET
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ object FFI {
|
|||||||
external fun translateLocale(localeName: String, input: String): String
|
external fun translateLocale(localeName: String, input: String): String
|
||||||
external fun refreshScreen()
|
external fun refreshScreen()
|
||||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
|
external fun setCodecInfo(info: String)
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,13 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uni_links/uni_links.dart';
|
import 'package:uni_links/uni_links.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -550,7 +550,8 @@ class MyTheme {
|
|||||||
Get.changeThemeMode(mode);
|
Get.changeThemeMode(mode);
|
||||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||||
if (mode == ThemeMode.system) {
|
if (mode == ThemeMode.system) {
|
||||||
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
|
await bind.mainSetLocalOption(
|
||||||
|
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||||
} else {
|
} else {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||||
@@ -858,30 +859,10 @@ class OverlayDialogManager {
|
|||||||
final overlayState = _overlayKeyState.state;
|
final overlayState = _overlayKeyState.state;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
|
|
||||||
// compute overlay position
|
final overlay = makeMobileActionsOverlayEntry(
|
||||||
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
() => hideMobileActionsOverlay(),
|
||||||
final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
|
ffi: ffi,
|
||||||
const double overlayW = 200;
|
|
||||||
const double overlayH = 45;
|
|
||||||
final left = (screenW - overlayW) / 2;
|
|
||||||
final top = screenH - overlayH - 80;
|
|
||||||
|
|
||||||
final overlay = OverlayEntry(builder: (context) {
|
|
||||||
final session = ffi ?? gFFI;
|
|
||||||
return DraggableMobileActions(
|
|
||||||
position: Offset(left, top),
|
|
||||||
width: overlayW,
|
|
||||||
height: overlayH,
|
|
||||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
|
||||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
|
||||||
onRecentPressed: () async {
|
|
||||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
|
||||||
},
|
|
||||||
onHidePressed: () => hideMobileActionsOverlay(),
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
overlayState.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
_mobileActionsOverlayEntry = overlay;
|
_mobileActionsOverlayEntry = overlay;
|
||||||
mobileActionsOverlayVisible.value = true;
|
mobileActionsOverlayVisible.value = true;
|
||||||
@@ -909,6 +890,45 @@ class OverlayDialogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||||
|
final position = SimpleWrapper(Offset(0, 0));
|
||||||
|
makeMobileActions(BuildContext context, double s) {
|
||||||
|
final scale = s < 0.85 ? 0.85 : s;
|
||||||
|
final session = ffi ?? gFFI;
|
||||||
|
// compute overlay position
|
||||||
|
final screenW = MediaQuery.of(context).size.width;
|
||||||
|
final screenH = MediaQuery.of(context).size.height;
|
||||||
|
const double overlayW = 200;
|
||||||
|
const double overlayH = 45;
|
||||||
|
final left = (screenW - overlayW * scale) / 2;
|
||||||
|
final top = screenH - (overlayH + 80) * scale;
|
||||||
|
position.value = Offset(left, top);
|
||||||
|
return DraggableMobileActions(
|
||||||
|
scale: scale,
|
||||||
|
position: position,
|
||||||
|
width: overlayW,
|
||||||
|
height: overlayH,
|
||||||
|
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
||||||
|
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
||||||
|
onRecentPressed: () async {
|
||||||
|
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
||||||
|
},
|
||||||
|
onHidePressed: onHide,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OverlayEntry(builder: (context) {
|
||||||
|
if (isDesktop) {
|
||||||
|
final c = Provider.of<CanvasModel>(context);
|
||||||
|
return makeMobileActions(context, c.scale * 2.0);
|
||||||
|
} else {
|
||||||
|
return makeMobileActions(globalKey.currentContext!, 1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
||||||
final overlayState = globalKey.currentState?.overlay;
|
final overlayState = globalKey.currentState?.overlay;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
@@ -988,7 +1008,7 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||||
} else if (!tabTapped &&
|
} else if (!tabTapped &&
|
||||||
onSubmit != null &&
|
onSubmit != null &&
|
||||||
key.logicalKey == LogicalKeyboardKey.enter) {
|
(key.logicalKey == LogicalKeyboardKey.enter || key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
||||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
@@ -1407,7 +1427,7 @@ bool option2bool(String option, String value) {
|
|||||||
res = value != "N";
|
res = value != "N";
|
||||||
} else if (option.startsWith("allow-") ||
|
} else if (option.startsWith("allow-") ||
|
||||||
option == "stop-service" ||
|
option == "stop-service" ||
|
||||||
option == "direct-server" ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
option == "stop-rendezvous-service" ||
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = value == "Y";
|
res = value == "Y";
|
||||||
@@ -1421,13 +1441,13 @@ bool option2bool(String option, String value) {
|
|||||||
String bool2option(String option, bool b) {
|
String bool2option(String option, bool b) {
|
||||||
String res;
|
String res;
|
||||||
if (option.startsWith('enable-')) {
|
if (option.startsWith('enable-')) {
|
||||||
res = b ? '' : 'N';
|
res = b ? defaultOptionYes : 'N';
|
||||||
} else if (option.startsWith('allow-') ||
|
} else if (option.startsWith('allow-') ||
|
||||||
option == "stop-service" ||
|
option == "stop-service" ||
|
||||||
option == "direct-server" ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
option == "stop-rendezvous-service" ||
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = b ? 'Y' : '';
|
res = b ? 'Y' : defaultOptionNo;
|
||||||
} else {
|
} else {
|
||||||
assert(false);
|
assert(false);
|
||||||
res = b ? 'Y' : 'N';
|
res = b ? 'Y' : 'N';
|
||||||
@@ -1499,6 +1519,12 @@ Widget getPlatformImage(String platform, {double size = 50}) {
|
|||||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OffsetDevicePixelRatio {
|
||||||
|
Offset offset;
|
||||||
|
final double devicePixelRatio;
|
||||||
|
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
class LastWindowPosition {
|
class LastWindowPosition {
|
||||||
double? width;
|
double? width;
|
||||||
double? height;
|
double? height;
|
||||||
@@ -1560,17 +1586,14 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
late Offset position;
|
late Offset position;
|
||||||
late Size sz;
|
late Size sz;
|
||||||
late bool isMaximized;
|
late bool isMaximized;
|
||||||
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
|
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||||
(isMacOS && stateGlobal.closeOnFullscreen == true);
|
setPreFrame() {
|
||||||
setFrameIfMaximized() {
|
|
||||||
if (isMaximized) {
|
|
||||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||||
var lpos = LastWindowPosition.loadFromString(pos);
|
var lpos = LastWindowPosition.loadFromString(pos);
|
||||||
position = Offset(
|
position = Offset(
|
||||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WindowType.Main:
|
case WindowType.Main:
|
||||||
@@ -1581,23 +1604,30 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
||||||
isMaximized =
|
isMaximized =
|
||||||
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
||||||
|
if (isFullscreen || isMaximized) {
|
||||||
|
setPreFrame();
|
||||||
|
} else {
|
||||||
position = await windowManager.getPosition();
|
position = await windowManager.getPosition();
|
||||||
sz = await windowManager.getSize();
|
sz = await windowManager.getSize();
|
||||||
setFrameIfMaximized();
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
isMaximized = await wc.isMaximized();
|
isMaximized = await wc.isMaximized();
|
||||||
|
if (isFullscreen || isMaximized) {
|
||||||
|
setPreFrame();
|
||||||
|
} else {
|
||||||
final Rect frame;
|
final Rect frame;
|
||||||
try {
|
try {
|
||||||
frame = await wc.getFrame();
|
frame = await wc.getFrame();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to get frame of window $windowId, it may be hidden");
|
debugPrint(
|
||||||
|
"Failed to get frame of window $windowId, it may be hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
position = frame.topLeft;
|
position = frame.topLeft;
|
||||||
sz = frame.size;
|
sz = frame.size;
|
||||||
setFrameIfMaximized();
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@@ -1631,7 +1661,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
|
|||||||
final remoteList = await DesktopMultiWindow.invokeMethod(
|
final remoteList = await DesktopMultiWindow.invokeMethod(
|
||||||
windowId, kWindowEventGetRemoteList, null);
|
windowId, kWindowEventGetRemoteList, null);
|
||||||
getPeerPos(String peerId) {
|
getPeerPos(String peerId) {
|
||||||
if (isMaximized) {
|
if (isMaximized || isFullscreen) {
|
||||||
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||||
id: peerId, k: windowFramePrefix + windowType.name);
|
id: peerId, k: windowFramePrefix + windowType.name);
|
||||||
var lpos = LastWindowPosition.loadFromString(peerPos);
|
var lpos = LastWindowPosition.loadFromString(peerPos);
|
||||||
@@ -1688,8 +1718,15 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
|
|||||||
return Size(restoreWidth, restoreHeight);
|
return Size(restoreWidth, restoreHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPointInRect(Offset point, Rect rect) {
|
||||||
|
return point.dx >= rect.left &&
|
||||||
|
point.dx <= rect.right &&
|
||||||
|
point.dy >= rect.top &&
|
||||||
|
point.dy <= rect.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
/// return null means center
|
/// return null means center
|
||||||
Future<Offset?> _adjustRestoreMainWindowOffset(
|
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||||
double? left,
|
double? left,
|
||||||
double? top,
|
double? top,
|
||||||
double? width,
|
double? width,
|
||||||
@@ -1703,9 +1740,13 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
|||||||
double? frameTop;
|
double? frameTop;
|
||||||
double? frameRight;
|
double? frameRight;
|
||||||
double? frameBottom;
|
double? frameBottom;
|
||||||
|
double devicePixelRatio = 1.0;
|
||||||
|
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
for (final screen in await window_size.getScreenList()) {
|
for (final screen in await window_size.getScreenList()) {
|
||||||
|
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
|
||||||
|
devicePixelRatio = screen.scaleFactor;
|
||||||
|
}
|
||||||
frameLeft = frameLeft == null
|
frameLeft = frameLeft == null
|
||||||
? screen.visibleFrame.left
|
? screen.visibleFrame.left
|
||||||
: min(screen.visibleFrame.left, frameLeft);
|
: min(screen.visibleFrame.left, frameLeft);
|
||||||
@@ -1739,7 +1780,7 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
|||||||
top < frameTop!) {
|
top < frameTop!) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return Offset(left, top);
|
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,15 +1806,10 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||||
// Though "open in tabs" is true and the new window restore peer position, it's ok.
|
// Though "open in tabs" is true and the new window restore peer position, it's ok.
|
||||||
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
|
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
|
||||||
// If the restore position is called by main window, and the peer id is not null
|
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||||
// then we may need to get the position by reading the peer config.
|
|
||||||
// Because the session may not be read at this time.
|
|
||||||
if (desktopType == DesktopType.main) {
|
|
||||||
pos = bind.mainGetPeerFlutterOptionSync(
|
|
||||||
id: peerId, k: windowFramePrefix + type.name);
|
|
||||||
} else {
|
|
||||||
pos = await bind.sessionGetFlutterOptionByPeerId(
|
|
||||||
id: peerId, k: windowFramePrefix + type.name);
|
id: peerId, k: windowFramePrefix + type.name);
|
||||||
|
if (peerPos.isNotEmpty) {
|
||||||
|
pos = peerPos;
|
||||||
}
|
}
|
||||||
isRemotePeerPos = pos != null;
|
isRemotePeerPos = pos != null;
|
||||||
}
|
}
|
||||||
@@ -1804,22 +1840,47 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
}
|
}
|
||||||
|
|
||||||
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
||||||
final offset = await _adjustRestoreMainWindowOffset(
|
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
|
||||||
lpos.offsetWidth,
|
lpos.offsetWidth,
|
||||||
lpos.offsetHeight,
|
lpos.offsetHeight,
|
||||||
size.width,
|
size.width,
|
||||||
size.height,
|
size.height,
|
||||||
);
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
|
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WindowType.Main:
|
case WindowType.Main:
|
||||||
|
// https://github.com/rustdesk/rustdesk/issues/8038
|
||||||
|
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
|
||||||
|
// So we need to adjust the offset by the scale factor.
|
||||||
|
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
|
||||||
|
if (isWindows) {
|
||||||
|
double? curDevicePixelRatio;
|
||||||
|
Offset curPos = await windowManager.getPosition();
|
||||||
|
for (final screen in await window_size.getScreenList()) {
|
||||||
|
if (isPointInRect(curPos, screen.visibleFrame)) {
|
||||||
|
curDevicePixelRatio = screen.scaleFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (curDevicePixelRatio != null &&
|
||||||
|
curDevicePixelRatio != 0 &&
|
||||||
|
offsetDevicePixelRatio != null) {
|
||||||
|
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
|
||||||
|
final scale =
|
||||||
|
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
|
||||||
|
offsetDevicePixelRatio.offset =
|
||||||
|
offsetDevicePixelRatio.offset.scale(scale, scale);
|
||||||
|
debugPrint(
|
||||||
|
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
restorePos() async {
|
restorePos() async {
|
||||||
if (offset == null) {
|
if (offsetDevicePixelRatio == null) {
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
} else {
|
} else {
|
||||||
await windowManager.setPosition(offset);
|
await windowManager.setPosition(offsetDevicePixelRatio.offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isMaximized == true) {
|
if (lpos.isMaximized == true) {
|
||||||
@@ -1837,19 +1898,27 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
restoreFrame() async {
|
restoreFrame() async {
|
||||||
if (offset == null) {
|
if (offsetDevicePixelRatio == null) {
|
||||||
await wc.center();
|
await wc.center();
|
||||||
} else {
|
} else {
|
||||||
final frame =
|
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
|
||||||
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
offsetDevicePixelRatio.offset.dy, size.width, size.height);
|
||||||
await wc.setFrame(frame);
|
await wc.setFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isFullscreen == true) {
|
if (lpos.isFullscreen == true) {
|
||||||
|
if (!isMacOS) {
|
||||||
await restoreFrame();
|
await restoreFrame();
|
||||||
|
}
|
||||||
// An duration is needed to avoid the window being restored after fullscreen.
|
// An duration is needed to avoid the window being restored after fullscreen.
|
||||||
Future.delayed(Duration(milliseconds: 300), () async {
|
Future.delayed(Duration(milliseconds: 300), () async {
|
||||||
|
if (kWindowId == windowId) {
|
||||||
stateGlobal.setFullscreen(true);
|
stateGlobal.setFullscreen(true);
|
||||||
|
} else {
|
||||||
|
// If is not current window, we need to send a fullscreen message to `windowId`
|
||||||
|
DesktopMultiWindow.invokeMethod(
|
||||||
|
windowId, kWindowEventSetFullscreen, 'true');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (lpos.isMaximized == true) {
|
} else if (lpos.isMaximized == true) {
|
||||||
await restoreFrame();
|
await restoreFrame();
|
||||||
@@ -2315,7 +2384,13 @@ Future<void> onActiveWindowChanged() async {
|
|||||||
await windowManager.setPreventClose(false);
|
await windowManager.setPreventClose(false);
|
||||||
await windowManager.close();
|
await windowManager.close();
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
|
// If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event.
|
||||||
|
// But the app will not close.
|
||||||
|
//
|
||||||
|
// No idea why we need to delay here, `terminate()` itself is also an async function.
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
RdPlatformChannel.instance.terminate();
|
RdPlatformChannel.instance.terminate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2749,11 +2824,6 @@ sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
|
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
useTextureRender &&
|
|
||||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
|
|
||||||
|
|
||||||
Future<List<Rect>> getScreenListWayland() async {
|
Future<List<Rect>> getScreenListWayland() async {
|
||||||
final screenRectList = <Rect>[];
|
final screenRectList = <Rect>[];
|
||||||
if (isMainDesktopWindow) {
|
if (isMainDesktopWindow) {
|
||||||
@@ -2852,6 +2922,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
|||||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNewConnectWindowFrame(int windowId, String peerId, Rect? screenRect) async {
|
||||||
|
if (screenRect == null) {
|
||||||
|
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||||
|
windowId: windowId, peerId: peerId);
|
||||||
|
} else {
|
||||||
|
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
||||||
if (screenRect == null) {
|
if (screenRect == null) {
|
||||||
return;
|
return;
|
||||||
@@ -3202,7 +3281,8 @@ Widget buildPresetPasswordWarning() {
|
|||||||
translate("Security Alert"),
|
translate("Security Alert"),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontSize: 20,
|
fontSize:
|
||||||
|
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
)).paddingOnly(bottom: 8),
|
)).paddingOnly(bottom: 8),
|
||||||
@@ -3281,3 +3361,14 @@ setResizable(bool resizable) {
|
|||||||
windowManager.setResizable(resizable);
|
windowManager.setResizable(resizable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||||
|
|
||||||
|
final isCustomClient = bind.isCustomClient();
|
||||||
|
get defaultOptionLang => isCustomClient ? 'default' : '';
|
||||||
|
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||||
|
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||||
|
get defaultOptionNo => isCustomClient ? 'N' : '';
|
||||||
|
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
||||||
|
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||||
|
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ initSharedStates(String id) {
|
|||||||
ShowRemoteCursorLockState.init(id);
|
ShowRemoteCursorLockState.init(id);
|
||||||
RemoteCursorMovedState.init(id);
|
RemoteCursorMovedState.init(id);
|
||||||
FingerprintState.init(id);
|
FingerprintState.init(id);
|
||||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
PeerBoolOption.init(id, kOptionZoomCursor, () => false);
|
||||||
UnreadChatCountState.init(id);
|
UnreadChatCountState.init(id);
|
||||||
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ removeSharedStates(String id) {
|
|||||||
KeyboardEnabledState.delete(id);
|
KeyboardEnabledState.delete(id);
|
||||||
RemoteCursorMovedState.delete(id);
|
RemoteCursorMovedState.delete(id);
|
||||||
FingerprintState.delete(id);
|
FingerprintState.delete(id);
|
||||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
PeerBoolOption.delete(id, kOptionZoomCursor);
|
||||||
UnreadChatCountState.delete(id);
|
UnreadChatCountState.delete(id);
|
||||||
if (isMobile) ConnectionTypeState.delete(id);
|
if (isMobile) ConnectionTypeState.delete(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
|||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
@@ -189,23 +190,15 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (!names.contains(gFFI.abModel.currentName.value)) {
|
if (!names.contains(gFFI.abModel.currentName.value)) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
final TextEditingController textEditingController = TextEditingController();
|
// order: personal, divider, character order
|
||||||
|
// https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
|
||||||
return DropdownButton2<String>(
|
final personalAddressBookName = gFFI.abModel.personalAddressBookName();
|
||||||
value: gFFI.abModel.currentName.value,
|
bool contains = names.remove(personalAddressBookName);
|
||||||
onChanged: (value) {
|
names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
|
||||||
if (value != null) {
|
if (contains) {
|
||||||
gFFI.abModel.setCurrentName(value);
|
names.insert(0, personalAddressBookName);
|
||||||
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
|
|
||||||
}
|
}
|
||||||
},
|
final items = names
|
||||||
underline: Container(
|
|
||||||
height: 0.7,
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
buttonStyleData: ButtonStyleData(height: 48),
|
|
||||||
menuItemStyleData: MenuItemStyleData(height: 36),
|
|
||||||
items: names
|
|
||||||
.map((e) => DropdownMenuItem(
|
.map((e) => DropdownMenuItem(
|
||||||
value: e,
|
value: e,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -223,7 +216,34 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)))
|
)))
|
||||||
.toList(),
|
.toList();
|
||||||
|
var menuItemStyleData = MenuItemStyleData(height: 36);
|
||||||
|
if (contains && items.length > 1) {
|
||||||
|
items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
|
||||||
|
List<double> customHeights = List.filled(items.length, 36);
|
||||||
|
customHeights[1] = 4;
|
||||||
|
menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
|
||||||
|
}
|
||||||
|
final TextEditingController textEditingController = TextEditingController();
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
|
||||||
|
return DropdownButton2<String>(
|
||||||
|
value: gFFI.abModel.currentName.value,
|
||||||
|
onChanged: isOptFixed
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
gFFI.abModel.setCurrentName(value);
|
||||||
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
underline: Container(
|
||||||
|
height: 0.7,
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
buttonStyleData: ButtonStyleData(height: 48),
|
||||||
|
menuItemStyleData: menuItemStyleData,
|
||||||
|
items: items,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
dropdownSearchData: DropdownSearchData(
|
dropdownSearchData: DropdownSearchData(
|
||||||
searchController: textEditingController,
|
searchController: textEditingController,
|
||||||
@@ -333,6 +353,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> syncMenuItem() {
|
MenuEntryBase<String> syncMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(syncAbOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Sync with recent sessions'),
|
text: translate('Sync with recent sessions'),
|
||||||
@@ -343,11 +364,13 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
gFFI.abModel.setShouldAsync(v);
|
gFFI.abModel.setShouldAsync(v);
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> sortMenuItem() {
|
MenuEntryBase<String> sortMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(sortAbTagsOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Sort tags'),
|
text: translate('Sort tags'),
|
||||||
@@ -355,15 +378,18 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return shouldSortTags();
|
return shouldSortTags();
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
|
bind.mainSetLocalOption(
|
||||||
|
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
gFFI.abModel.sortTags.value = v;
|
gFFI.abModel.sortTags.value = v;
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> filterMenuItem() {
|
MenuEntryBase<String> filterMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(filterAbTagOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Filter by intersection'),
|
text: translate('Filter by intersection'),
|
||||||
@@ -371,10 +397,12 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return filterAbTagByIntersection();
|
return filterAbTagByIntersection();
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
|
bind.mainSetLocalOption(
|
||||||
|
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
gFFI.abModel.filterByIntersection.value = v;
|
gFFI.abModel.filterByIntersection.value = v;
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,11 +177,14 @@ void changeIdDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void changeWhiteList({Function()? callback}) async {
|
void changeWhiteList({Function()? callback}) async {
|
||||||
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(',');
|
final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
|
||||||
var newWhiteListField = newWhiteList.join('\n');
|
var newWhiteListField = curWhiteList == defaultOptionWhitelist
|
||||||
|
? ''
|
||||||
|
: curWhiteList.split(',').join('\n');
|
||||||
var controller = TextEditingController(text: newWhiteListField);
|
var controller = TextEditingController(text: newWhiteListField);
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("IP Whitelisting")),
|
title: Text(translate("IP Whitelisting")),
|
||||||
@@ -201,6 +204,7 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
enabled: !isOptFixed,
|
||||||
autofocus: true),
|
autofocus: true),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -214,12 +218,13 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("Clear", onPressed: () async {
|
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
|
||||||
await bind.mainSetOption(key: 'whitelist', value: '');
|
await bind.mainSetOption(
|
||||||
|
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
}, isOutline: true),
|
}, isOutline: true),
|
||||||
dialogButton(
|
if (!isOptFixed) dialogButton(
|
||||||
"OK",
|
"OK",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -248,7 +253,11 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
}
|
}
|
||||||
newWhiteList = ips.join(',');
|
newWhiteList = ips.join(',');
|
||||||
}
|
}
|
||||||
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
|
if (newWhiteList.trim().isEmpty) {
|
||||||
|
newWhiteList = defaultOptionWhitelist;
|
||||||
|
}
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionWhitelist, value: newWhiteList);
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
@@ -298,7 +307,7 @@ Future<String> changeDirectAccessPort(
|
|||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("OK", onPressed: () async {
|
dialogButton("OK", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'direct-access-port', value: controller.text);
|
key: kOptionDirectAccessPort, value: controller.text);
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -345,7 +354,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
|
|||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("OK", onPressed: () async {
|
dialogButton("OK", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'auto-disconnect-timeout', value: controller.text);
|
key: kOptionAutoDisconnectTimeout, value: controller.text);
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Draggable(
|
: Draggable(
|
||||||
checkKeyboard: true,
|
checkKeyboard: true,
|
||||||
position: position,
|
position: SimpleWrapper(position),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
@@ -166,15 +166,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// floating buttons of back/home/recent actions for android
|
/// floating buttons of back/home/recent actions for android
|
||||||
class DraggableMobileActions extends StatelessWidget {
|
class DraggableMobileActions extends StatelessWidget {
|
||||||
DraggableMobileActions(
|
DraggableMobileActions(
|
||||||
{this.position = Offset.zero,
|
{this.onBackPressed,
|
||||||
this.onBackPressed,
|
|
||||||
this.onRecentPressed,
|
this.onRecentPressed,
|
||||||
this.onHomePressed,
|
this.onHomePressed,
|
||||||
this.onHidePressed,
|
this.onHidePressed,
|
||||||
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height});
|
required this.height,
|
||||||
|
required this.scale});
|
||||||
|
|
||||||
final Offset position;
|
final double scale;
|
||||||
|
final SimpleWrapper<Offset> position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
@@ -186,8 +188,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Draggable(
|
return Draggable(
|
||||||
position: position,
|
position: position,
|
||||||
width: width,
|
width: scale * width,
|
||||||
height: height,
|
height: scale * height,
|
||||||
builder: (_, onPanUpdate) {
|
builder: (_, onPanUpdate) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onPanUpdate: onPanUpdate,
|
onPanUpdate: onPanUpdate,
|
||||||
@@ -197,7 +199,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.accent.withOpacity(0.4),
|
color: MyTheme.accent.withOpacity(0.4),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(15))),
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(15 * scale))),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
@@ -205,17 +208,20 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.arrow_back)),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
iconSize: 24 * scale),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onHomePressed,
|
onPressed: onHomePressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.home)),
|
icon: const Icon(Icons.home),
|
||||||
|
iconSize: 24 * scale),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onRecentPressed,
|
onPressed: onRecentPressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.more_horiz)),
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
iconSize: 24 * scale),
|
||||||
const VerticalDivider(
|
const VerticalDivider(
|
||||||
width: 0,
|
width: 0,
|
||||||
thickness: 2,
|
thickness: 2,
|
||||||
@@ -226,7 +232,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onHidePressed,
|
onPressed: onHidePressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.keyboard_arrow_down)),
|
icon: const Icon(Icons.keyboard_arrow_down),
|
||||||
|
iconSize: 24 * scale),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
@@ -235,11 +242,11 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Draggable extends StatefulWidget {
|
class Draggable extends StatefulWidget {
|
||||||
const Draggable(
|
Draggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.checkKeyboard = false,
|
this.checkKeyboard = false,
|
||||||
this.checkScreenSize = false,
|
this.checkScreenSize = false,
|
||||||
this.position = Offset.zero,
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
this.chatModel,
|
this.chatModel,
|
||||||
@@ -248,7 +255,7 @@ class Draggable extends StatefulWidget {
|
|||||||
|
|
||||||
final bool checkKeyboard;
|
final bool checkKeyboard;
|
||||||
final bool checkScreenSize;
|
final bool checkScreenSize;
|
||||||
final Offset position;
|
final SimpleWrapper<Offset> position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
@@ -259,7 +266,6 @@ class Draggable extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableState extends State<Draggable> {
|
class _DraggableState extends State<Draggable> {
|
||||||
late Offset _position;
|
|
||||||
late ChatModel? _chatModel;
|
late ChatModel? _chatModel;
|
||||||
bool _keyboardVisible = false;
|
bool _keyboardVisible = false;
|
||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
@@ -268,35 +274,36 @@ class _DraggableState extends State<Draggable> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_position = widget.position;
|
|
||||||
_chatModel = widget.chatModel;
|
_chatModel = widget.chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get position => widget.position.value;
|
||||||
|
|
||||||
void onPanUpdate(DragUpdateDetails d) {
|
void onPanUpdate(DragUpdateDetails d) {
|
||||||
final offset = d.delta;
|
final offset = d.delta;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
double x = 0;
|
double x = 0;
|
||||||
double y = 0;
|
double y = 0;
|
||||||
|
|
||||||
if (_position.dx + offset.dx + widget.width > size.width) {
|
if (position.dx + offset.dx + widget.width > size.width) {
|
||||||
x = size.width - widget.width;
|
x = size.width - widget.width;
|
||||||
} else if (_position.dx + offset.dx < 0) {
|
} else if (position.dx + offset.dx < 0) {
|
||||||
x = 0;
|
x = 0;
|
||||||
} else {
|
} else {
|
||||||
x = _position.dx + offset.dx;
|
x = position.dx + offset.dx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_position.dy + offset.dy + widget.height > size.height) {
|
if (position.dy + offset.dy + widget.height > size.height) {
|
||||||
y = size.height - widget.height;
|
y = size.height - widget.height;
|
||||||
} else if (_position.dy + offset.dy < 0) {
|
} else if (position.dy + offset.dy < 0) {
|
||||||
y = 0;
|
y = 0;
|
||||||
} else {
|
} else {
|
||||||
y = _position.dy + offset.dy;
|
y = position.dy + offset.dy;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(x, y);
|
widget.position.value = Offset(x, y);
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(_position);
|
_chatModel?.setChatWindowPosition(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkScreenSize() {}
|
checkScreenSize() {}
|
||||||
@@ -307,13 +314,13 @@ class _DraggableState extends State<Draggable> {
|
|||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = _position.dy;
|
_saveHeight = position.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, _saveHeight);
|
widget.position.value = Offset(position.dx, _saveHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,10 +328,10 @@ class _DraggableState extends State<Draggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + widget.height;
|
final sumHeight = bottomHeight + widget.height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + _position.dy > contextHeight) {
|
if (sumHeight + position.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, y);
|
widget.position.value = Offset(position.dx, y);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,8 +350,8 @@ class _DraggableState extends State<Draggable> {
|
|||||||
}
|
}
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
top: _position.dy,
|
top: position.dy,
|
||||||
left: _position.dx,
|
left: position.dx,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
child: widget.builder(context, onPanUpdate))
|
child: widget.builder(context, onPanUpdate))
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
];
|
];
|
||||||
RelativeRect? mobileTabContextMenuPos;
|
RelativeRect? mobileTabContextMenuPos;
|
||||||
|
|
||||||
|
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||||
if (uiType != '') {
|
if (uiType != '') {
|
||||||
peerCardUiType.value = int.parse(uiType) == 0
|
peerCardUiType.value = int.parse(uiType) == 0
|
||||||
? PeerUiType.grid
|
? PeerUiType.grid
|
||||||
@@ -85,7 +87,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
: PeerUiType.list;
|
: PeerUiType.list;
|
||||||
}
|
}
|
||||||
hideAbTagsPanel.value =
|
hideAbTagsPanel.value =
|
||||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,10 +175,12 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
child: Icon(model.tabIcon(t), color: color)
|
child: Icon(model.tabIcon(t), color: color)
|
||||||
.paddingSymmetric(horizontal: 4),
|
.paddingSymmetric(horizontal: 4),
|
||||||
).paddingSymmetric(horizontal: 4),
|
).paddingSymmetric(horizontal: 4),
|
||||||
onTap: () async {
|
onTap: isOptionFixed(kOptionPeerTabIndex)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
await handleTabSelection(t);
|
await handleTabSelection(t);
|
||||||
await bind.setLocalFlutterOption(
|
await bind.setLocalFlutterOption(
|
||||||
k: PeerTabModel.kPeerTabIndex, v: t.toString());
|
k: kOptionPeerTabIndex, v: t.toString());
|
||||||
},
|
},
|
||||||
onHover: (value) => hover.value = value,
|
onHover: (value) => hover.value = value,
|
||||||
),
|
),
|
||||||
@@ -265,12 +269,17 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
if (!model.isEnabled[i]) continue;
|
if (!model.isEnabled[i]) continue;
|
||||||
items.add(PopupMenuItem(
|
items.add(PopupMenuItem(
|
||||||
height: kMinInteractiveDimension * 0.8,
|
height: kMinInteractiveDimension * 0.8,
|
||||||
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
onTap: isOptVisiableFixed
|
||||||
|
? null
|
||||||
|
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||||
|
enabled: !isOptVisiableFixed,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: model.isVisibleEnabled[i],
|
value: model.isVisibleEnabled[i],
|
||||||
onChanged: (_) {
|
onChanged: isOptVisiableFixed
|
||||||
|
? null
|
||||||
|
: (_) {
|
||||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -332,8 +341,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
currentValue: model.isVisibleEnabled[tabIndex],
|
currentValue: model.isVisibleEnabled[tabIndex],
|
||||||
setter: (show) async {
|
setter: (show) async {
|
||||||
model.setTabVisible(tabIndex, show);
|
model.setTabVisible(tabIndex, show);
|
||||||
cancelFunc();
|
// Do not hide the current menu (checkbox)
|
||||||
}));
|
// cancelFunc();
|
||||||
|
},
|
||||||
|
enabled: (!isOptVisiableFixed).obs));
|
||||||
}
|
}
|
||||||
return mod_menu.PopupMenu(
|
return mod_menu.PopupMenu(
|
||||||
items: menu
|
items: menu
|
||||||
@@ -529,7 +540,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Widget _toggleTags() {
|
Widget _toggleTags() {
|
||||||
return _hoverAction(
|
return _hoverAction(
|
||||||
context: context,
|
context: context,
|
||||||
toolTip: translate('Toggle tags'),
|
toolTip: translate('Toggle Tags'),
|
||||||
hoverableWhenfalse: hideAbTagsPanel,
|
hoverableWhenfalse: hideAbTagsPanel,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.tag_rounded,
|
Icons.tag_rounded,
|
||||||
@@ -537,7 +548,8 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
|
key: kOptionHideAbTagsPanel,
|
||||||
|
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
|
||||||
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -799,14 +811,20 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
style: style),
|
style: style),
|
||||||
e,
|
e,
|
||||||
peerCardUiType.value,
|
peerCardUiType.value,
|
||||||
dense: true, (PeerUiType? v) async {
|
dense: true,
|
||||||
|
isOptionFixed(kOptionPeerCardUiType)
|
||||||
|
? null
|
||||||
|
: (PeerUiType? v) async {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
peerCardUiType.value = v;
|
peerCardUiType.value = v;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
await bind.setLocalFlutterOption(
|
await bind.setLocalFlutterOption(
|
||||||
k: "peer-card-ui-type",
|
k: kOptionPeerCardUiType,
|
||||||
v: peerCardUiType.value.index.toString(),
|
v: peerCardUiType.value.index.toString(),
|
||||||
);
|
);
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -852,7 +870,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
|||||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||||
peerSort.value = PeerSortType.remoteId;
|
peerSort.value = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -882,7 +900,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
|||||||
if (v != null) {
|
if (v != null) {
|
||||||
peerSort.value = v;
|
peerSort.value = v;
|
||||||
await bind.setLocalFlutterOption(
|
await bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import 'dart:collection';
|
|||||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../models/peer_model.dart';
|
import '../../models/peer_model.dart';
|
||||||
@@ -45,7 +45,7 @@ class LoadEvent {
|
|||||||
final peerSearchText = "".obs;
|
final peerSearchText = "".obs;
|
||||||
|
|
||||||
/// for peer sort, global obs value
|
/// for peer sort, global obs value
|
||||||
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
|
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||||
|
|
||||||
// list for listener
|
// list for listener
|
||||||
final obslist = [peerSearchText, peerSort].obs;
|
final obslist = [peerSearchText, peerSort].obs;
|
||||||
@@ -302,7 +302,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
if (!PeerSortType.values.contains(sortedBy)) {
|
if (!PeerSortType.values.contains(sortedBy)) {
|
||||||
sortedBy = PeerSortType.remoteId;
|
sortedBy = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: sortedBy,
|
v: sortedBy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
customImageQualityWidget(
|
customImageQualityWidget(
|
||||||
{required double initQuality,
|
{required double initQuality,
|
||||||
required double initFps,
|
required double initFps,
|
||||||
required Function(double) setQuality,
|
required Function(double)? setQuality,
|
||||||
required Function(double) setFps,
|
required Function(double)? setFps,
|
||||||
required bool showFps,
|
required bool showFps,
|
||||||
required bool showMoreQuality}) {
|
required bool showMoreQuality}) {
|
||||||
if (initQuality < kMinQuality ||
|
if (initQuality < kMinQuality ||
|
||||||
@@ -27,16 +26,12 @@ customImageQualityWidget(
|
|||||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
||||||
final debouncerQuality = Debouncer<double>(
|
final debouncerQuality = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: setQuality,
|
||||||
setQuality(v);
|
|
||||||
},
|
|
||||||
initialValue: qualityValue.value,
|
initialValue: qualityValue.value,
|
||||||
);
|
);
|
||||||
final debouncerFps = Debouncer<double>(
|
final debouncerFps = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: setFps,
|
||||||
setFps(v);
|
|
||||||
},
|
|
||||||
initialValue: fpsValue.value,
|
initialValue: fpsValue.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,7 +57,9 @@ customImageQualityWidget(
|
|||||||
divisions: moreQualityChecked.value
|
divisions: moreQualityChecked.value
|
||||||
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
||||||
: ((kMaxQuality - kMinQuality) / 5).round(),
|
: ((kMaxQuality - kMinQuality) / 5).round(),
|
||||||
onChanged: (double value) async {
|
onChanged: setQuality == null
|
||||||
|
? null
|
||||||
|
: (double value) async {
|
||||||
qualityValue.value = value;
|
qualityValue.value = value;
|
||||||
debouncerQuality.value = value;
|
debouncerQuality.value = value;
|
||||||
},
|
},
|
||||||
@@ -124,7 +121,9 @@ customImageQualityWidget(
|
|||||||
min: kMinFps,
|
min: kMinFps,
|
||||||
max: kMaxFps,
|
max: kMaxFps,
|
||||||
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
||||||
onChanged: (double value) async {
|
onChanged: setFps == null
|
||||||
|
? null
|
||||||
|
: (double value) async {
|
||||||
fpsValue.value = value;
|
fpsValue.value = value;
|
||||||
debouncerFps.value = value;
|
debouncerFps.value = value;
|
||||||
},
|
},
|
||||||
@@ -152,19 +151,27 @@ customImageQualitySetting() {
|
|||||||
final qualityKey = 'custom_image_quality';
|
final qualityKey = 'custom_image_quality';
|
||||||
final fpsKey = 'custom-fps';
|
final fpsKey = 'custom-fps';
|
||||||
|
|
||||||
var initQuality =
|
final initQuality =
|
||||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||||
kDefaultQuality);
|
kDefaultQuality);
|
||||||
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
final isQuanlityFixed = isOptionFixed(qualityKey);
|
||||||
|
final initFps =
|
||||||
|
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||||
kDefaultFps);
|
kDefaultFps);
|
||||||
|
final isFpsFixed = isOptionFixed(fpsKey);
|
||||||
|
|
||||||
return customImageQualityWidget(
|
return customImageQualityWidget(
|
||||||
initQuality: initQuality,
|
initQuality: initQuality,
|
||||||
initFps: initFps,
|
initFps: initFps,
|
||||||
setQuality: (v) {
|
setQuality: isQuanlityFixed
|
||||||
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
|
? null
|
||||||
|
: (v) {
|
||||||
|
bind.mainSetUserDefaultOption(
|
||||||
|
key: qualityKey, value: v.toString());
|
||||||
},
|
},
|
||||||
setFps: (v) {
|
setFps: isFpsFixed
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||||
},
|
},
|
||||||
showFps: true,
|
showFps: true,
|
||||||
@@ -208,29 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
|
|||||||
|
|
||||||
List<(String, String)> otherDefaultSettings() {
|
List<(String, String)> otherDefaultSettings() {
|
||||||
List<(String, String)> v = [
|
List<(String, String)> v = [
|
||||||
('View Mode', 'view_only'),
|
('View Mode', kOptionViewOnly),
|
||||||
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
if ((isDesktop || isWebDesktop))
|
||||||
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
|
('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||||
('Show remote cursor', 'show_remote_cursor'),
|
if ((isDesktop || isWebDesktop))
|
||||||
('Follow remote cursor', 'follow_remote_cursor'),
|
('Collapse toolbar', kOptionCollapseToolbar),
|
||||||
('Follow remote window focus', 'follow_remote_window'),
|
('Show remote cursor', kOptionShowRemoteCursor),
|
||||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
|
('Follow remote cursor', kOptionFollowRemoteCursor),
|
||||||
('Show quality monitor', 'show_quality_monitor'),
|
('Follow remote window focus', kOptionFollowRemoteWindow),
|
||||||
('Mute', 'disable_audio'),
|
if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
|
||||||
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
|
('Show quality monitor', kOptionShowQualityMonitor),
|
||||||
('Disable clipboard', 'disable_clipboard'),
|
('Mute', kOptionDisableAudio),
|
||||||
('Lock after session end', 'lock_after_session_end'),
|
if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
|
||||||
('Privacy mode', 'privacy_mode'),
|
('Disable clipboard', kOptionDisableClipboard),
|
||||||
if (isMobile) ('Touch mode', 'touch-mode'),
|
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||||
('True color (4:4:4)', 'i444'),
|
('Privacy mode', kOptionPrivacyMode),
|
||||||
|
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||||
|
('True color (4:4:4)', kOptionI444),
|
||||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||||
if (isDesktop && useTextureRender)
|
if (isDesktop && bind.mainGetUseTextureRender())
|
||||||
(
|
(
|
||||||
'Show displays as individual windows',
|
'Show displays as individual windows',
|
||||||
kKeyShowDisplaysAsIndividualWindows
|
kKeyShowDisplaysAsIndividualWindows
|
||||||
),
|
),
|
||||||
if (isDesktop && useTextureRender)
|
if (isDesktop && bind.mainGetUseTextureRender())
|
||||||
(
|
(
|
||||||
'Use all my displays for the remote session',
|
'Use all my displays for the remote session',
|
||||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
bool isEditOsPassword = false;
|
bool isEditOsPassword = false;
|
||||||
@@ -116,7 +115,10 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// paste
|
// paste
|
||||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
if (isMobile &&
|
||||||
|
pi.platform != kPeerPlatformAndroid &&
|
||||||
|
perms['keyboard'] != false &&
|
||||||
|
perms['clipboard'] != false) {
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Paste')),
|
child: Text(translate('Paste')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -327,7 +329,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
final alternativeCodecs =
|
final alternativeCodecs =
|
||||||
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
||||||
final groupValue = await bind.sessionGetOption(
|
final groupValue = await bind.sessionGetOption(
|
||||||
sessionId: sessionId, arg: 'codec-preference') ??
|
sessionId: sessionId, arg: kOptionCodecPreference) ??
|
||||||
'';
|
'';
|
||||||
final List<bool> codecs = [];
|
final List<bool> codecs = [];
|
||||||
try {
|
try {
|
||||||
@@ -349,7 +351,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
onChanged(String? value) async {
|
onChanged(String? value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionPeerOption(
|
await bind.sessionPeerOption(
|
||||||
sessionId: sessionId, name: 'codec-preference', value: value);
|
sessionId: sessionId, name: kOptionCodecPreference, value: value);
|
||||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +364,8 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var autoLabel = translate('Auto');
|
var autoLabel = translate('Auto');
|
||||||
if (groupValue == 'auto') {
|
if (groupValue == 'auto' &&
|
||||||
|
ffi.qualityMonitorModel.data.codecFormat != null) {
|
||||||
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@@ -380,7 +383,6 @@ Future<List<TToggleMenu>> toolbarCursor(
|
|||||||
List<TToggleMenu> v = [];
|
List<TToggleMenu> v = [];
|
||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final perms = ffiModel.permissions;
|
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
|
||||||
// show remote cursor
|
// show remote cursor
|
||||||
@@ -534,15 +536,15 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
perms['file'] != false &&
|
perms['file'] != false &&
|
||||||
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'enable-file-transfer';
|
final value = bind.sessionGetToggleOptionSync(
|
||||||
final value =
|
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
||||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
||||||
v.add(TToggleMenu(
|
v.add(TToggleMenu(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(translate('Enable file copy and paste'))));
|
child: Text(translate('Enable file copy and paste'))));
|
||||||
@@ -565,7 +567,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Disable clipboard'))));
|
child: Text(translate('Disable clipboard'))));
|
||||||
}
|
}
|
||||||
// lock after session end
|
// lock after session end
|
||||||
if (ffiModel.keyboard) {
|
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'lock-after-session-end';
|
final option = 'lock-after-session-end';
|
||||||
final value =
|
final value =
|
||||||
@@ -581,7 +583,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Lock after session end'))));
|
child: Text(translate('Lock after session end'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useTextureRender &&
|
if (bind.mainGetUseTextureRender() &&
|
||||||
pi.isSupportMultiDisplay &&
|
pi.isSupportMultiDisplay &&
|
||||||
PrivacyModeState.find(id).isEmpty &&
|
PrivacyModeState.find(id).isEmpty &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
@@ -600,7 +602,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||||
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
|
if (bind.mainGetUseTextureRender() &&
|
||||||
|
pi.isSupportMultiDisplay &&
|
||||||
|
isMultiScreens) {
|
||||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
|
|||||||
@@ -63,16 +63,78 @@ const String kWindowEventActiveDisplaySession = "active_display_session";
|
|||||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||||
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
||||||
|
const String kWindowEventSetFullscreen = "set_fullscreen";
|
||||||
|
|
||||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||||
|
|
||||||
|
const String kOptionViewStyle = "view_style";
|
||||||
|
const String kOptionScrollStyle = "scroll_style";
|
||||||
|
const String kOptionImageQuality = "image_quality";
|
||||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||||
|
const String kOptionTextureRender = "use-texture-render";
|
||||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||||
const String kOptionForceAlwaysRelay = "force-always-relay";
|
const String kOptionForceAlwaysRelay = "force-always-relay";
|
||||||
const String kOptionViewOnly = "view-only";
|
const String kOptionViewOnly = "view_only";
|
||||||
|
const String kOptionEnableLanDiscovery = "enable-lan-discovery";
|
||||||
|
const String kOptionWhitelist = "whitelist";
|
||||||
|
const String kOptionEnableAbr = "enable-abr";
|
||||||
|
const String kOptionEnableRecordSession = "enable-record-session";
|
||||||
|
const String kOptionDirectServer = "direct-server";
|
||||||
|
const String kOptionDirectAccessPort = "direct-access-port";
|
||||||
|
const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
||||||
|
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||||
|
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||||
|
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||||
|
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||||
|
const String kOptionAccessMode = "access-mode";
|
||||||
|
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||||
|
// "Settings -> Security -> Permissions"
|
||||||
|
const String kOptionEnableClipboard = "enable-clipboard";
|
||||||
|
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||||
|
const String kOptionEnableAudio = "enable-audio";
|
||||||
|
const String kOptionEnableTunnel = "enable-tunnel";
|
||||||
|
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||||
|
const String kOptionEnableBlockInput = "enable-block-input";
|
||||||
|
const String kOptionAllowRemoteConfigModification =
|
||||||
|
"allow-remote-config-modification";
|
||||||
|
const String kOptionVerificationMethod = "verification-method";
|
||||||
|
const String kOptionApproveMode = "approve-mode";
|
||||||
|
const String kOptionCollapseToolbar = "collapse_toolbar";
|
||||||
|
const String kOptionShowRemoteCursor = "show_remote_cursor";
|
||||||
|
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
|
||||||
|
const String kOptionFollowRemoteWindow = "follow_remote_window";
|
||||||
|
const String kOptionZoomCursor = "zoom-cursor";
|
||||||
|
const String kOptionShowQualityMonitor = "show_quality_monitor";
|
||||||
|
const String kOptionDisableAudio = "disable_audio";
|
||||||
|
const String kOptionEnableFileCopyPaste = "enable-file-copy-paste";
|
||||||
|
// "Settings -> Display -> Other default options"
|
||||||
|
const String kOptionDisableClipboard = "disable_clipboard";
|
||||||
|
const String kOptionLockAfterSessionEnd = "lock_after_session_end";
|
||||||
|
const String kOptionPrivacyMode = "privacy_mode";
|
||||||
|
const String kOptionTouchMode = "touch-mode";
|
||||||
|
const String kOptionI444 = "i444";
|
||||||
|
const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
|
||||||
|
const String kOptionCodecPreference = "codec-preference";
|
||||||
|
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
|
||||||
|
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
|
||||||
|
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
|
||||||
|
const String kOptionRemoteMenubarState = "remoteMenubarState";
|
||||||
|
const String kOptionPeerSorting = "peer-sorting";
|
||||||
|
const String kOptionPeerTabIndex = "peer-tab-index";
|
||||||
|
const String kOptionPeerTabOrder = "peer-tab-order";
|
||||||
|
const String kOptionPeerTabVisible = "peer-tab-visible";
|
||||||
|
const String kOptionPeerCardUiType = "peer-card-ui-type";
|
||||||
|
const String kOptionCurrentAbName = "current-ab-name";
|
||||||
|
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
||||||
|
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||||
|
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||||
|
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||||
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
|
|
||||||
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
|
|
||||||
const String kUrlActionClose = "close";
|
const String kUrlActionClose = "close";
|
||||||
|
|
||||||
@@ -92,10 +154,13 @@ const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
|||||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||||
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
||||||
|
|
||||||
|
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
|
||||||
|
|
||||||
// the executable name of the portable version
|
// the executable name of the portable version
|
||||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||||
|
|
||||||
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
||||||
|
const Color kColorCanvas = Colors.black;
|
||||||
|
|
||||||
const int kMobileDefaultDisplayWidth = 720;
|
const int kMobileDefaultDisplayWidth = 720;
|
||||||
const int kMobileDefaultDisplayHeight = 1280;
|
const int kMobileDefaultDisplayHeight = 1280;
|
||||||
@@ -156,7 +221,7 @@ const kFullScreenEdgeSize = 0.0;
|
|||||||
const kMaximizeEdgeSize = 0.0;
|
const kMaximizeEdgeSize = 0.0;
|
||||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||||
final kWindowBorderWidth = isLinux ? 1.0 : 0.0;
|
final kWindowBorderWidth = 1.0;
|
||||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||||
const kFrameBorderRadius = 12.0;
|
const kFrameBorderRadius = 12.0;
|
||||||
const kFrameClipRRectBorderRadius = 12.0;
|
const kFrameClipRRectBorderRadius = 12.0;
|
||||||
@@ -208,12 +273,6 @@ const kRemoteImageQualityLow = 'low';
|
|||||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||||
const kRemoteImageQualityCustom = 'custom';
|
const kRemoteImageQualityCustom = 'custom';
|
||||||
|
|
||||||
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
|
|
||||||
const kRemoteAudioGuestToHost = 'guest-to-host';
|
|
||||||
|
|
||||||
/// [kRemoteAudioDualWay] dual-way audio mode(default).
|
|
||||||
const kRemoteAudioDualWay = 'dual-way';
|
|
||||||
|
|
||||||
const kIgnoreDpi = true;
|
const kIgnoreDpi = true;
|
||||||
|
|
||||||
// ================================ mobile ================================
|
// ================================ mobile ================================
|
||||||
|
|||||||
@@ -658,10 +658,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (!bind.isCustomClient()) {
|
||||||
Timer(const Duration(seconds: 1), () async {
|
Timer(const Duration(seconds: 1), () async {
|
||||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
if (updateUrl.isNotEmpty) setState(() {});
|
if (updateUrl.isNotEmpty) setState(() {});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||||
await gFFI.serverModel.fetchID();
|
await gFFI.serverModel.fetchID();
|
||||||
final error = await bind.mainGetError();
|
final error = await bind.mainGetError();
|
||||||
|
|||||||
@@ -332,22 +332,23 @@ class _GeneralState extends State<_General> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
|
||||||
return _Card(title: 'Theme', children: [
|
return _Card(title: 'Theme', children: [
|
||||||
_Radio<String>(context,
|
_Radio<String>(context,
|
||||||
value: 'light',
|
value: 'light',
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
label: 'Light',
|
label: 'Light',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio<String>(context,
|
_Radio<String>(context,
|
||||||
value: 'dark',
|
value: 'dark',
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
label: 'Dark',
|
label: 'Dark',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio<String>(context,
|
_Radio<String>(context,
|
||||||
value: 'system',
|
value: 'system',
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
label: 'Follow System',
|
label: 'Follow System',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,9 +375,9 @@ class _GeneralState extends State<_General> {
|
|||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
if (!bind.isIncomingOnly())
|
if (!bind.isIncomingOnly())
|
||||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||||
'enable-confirm-closing-tabs',
|
kOptionEnableConfirmClosingTabs,
|
||||||
isServer: false),
|
isServer: false),
|
||||||
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
|
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||||
wallpaper(),
|
wallpaper(),
|
||||||
if (!bind.isIncomingOnly()) ...[
|
if (!bind.isIncomingOnly()) ...[
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
@@ -386,22 +387,38 @@ class _GeneralState extends State<_General> {
|
|||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
|
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
|
||||||
|
if (isLinux)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: translate('software_render_tip'),
|
message: translate('software_render_tip'),
|
||||||
child: _OptionCheckBox(context, "Always use software rendering",
|
child: _OptionCheckBox(
|
||||||
'allow-always-software-render'),
|
context,
|
||||||
|
"Always use software rendering",
|
||||||
|
kOptionAllowAlwaysSoftwareRender,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: translate('texture_render_tip'),
|
||||||
|
child: _OptionCheckBox(
|
||||||
|
context,
|
||||||
|
"Use texture rendering",
|
||||||
|
kOptionTextureRender,
|
||||||
|
optGetter: bind.mainGetUseTextureRender,
|
||||||
|
optSetter: (k, v) async =>
|
||||||
|
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!bind.isCustomClient())
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Check for software update on startup',
|
'Check for software update on startup',
|
||||||
'enable-check-update',
|
kOptionEnableCheckUpdate,
|
||||||
isServer: false,
|
isServer: false,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (bind.mainShowOption(key: 'allow-linux-headless')) {
|
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||||
children.add(_OptionCheckBox(
|
children.add(_OptionCheckBox(
|
||||||
context, 'Allow linux headless', 'allow-linux-headless'));
|
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||||
}
|
}
|
||||||
return _Card(title: 'Other', children: children);
|
return _Card(title: 'Other', children: children);
|
||||||
}
|
}
|
||||||
@@ -416,16 +433,15 @@ class _GeneralState extends State<_General> {
|
|||||||
return support;
|
return support;
|
||||||
}(), hasData: (data) {
|
}(), hasData: (data) {
|
||||||
if (data is bool && data == true) {
|
if (data is bool && data == true) {
|
||||||
final option = 'allow-remove-wallpaper';
|
bool value = mainGetBoolOptionSync(kOptionAllowRemoveWallpaper);
|
||||||
bool value = mainGetBoolOptionSync(option);
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _OptionCheckBox(
|
child: _OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Remove wallpaper during incoming sessions',
|
'Remove wallpaper during incoming sessions',
|
||||||
option,
|
kOptionAllowRemoveWallpaper,
|
||||||
update: () {
|
update: (bool v) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -455,9 +471,9 @@ class _GeneralState extends State<_General> {
|
|||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Enable hardware codec',
|
'Enable hardware codec',
|
||||||
'enable-hwcodec',
|
kOptionEnableHwcodec,
|
||||||
update: () {
|
update: (bool v) {
|
||||||
if (mainGetBoolOptionSync('enable-hwcodec')) {
|
if (v) {
|
||||||
bind.mainCheckHwcodec();
|
bind.mainCheckHwcodec();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -509,7 +525,7 @@ class _GeneralState extends State<_General> {
|
|||||||
bool user_dir_exists = map['user_dir_exists']!;
|
bool user_dir_exists = map['user_dir_exists']!;
|
||||||
return _Card(title: 'Recording', children: [
|
return _Card(title: 'Recording', children: [
|
||||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||||
'allow-auto-record-incoming'),
|
kOptionAllowAutoRecordIncoming),
|
||||||
if (showRootDir)
|
if (showRootDir)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -547,17 +563,20 @@ class _GeneralState extends State<_General> {
|
|||||||
)).marginOnly(left: 10),
|
)).marginOnly(left: 10),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
String? initialDirectory;
|
String? initialDirectory;
|
||||||
if (await Directory.fromUri(Uri.directory(user_dir))
|
if (await Directory.fromUri(Uri.directory(user_dir))
|
||||||
.exists()) {
|
.exists()) {
|
||||||
initialDirectory = user_dir;
|
initialDirectory = user_dir;
|
||||||
}
|
}
|
||||||
String? selectedDirectory = await FilePicker.platform
|
String? selectedDirectory =
|
||||||
.getDirectoryPath(initialDirectory: initialDirectory);
|
await FilePicker.platform.getDirectoryPath(
|
||||||
|
initialDirectory: initialDirectory);
|
||||||
if (selectedDirectory != null) {
|
if (selectedDirectory != null) {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'video-save-directory',
|
key: kOptionVideoSaveDirectory,
|
||||||
value: selectedDirectory);
|
value: selectedDirectory);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
@@ -580,12 +599,13 @@ class _GeneralState extends State<_General> {
|
|||||||
Map<String, String> langsMap = {for (var v in langsList) v[0]: v[1]};
|
Map<String, String> langsMap = {for (var v in langsList) v[0]: v[1]};
|
||||||
List<String> keys = langsMap.keys.toList();
|
List<String> keys = langsMap.keys.toList();
|
||||||
List<String> values = langsMap.values.toList();
|
List<String> values = langsMap.values.toList();
|
||||||
keys.insert(0, '');
|
keys.insert(0, defaultOptionLang);
|
||||||
values.insert(0, translate('Default'));
|
values.insert(0, translate('Default'));
|
||||||
String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
||||||
if (!keys.contains(currentKey)) {
|
if (!keys.contains(currentKey)) {
|
||||||
currentKey = '';
|
currentKey = defaultOptionLang;
|
||||||
}
|
}
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
||||||
return ComboBox(
|
return ComboBox(
|
||||||
keys: keys,
|
keys: keys,
|
||||||
values: values,
|
values: values,
|
||||||
@@ -595,6 +615,7 @@ class _GeneralState extends State<_General> {
|
|||||||
reloadAllWindows();
|
reloadAllWindows();
|
||||||
bind.mainChangeLanguage(lang: key);
|
bind.mainChangeLanguage(lang: key);
|
||||||
},
|
},
|
||||||
|
enabled: !isOptFixed,
|
||||||
).marginOnly(left: _kContentHMargin);
|
).marginOnly(left: _kContentHMargin);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -699,7 +720,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
bool enabled = !locked;
|
bool enabled = !locked;
|
||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
String accessMode = bind.mainGetOptionSync(key: 'access-mode');
|
String accessMode = bind.mainGetOptionSync(key: kOptionAccessMode);
|
||||||
_AccessMode mode;
|
_AccessMode mode;
|
||||||
if (accessMode == 'full') {
|
if (accessMode == 'full') {
|
||||||
mode = _AccessMode.full;
|
mode = _AccessMode.full;
|
||||||
@@ -728,7 +749,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
return _Card(title: 'Permissions', children: [
|
return _Card(title: 'Permissions', children: [
|
||||||
ComboBox(
|
ComboBox(
|
||||||
keys: [
|
keys: [
|
||||||
'',
|
defaultOptionAccessMode,
|
||||||
'full',
|
'full',
|
||||||
'view',
|
'view',
|
||||||
],
|
],
|
||||||
@@ -737,37 +758,39 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
translate('Full Access'),
|
translate('Full Access'),
|
||||||
translate('Screen Share'),
|
translate('Screen Share'),
|
||||||
],
|
],
|
||||||
enabled: enabled,
|
enabled: enabled && !isOptionFixed(kOptionAccessMode),
|
||||||
initialKey: initialKey,
|
initialKey: initialKey,
|
||||||
onChanged: (mode) async {
|
onChanged: (mode) async {
|
||||||
await bind.mainSetOption(key: 'access-mode', value: mode);
|
await bind.mainSetOption(key: kOptionAccessMode, value: mode);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}).marginOnly(left: _kContentHMargin),
|
}).marginOnly(left: _kContentHMargin),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_OptionCheckBox(context, 'Enable keyboard/mouse', 'enable-keyboard',
|
_OptionCheckBox(
|
||||||
|
context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(context, 'Enable clipboard', 'enable-clipboard',
|
_OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context, 'Enable file transfer', 'enable-file-transfer',
|
context, 'Enable file transfer', kOptionEnableFileTransfer,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(context, 'Enable audio', 'enable-audio',
|
_OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
|
||||||
_OptionCheckBox(context, 'Enable TCP tunneling', 'enable-tunnel',
|
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context, 'Enable remote restart', 'enable-remote-restart',
|
context, 'Enable TCP tunneling', kOptionEnableTunnel,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context, 'Enable recording session', 'enable-record-session',
|
context, 'Enable remote restart', kOptionEnableRemoteRestart,
|
||||||
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
|
_OptionCheckBox(
|
||||||
|
context, 'Enable recording session', kOptionEnableRecordSession,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
if (isWindows)
|
if (isWindows)
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(context, 'Enable blocking user input',
|
||||||
context, 'Enable blocking user input', 'enable-block-input',
|
kOptionEnableBlockInput,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(context, 'Enable remote configuration modification',
|
_OptionCheckBox(context, 'Enable remote configuration modification',
|
||||||
'allow-remote-config-modification',
|
kOptionAllowRemoteConfigModification,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -801,14 +824,15 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
value: value,
|
value: value,
|
||||||
groupValue: currentValue,
|
groupValue: currentValue,
|
||||||
label: value,
|
label: value,
|
||||||
onChanged: ((value) {
|
onChanged: locked
|
||||||
|
? null
|
||||||
|
: ((value) {
|
||||||
() async {
|
() async {
|
||||||
await model.setVerificationMethod(
|
await model.setVerificationMethod(
|
||||||
passwordKeys[passwordValues.indexOf(value)]);
|
passwordKeys[passwordValues.indexOf(value)]);
|
||||||
await model.updatePasswordModel();
|
await model.updatePasswordModel();
|
||||||
}();
|
}();
|
||||||
}),
|
}),
|
||||||
enabled: !locked,
|
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -842,7 +866,11 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final modeKeys = ['password', 'click', ''];
|
final modeKeys = <String>[
|
||||||
|
'password',
|
||||||
|
'click',
|
||||||
|
defaultOptionApproveMode
|
||||||
|
];
|
||||||
final modeValues = [
|
final modeValues = [
|
||||||
translate('Accept sessions via password'),
|
translate('Accept sessions via password'),
|
||||||
translate('Accept sessions via click'),
|
translate('Accept sessions via click'),
|
||||||
@@ -852,9 +880,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
|
if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
|
||||||
final usePassword = model.approveMode != 'click';
|
final usePassword = model.approveMode != 'click';
|
||||||
|
|
||||||
|
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||||
return _Card(title: 'Password', children: [
|
return _Card(title: 'Password', children: [
|
||||||
ComboBox(
|
ComboBox(
|
||||||
enabled: !locked,
|
enabled: !locked && !isApproveModeFixed,
|
||||||
keys: modeKeys,
|
keys: modeKeys,
|
||||||
values: modeValues,
|
values: modeValues,
|
||||||
initialKey: modeInitialKey,
|
initialKey: modeInitialKey,
|
||||||
@@ -927,18 +956,20 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
List<Widget> directIp(BuildContext context) {
|
List<Widget> directIp(BuildContext context) {
|
||||||
TextEditingController controller = TextEditingController();
|
TextEditingController controller = TextEditingController();
|
||||||
update() => setState(() {});
|
update(bool v) => setState(() {});
|
||||||
RxBool applyEnabled = false.obs;
|
RxBool applyEnabled = false.obs;
|
||||||
return [
|
return [
|
||||||
_OptionCheckBox(context, 'Enable direct IP access', 'direct-server',
|
_OptionCheckBox(context, 'Enable direct IP access', kOptionDirectServer,
|
||||||
update: update, enabled: !locked),
|
update: update, enabled: !locked),
|
||||||
() {
|
() {
|
||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
bool enabled = option2bool(
|
bool enabled = option2bool(kOptionDirectServer,
|
||||||
'direct-server', bind.mainGetOptionSync(key: 'direct-server'));
|
bind.mainGetOptionSync(key: kOptionDirectServer));
|
||||||
if (!enabled) applyEnabled.value = false;
|
if (!enabled) applyEnabled.value = false;
|
||||||
controller.text = bind.mainGetOptionSync(key: 'direct-access-port');
|
controller.text =
|
||||||
|
bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||||
|
final isOptFixed = isOptionFixed(kOptionDirectAccessPort);
|
||||||
return Offstage(
|
return Offstage(
|
||||||
offstage: !enabled,
|
offstage: !enabled,
|
||||||
child: _SubLabeledWidget(
|
child: _SubLabeledWidget(
|
||||||
@@ -949,7 +980,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
width: 95,
|
width: 95,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled && !locked,
|
enabled: enabled && !locked && !isOptFixed,
|
||||||
onChanged: (_) => applyEnabled.value = true,
|
onChanged: (_) => applyEnabled.value = true,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
@@ -963,11 +994,14 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
).marginOnly(right: 15),
|
).marginOnly(right: 15),
|
||||||
),
|
),
|
||||||
Obx(() => ElevatedButton(
|
Obx(() => ElevatedButton(
|
||||||
onPressed: applyEnabled.value && enabled && !locked
|
onPressed: applyEnabled.value &&
|
||||||
|
enabled &&
|
||||||
|
!locked &&
|
||||||
|
!isOptFixed
|
||||||
? () async {
|
? () async {
|
||||||
applyEnabled.value = false;
|
applyEnabled.value = false;
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'direct-access-port',
|
key: kOptionDirectAccessPort,
|
||||||
value: controller.text);
|
value: controller.text);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -976,7 +1010,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
]),
|
]),
|
||||||
enabled: enabled && !locked,
|
enabled: enabled && !locked && !isOptFixed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -990,17 +1024,19 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
bool enabled = !locked;
|
bool enabled = !locked;
|
||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
RxBool hasWhitelist =
|
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
||||||
bind.mainGetOptionSync(key: 'whitelist').isNotEmpty.obs;
|
defaultOptionWhitelist)
|
||||||
|
.obs;
|
||||||
update() async {
|
update() async {
|
||||||
hasWhitelist.value =
|
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
||||||
bind.mainGetOptionSync(key: 'whitelist').isNotEmpty;
|
defaultOptionWhitelist;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChanged(bool? checked) async {
|
onChanged(bool? checked) async {
|
||||||
changeWhiteList(callback: update);
|
changeWhiteList(callback: update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: translate('whitelist_tip'),
|
message: translate('whitelist_tip'),
|
||||||
@@ -1008,13 +1044,16 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: hasWhitelist.value,
|
value: hasWhitelist.value,
|
||||||
onChanged: enabled ? onChanged : null)
|
onChanged: enabled && !isOptFixed ? onChanged : null)
|
||||||
.marginOnly(right: 5),
|
.marginOnly(right: 5),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !hasWhitelist.value,
|
offstage: !hasWhitelist.value,
|
||||||
|
child: MouseRegion(
|
||||||
child: const Icon(Icons.warning_amber_rounded,
|
child: const Icon(Icons.warning_amber_rounded,
|
||||||
color: Color.fromARGB(255, 255, 204, 0))
|
color: Color.fromARGB(255, 255, 204, 0))
|
||||||
.marginOnly(right: 5),
|
.marginOnly(right: 5),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1025,9 +1064,11 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: enabled
|
||||||
|
? () {
|
||||||
onChanged(!hasWhitelist.value);
|
onChanged(!hasWhitelist.value);
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
).marginOnly(left: _kCheckBoxLeftMargin);
|
).marginOnly(left: _kCheckBoxLeftMargin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,18 +1117,19 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
List<Widget> autoDisconnect(BuildContext context) {
|
List<Widget> autoDisconnect(BuildContext context) {
|
||||||
TextEditingController controller = TextEditingController();
|
TextEditingController controller = TextEditingController();
|
||||||
update() => setState(() {});
|
update(bool v) => setState(() {});
|
||||||
RxBool applyEnabled = false.obs;
|
RxBool applyEnabled = false.obs;
|
||||||
final optionKey = 'allow-auto-disconnect';
|
|
||||||
final timeoutKey = 'auto-disconnect-timeout';
|
|
||||||
return [
|
return [
|
||||||
_OptionCheckBox(context, 'auto_disconnect_option_tip', optionKey,
|
_OptionCheckBox(
|
||||||
|
context, 'auto_disconnect_option_tip', kOptionAllowAutoDisconnect,
|
||||||
update: update, enabled: !locked),
|
update: update, enabled: !locked),
|
||||||
() {
|
() {
|
||||||
bool enabled =
|
bool enabled = option2bool(kOptionAllowAutoDisconnect,
|
||||||
option2bool(optionKey, bind.mainGetOptionSync(key: optionKey));
|
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
||||||
if (!enabled) applyEnabled.value = false;
|
if (!enabled) applyEnabled.value = false;
|
||||||
controller.text = bind.mainGetOptionSync(key: timeoutKey);
|
controller.text =
|
||||||
|
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
||||||
|
final isOptFixed = isOptionFixed(kOptionAutoDisconnectTimeout);
|
||||||
return Offstage(
|
return Offstage(
|
||||||
offstage: !enabled,
|
offstage: !enabled,
|
||||||
child: _SubLabeledWidget(
|
child: _SubLabeledWidget(
|
||||||
@@ -1098,7 +1140,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
width: 95,
|
width: 95,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled && !locked,
|
enabled: enabled && !locked && isOptFixed,
|
||||||
onChanged: (_) => applyEnabled.value = true,
|
onChanged: (_) => applyEnabled.value = true,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
@@ -1112,11 +1154,13 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
).marginOnly(right: 15),
|
).marginOnly(right: 15),
|
||||||
),
|
),
|
||||||
Obx(() => ElevatedButton(
|
Obx(() => ElevatedButton(
|
||||||
onPressed: applyEnabled.value && enabled && !locked
|
onPressed:
|
||||||
|
applyEnabled.value && enabled && !locked && !isOptFixed
|
||||||
? () async {
|
? () async {
|
||||||
applyEnabled.value = false;
|
applyEnabled.value = false;
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: timeoutKey, value: controller.text);
|
key: kOptionAutoDisconnectTimeout,
|
||||||
|
value: controller.text);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1124,7 +1168,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
]),
|
]),
|
||||||
enabled: enabled && !locked,
|
enabled: enabled && !locked && !isOptFixed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}(),
|
}(),
|
||||||
@@ -1273,78 +1317,80 @@ class _DisplayState extends State<_Display> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget viewStyle(BuildContext context) {
|
Widget viewStyle(BuildContext context) {
|
||||||
final key = 'view_style';
|
final isOptFixed = isOptionFixed(kOptionViewStyle);
|
||||||
onChanged(String value) async {
|
onChanged(String value) async {
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: value);
|
await bind.mainSetUserDefaultOption(key: kOptionViewStyle, value: value);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
final groupValue = bind.mainGetUserDefaultOption(key: kOptionViewStyle);
|
||||||
return _Card(title: 'Default View Style', children: [
|
return _Card(title: 'Default View Style', children: [
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteViewStyleOriginal,
|
value: kRemoteViewStyleOriginal,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Scale original',
|
label: 'Scale original',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteViewStyleAdaptive,
|
value: kRemoteViewStyleAdaptive,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Scale adaptive',
|
label: 'Scale adaptive',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget scrollStyle(BuildContext context) {
|
Widget scrollStyle(BuildContext context) {
|
||||||
final key = 'scroll_style';
|
final isOptFixed = isOptionFixed(kOptionScrollStyle);
|
||||||
onChanged(String value) async {
|
onChanged(String value) async {
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: value);
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionScrollStyle, value: value);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
|
||||||
return _Card(title: 'Default Scroll Style', children: [
|
return _Card(title: 'Default Scroll Style', children: [
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteScrollStyleAuto,
|
value: kRemoteScrollStyleAuto,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'ScrollAuto',
|
label: 'ScrollAuto',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteScrollStyleBar,
|
value: kRemoteScrollStyleBar,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Scrollbar',
|
label: 'Scrollbar',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget imageQuality(BuildContext context) {
|
Widget imageQuality(BuildContext context) {
|
||||||
final key = 'image_quality';
|
|
||||||
onChanged(String value) async {
|
onChanged(String value) async {
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: value);
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionImageQuality, value: value);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
final isOptFixed = isOptionFixed(kOptionImageQuality);
|
||||||
|
final groupValue = bind.mainGetUserDefaultOption(key: kOptionImageQuality);
|
||||||
return _Card(title: 'Default Image Quality', children: [
|
return _Card(title: 'Default Image Quality', children: [
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteImageQualityBest,
|
value: kRemoteImageQualityBest,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Good image quality',
|
label: 'Good image quality',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteImageQualityBalanced,
|
value: kRemoteImageQualityBalanced,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Balanced',
|
label: 'Balanced',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteImageQualityLow,
|
value: kRemoteImageQualityLow,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Optimize reaction time',
|
label: 'Optimize reaction time',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: kRemoteImageQualityCustom,
|
value: kRemoteImageQualityCustom,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Custom',
|
label: 'Custom',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: groupValue != kRemoteImageQualityCustom,
|
offstage: groupValue != kRemoteImageQualityCustom,
|
||||||
child: customImageQualitySetting(),
|
child: customImageQualitySetting(),
|
||||||
@@ -1353,14 +1399,16 @@ class _DisplayState extends State<_Display> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget codec(BuildContext context) {
|
Widget codec(BuildContext context) {
|
||||||
final key = 'codec-preference';
|
|
||||||
onChanged(String value) async {
|
onChanged(String value) async {
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: value);
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionCodecPreference, value: value);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
final groupValue =
|
||||||
|
bind.mainGetUserDefaultOption(key: kOptionCodecPreference);
|
||||||
var hwRadios = [];
|
var hwRadios = [];
|
||||||
|
final isOptFixed = isOptionFixed(kOptionCodecPreference);
|
||||||
try {
|
try {
|
||||||
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
|
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
|
||||||
final h264 = codecsJson['h264'] ?? false;
|
final h264 = codecsJson['h264'] ?? false;
|
||||||
@@ -1370,14 +1418,14 @@ class _DisplayState extends State<_Display> {
|
|||||||
value: 'h264',
|
value: 'h264',
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'H264',
|
label: 'H264',
|
||||||
onChanged: onChanged));
|
onChanged: isOptFixed ? null : onChanged));
|
||||||
}
|
}
|
||||||
if (h265) {
|
if (h265) {
|
||||||
hwRadios.add(_Radio(context,
|
hwRadios.add(_Radio(context,
|
||||||
value: 'h265',
|
value: 'h265',
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'H265',
|
label: 'H265',
|
||||||
onChanged: onChanged));
|
onChanged: isOptFixed ? null : onChanged));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("failed to parse supported hwdecodings, err=$e");
|
debugPrint("failed to parse supported hwdecodings, err=$e");
|
||||||
@@ -1387,22 +1435,22 @@ class _DisplayState extends State<_Display> {
|
|||||||
value: 'auto',
|
value: 'auto',
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'Auto',
|
label: 'Auto',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: 'vp8',
|
value: 'vp8',
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'VP8',
|
label: 'VP8',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: 'vp9',
|
value: 'vp9',
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'VP9',
|
label: 'VP9',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
_Radio(context,
|
_Radio(context,
|
||||||
value: 'av1',
|
value: 'av1',
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
label: 'AV1',
|
label: 'AV1',
|
||||||
onChanged: onChanged),
|
onChanged: isOptFixed ? null : onChanged),
|
||||||
...hwRadios,
|
...hwRadios,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1445,22 +1493,29 @@ class _DisplayState extends State<_Display> {
|
|||||||
|
|
||||||
Widget otherRow(String label, String key) {
|
Widget otherRow(String label, String key) {
|
||||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||||
|
final isOptFixed = isOptionFixed(key);
|
||||||
onChanged(bool b) async {
|
onChanged(bool b) async {
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: key,
|
||||||
|
value: b
|
||||||
|
? 'Y'
|
||||||
|
: (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo));
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(value: value, onChanged: (_) => onChanged(!value))
|
Checkbox(
|
||||||
|
value: value,
|
||||||
|
onChanged: isOptFixed ? null : (_) => onChanged(!value))
|
||||||
.marginOnly(right: 5),
|
.marginOnly(right: 5),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(translate(label)),
|
child: Text(translate(label)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).marginOnly(left: _kCheckBoxLeftMargin),
|
).marginOnly(left: _kCheckBoxLeftMargin),
|
||||||
onTap: () => onChanged(!value));
|
onTap: isOptFixed ? null : () => onChanged(!value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget other(BuildContext context) {
|
Widget other(BuildContext context) {
|
||||||
@@ -1763,32 +1818,41 @@ Widget _Card(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore: non_constant_identifier_names
|
// ignore: non_constant_identifier_names
|
||||||
Widget _OptionCheckBox(BuildContext context, String label, String key,
|
Widget _OptionCheckBox(
|
||||||
{Function()? update,
|
BuildContext context,
|
||||||
|
String label,
|
||||||
|
String key, {
|
||||||
|
Function(bool)? update,
|
||||||
bool reverse = false,
|
bool reverse = false,
|
||||||
bool enabled = true,
|
bool enabled = true,
|
||||||
Icon? checkedIcon,
|
Icon? checkedIcon,
|
||||||
bool? fakeValue,
|
bool? fakeValue,
|
||||||
bool isServer = true}) {
|
bool isServer = true,
|
||||||
bool value =
|
bool Function()? optGetter,
|
||||||
isServer ? mainGetBoolOptionSync(key) : mainGetLocalBoolOptionSync(key);
|
Future<void> Function(String, bool)? optSetter,
|
||||||
|
}) {
|
||||||
|
getOpt() => optGetter != null
|
||||||
|
? optGetter()
|
||||||
|
: (isServer
|
||||||
|
? mainGetBoolOptionSync(key)
|
||||||
|
: mainGetLocalBoolOptionSync(key));
|
||||||
|
bool value = getOpt();
|
||||||
|
final isOptFixed = isOptionFixed(key);
|
||||||
if (reverse) value = !value;
|
if (reverse) value = !value;
|
||||||
var ref = value.obs;
|
var ref = value.obs;
|
||||||
onChanged(option) async {
|
onChanged(option) async {
|
||||||
if (option != null) {
|
if (option != null) {
|
||||||
if (reverse) option = !option;
|
if (reverse) option = !option;
|
||||||
isServer
|
final setter =
|
||||||
? await mainSetBoolOption(key, option)
|
optSetter ?? (isServer ? mainSetBoolOption : mainSetLocalBoolOption);
|
||||||
: await mainSetLocalBoolOption(key, option);
|
await setter(key, option);
|
||||||
final readOption = isServer
|
final readOption = getOpt();
|
||||||
? mainGetBoolOptionSync(key)
|
|
||||||
: mainGetLocalBoolOptionSync(key);
|
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
ref.value = !readOption;
|
ref.value = !readOption;
|
||||||
} else {
|
} else {
|
||||||
ref.value = readOption;
|
ref.value = readOption;
|
||||||
}
|
}
|
||||||
update?.call();
|
update?.call(readOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1801,7 +1865,9 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
|
|||||||
child: Obx(
|
child: Obx(
|
||||||
() => Row(
|
() => Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(value: ref.value, onChanged: enabled ? onChanged : null)
|
Checkbox(
|
||||||
|
value: ref.value,
|
||||||
|
onChanged: enabled && !isOptFixed ? onChanged : null)
|
||||||
.marginOnly(right: 5),
|
.marginOnly(right: 5),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !ref.value || checkedIcon == null,
|
offstage: !ref.value || checkedIcon == null,
|
||||||
@@ -1815,7 +1881,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
).marginOnly(left: _kCheckBoxLeftMargin),
|
).marginOnly(left: _kCheckBoxLeftMargin),
|
||||||
onTap: enabled
|
onTap: enabled && !isOptFixed
|
||||||
? () {
|
? () {
|
||||||
onChanged(!ref.value);
|
onChanged(!ref.value);
|
||||||
}
|
}
|
||||||
@@ -1828,10 +1894,9 @@ Widget _Radio<T>(BuildContext context,
|
|||||||
{required T value,
|
{required T value,
|
||||||
required T groupValue,
|
required T groupValue,
|
||||||
required String label,
|
required String label,
|
||||||
required Function(T value) onChanged,
|
required Function(T value)? onChanged,
|
||||||
bool autoNewLine = true,
|
bool autoNewLine = true}) {
|
||||||
bool enabled = true}) {
|
final onChange2 = onChanged != null
|
||||||
var onChange = enabled
|
|
||||||
? (T? value) {
|
? (T? value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
onChanged(value);
|
onChanged(value);
|
||||||
@@ -1841,18 +1906,18 @@ Widget _Radio<T>(BuildContext context,
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Radio<T>(value: value, groupValue: groupValue, onChanged: onChange),
|
Radio<T>(value: value, groupValue: groupValue, onChanged: onChange2),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(translate(label),
|
child: Text(translate(label),
|
||||||
overflow: autoNewLine ? null : TextOverflow.ellipsis,
|
overflow: autoNewLine ? null : TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: _kContentFontSize,
|
fontSize: _kContentFontSize,
|
||||||
color: disabledTextColor(context, enabled)))
|
color: disabledTextColor(context, onChange2 != null)))
|
||||||
.marginOnly(left: 5),
|
.marginOnly(left: 5),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(left: _kRadioLeftMargin),
|
).marginOnly(left: _kRadioLeftMargin),
|
||||||
onTap: () => onChange?.call(value),
|
onTap: () => onChange2?.call(value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2128,6 +2193,11 @@ void changeSocks5Proxy() async {
|
|||||||
var pwdController = TextEditingController(text: password);
|
var pwdController = TextEditingController(text: password);
|
||||||
RxBool obscure = true.obs;
|
RxBool obscure = true.obs;
|
||||||
|
|
||||||
|
// proxy settings
|
||||||
|
// The following option is a not real key, it is just used for custom client advanced settings.
|
||||||
|
const String optionProxyUrl = "proxy-url";
|
||||||
|
final isOptFixed = isOptionFixed(optionProxyUrl);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
submit() async {
|
submit() async {
|
||||||
@@ -2205,6 +2275,7 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
controller: proxyController,
|
controller: proxyController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
enabled: !isOptFixed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2220,6 +2291,7 @@ void changeSocks5Proxy() async {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: userController,
|
controller: userController,
|
||||||
|
enabled: !isOptFixed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2242,6 +2314,7 @@ void changeSocks5Proxy() async {
|
|||||||
? Icons.visibility_off
|
? Icons.visibility_off
|
||||||
: Icons.visibility))),
|
: Icons.visibility))),
|
||||||
controller: pwdController,
|
controller: pwdController,
|
||||||
|
enabled: !isOptFixed,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2254,7 +2327,7 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||||
dialogButton('OK', onPressed: submit),
|
if (!isOptFixed) dialogButton('OK', onPressed: submit),
|
||||||
],
|
],
|
||||||
onSubmit: submit,
|
onSubmit: submit,
|
||||||
onCancel: close,
|
onCancel: close,
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
tabController.clear();
|
tabController.clear();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import '../../common.dart';
|
|||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
import '../../common/widgets/toolbar.dart';
|
import '../../common/widgets/toolbar.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/desktop_render_texture.dart';
|
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
@@ -94,7 +93,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
void _initStates(String id) {
|
void _initStates(String id) {
|
||||||
initSharedStates(id);
|
initSharedStates(id);
|
||||||
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||||
@@ -136,7 +135,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'zoom-cursor');
|
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
// if (!_isCustomCursorInited) {
|
// if (!_isCustomCursorInited) {
|
||||||
// customCursorController.registerNeedUpdateCursorCallback(
|
// customCursorController.registerNeedUpdateCursorCallback(
|
||||||
@@ -209,6 +208,22 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowEnterFullScreen() {
|
||||||
|
super.onWindowEnterFullScreen();
|
||||||
|
if (isMacOS) {
|
||||||
|
stateGlobal.setFullscreen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowLeaveFullScreen() {
|
||||||
|
super.onWindowLeaveFullScreen();
|
||||||
|
if (isMacOS) {
|
||||||
|
stateGlobal.setFullscreen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
||||||
@@ -221,6 +236,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
DesktopMultiWindow.removeListener(this);
|
DesktopMultiWindow.removeListener(this);
|
||||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||||
|
_ffi.imageModel.disposeImage();
|
||||||
|
_ffi.cursorModel.disposeImages();
|
||||||
_ffi.recordingModel.onClose();
|
_ffi.recordingModel.onClose();
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
await _ffi.close(closeSession: closeSession);
|
await _ffi.close(closeSession: closeSession);
|
||||||
@@ -261,7 +278,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black,
|
color: kColorCanvas,
|
||||||
child: RawKeyFocusScope(
|
child: RawKeyFocusScope(
|
||||||
focusNode: _rawKeyFocusNode,
|
focusNode: _rawKeyFocusNode,
|
||||||
onFocusChange: (bool imageFocused) {
|
onFocusChange: (bool imageFocused) {
|
||||||
@@ -290,8 +307,30 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||||
? emptyOverlay()
|
? emptyOverlay()
|
||||||
: () {
|
: () {
|
||||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
|
} else {
|
||||||
|
if (_ffi.connType == ConnType.defaultConn &&
|
||||||
|
_ffi.ffiModel.permissions['keyboard'] != false) {
|
||||||
|
Timer(
|
||||||
|
Duration(milliseconds: 10),
|
||||||
|
() => _ffi.dialogManager
|
||||||
|
.mobileActionsOverlayVisible.value = true);
|
||||||
|
}
|
||||||
|
return Obx(() => Offstage(
|
||||||
|
offstage: _ffi.dialogManager
|
||||||
|
.mobileActionsOverlayVisible.isFalse,
|
||||||
|
child: Overlay(initialEntries: [
|
||||||
|
makeMobileActionsOverlayEntry(
|
||||||
|
() => _ffi
|
||||||
|
.dialogManager
|
||||||
|
.mobileActionsOverlayVisible
|
||||||
|
.value = false,
|
||||||
|
ffi: _ffi,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
}(),
|
}(),
|
||||||
// Use Overlay to enable rebuild every time on menu button click.
|
// Use Overlay to enable rebuild every time on menu button click.
|
||||||
_ffi.ffiModel.pi.isSet.isTrue
|
_ffi.ffiModel.pi.isSet.isTrue
|
||||||
@@ -438,9 +477,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}, onExit: (evt) {
|
}, onExit: (evt) {
|
||||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||||
Future.delayed(Duration.zero, () {
|
final c = Provider.of<CanvasModel>(context, listen: false);
|
||||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
||||||
});
|
|
||||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||||
return Obx(
|
return Obx(
|
||||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||||
@@ -575,12 +613,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
onHover: (evt) {},
|
onHover: (evt) {},
|
||||||
child: child);
|
child: child);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||||
final paintWidth = c.getDisplayWidth() * s;
|
final paintWidth = c.getDisplayWidth() * s;
|
||||||
final paintHeight = c.getDisplayHeight() * s;
|
final paintHeight = c.getDisplayHeight() * s;
|
||||||
final paintSize = Size(paintWidth, paintHeight);
|
final paintSize = Size(paintWidth, paintHeight);
|
||||||
final paintWidget = useTextureRender
|
final paintWidget = m.useTextureRender
|
||||||
? _BuildPaintTextureRender(
|
? _BuildPaintTextureRender(
|
||||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||||
@@ -601,7 +638,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
if (c.size.width > 0 && c.size.height > 0) {
|
if (c.size.width > 0 && c.size.height > 0) {
|
||||||
final paintWidget = useTextureRender
|
final paintWidget = m.useTextureRender
|
||||||
? _BuildPaintTextureRender(
|
? _BuildPaintTextureRender(
|
||||||
c,
|
c,
|
||||||
s,
|
s,
|
||||||
|
|||||||
@@ -350,7 +350,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
void onRemoveId(String id) async {
|
void onRemoveId(String id) async {
|
||||||
if (tabController.state.value.tabs.isEmpty) {
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
|
||||||
// Keep calling until the window status is hidden.
|
// Keep calling until the window status is hidden.
|
||||||
//
|
//
|
||||||
// Workaround for Windows:
|
// Workaround for Windows:
|
||||||
@@ -384,9 +383,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
tabController.clear();
|
tabController.clear();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
@@ -416,19 +415,19 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final display = args['display'];
|
final display = args['display'];
|
||||||
final displays = args['displays'];
|
final displays = args['displays'];
|
||||||
final screenRect = parseParamScreenRect(args);
|
final screenRect = parseParamScreenRect(args);
|
||||||
windowOnTop(windowId());
|
Future.delayed(Duration.zero, () async {
|
||||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
if (stateGlobal.fullscreen.isTrue) {
|
||||||
if (tabController.length == 0) {
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||||
// Show the hidden window.
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
|
|
||||||
stateGlobal.setFullscreen(true);
|
|
||||||
}
|
|
||||||
// Reset the state
|
|
||||||
stateGlobal.closeOnFullscreen = null;
|
|
||||||
}
|
}
|
||||||
|
await setNewConnectWindowFrame(windowId(), id!, screenRect);
|
||||||
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||||
|
await windowOnTop(windowId());
|
||||||
|
});
|
||||||
|
});
|
||||||
ConnectionTypeState.init(id);
|
ConnectionTypeState.init(id);
|
||||||
_toolbarState.setShow(
|
_toolbarState.setShow(
|
||||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: id,
|
key: id,
|
||||||
label: id,
|
label: id,
|
||||||
@@ -522,6 +521,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
returnValue = jsonEncode(coords.toJson());
|
returnValue = jsonEncode(coords.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (call.method == kWindowEventSetFullscreen) {
|
||||||
|
stateGlobal.setFullscreen(call.arguments == 'true');
|
||||||
}
|
}
|
||||||
_update_remote_count();
|
_update_remote_count();
|
||||||
return returnValue;
|
return returnValue;
|
||||||
|
|||||||
@@ -289,9 +289,9 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
windowManager.close();
|
windowManager.close();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
|
|||||||
@@ -341,9 +341,10 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
@protected
|
@protected
|
||||||
void handleTap() {
|
void handleTap() {
|
||||||
widget.onTap?.call();
|
widget.onTap?.call();
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
Navigator.pop<T>(context, widget.value);
|
Navigator.pop<T>(context, widget.value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -445,9 +445,18 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
dismissCallback: dismissCallback,
|
dismissCallback: dismissCallback,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bool get isEnabled => enabled?.value ?? true;
|
||||||
|
|
||||||
RxBool get curOption;
|
RxBool get curOption;
|
||||||
Future<void> setOption(bool? option);
|
Future<void> setOption(bool? option);
|
||||||
|
|
||||||
|
tryPop(BuildContext context) {
|
||||||
|
if (dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
super.dismissCallback?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<mod_menu.PopupMenuEntry<T>> build(
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
BuildContext context, MenuConfig conf) {
|
BuildContext context, MenuConfig conf) {
|
||||||
@@ -481,44 +490,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
if (switchType == SwitchType.sswitch) {
|
if (switchType == SwitchType.sswitch) {
|
||||||
return Switch(
|
return Switch(
|
||||||
value: curOption.value,
|
value: curOption.value,
|
||||||
onChanged: (v) {
|
onChanged: isEnabled
|
||||||
if (super.dismissOnClicked &&
|
? (v) {
|
||||||
Navigator.canPop(context)) {
|
tryPop(context);
|
||||||
Navigator.pop(context);
|
|
||||||
if (super.dismissCallback != null) {
|
|
||||||
super.dismissCallback!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(v);
|
setOption(v);
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Checkbox(
|
return Checkbox(
|
||||||
value: curOption.value,
|
value: curOption.value,
|
||||||
onChanged: (v) {
|
onChanged: isEnabled
|
||||||
if (super.dismissOnClicked &&
|
? (v) {
|
||||||
Navigator.canPop(context)) {
|
tryPop(context);
|
||||||
Navigator.pop(context);
|
|
||||||
if (super.dismissCallback != null) {
|
|
||||||
super.dismissCallback!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(v);
|
setOption(v);
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
])),
|
])),
|
||||||
onPressed: () {
|
onPressed: isEnabled
|
||||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
? () {
|
||||||
Navigator.pop(context);
|
tryPop(context);
|
||||||
if (super.dismissCallback != null) {
|
|
||||||
super.dismissCallback!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(!curOption.value);
|
setOption(!curOption.value);
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
|||||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||||
@@ -27,12 +26,11 @@ import './popup_menu.dart';
|
|||||||
import './kb_layout_type_chooser.dart';
|
import './kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
class ToolbarState {
|
class ToolbarState {
|
||||||
final kStoreKey = 'remoteMenubarState';
|
|
||||||
late RxBool show;
|
late RxBool show;
|
||||||
late RxBool _pin;
|
late RxBool _pin;
|
||||||
|
|
||||||
ToolbarState() {
|
ToolbarState() {
|
||||||
final s = bind.getLocalFlutterOption(k: kStoreKey);
|
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||||
if (s.isEmpty) {
|
if (s.isEmpty) {
|
||||||
_initSet(false, false);
|
_initSet(false, false);
|
||||||
return;
|
return;
|
||||||
@@ -53,8 +51,8 @@ class ToolbarState {
|
|||||||
|
|
||||||
_initSet(bool s, bool p) {
|
_initSet(bool s, bool p) {
|
||||||
// Show remubar when connection is established.
|
// Show remubar when connection is established.
|
||||||
show =
|
show = RxBool(
|
||||||
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
||||||
_pin = RxBool(p);
|
_pin = RxBool(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +84,7 @@ class ToolbarState {
|
|||||||
|
|
||||||
_savePin() async {
|
_savePin() async {
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
|
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||||
}
|
}
|
||||||
|
|
||||||
save() async {
|
save() async {
|
||||||
@@ -590,7 +588,7 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
assetName: 'assets/actions_mobile.svg',
|
assetName: 'assets/actions_mobile.svg',
|
||||||
tooltip: 'Mobile Actions',
|
tooltip: 'Mobile Actions',
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
|
ffi.dialogManager.mobileActionsOverlayVisible.toggle(),
|
||||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||||
? _ToolbarTheme.blueColor
|
? _ToolbarTheme.blueColor
|
||||||
: _ToolbarTheme.inactiveColor,
|
: _ToolbarTheme.inactiveColor,
|
||||||
@@ -616,14 +614,14 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
||||||
|
|
||||||
bool get supportIndividualWindows =>
|
bool get supportIndividualWindows =>
|
||||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => showMonitorsToolbar
|
Widget build(BuildContext context) => showMonitorsToolbar
|
||||||
? buildMultiMonitorMenu()
|
? buildMultiMonitorMenu(context)
|
||||||
: Obx(() => buildMonitorMenu());
|
: Obx(() => buildMonitorMenu(context));
|
||||||
|
|
||||||
Widget buildMonitorMenu() {
|
Widget buildMonitorMenu(BuildContext context) {
|
||||||
final width = SimpleWrapper<double>(0);
|
final width = SimpleWrapper<double>(0);
|
||||||
final monitorsIcon =
|
final monitorsIcon =
|
||||||
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
||||||
@@ -637,20 +635,23 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
menuStyle: MenuStyle(
|
menuStyle: MenuStyle(
|
||||||
padding:
|
padding:
|
||||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
|
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMultiMonitorMenu() {
|
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||||
return Row(children: buildMonitorList(true));
|
return Row(children: buildMonitorList(context, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMonitorSubmenuWidget() {
|
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||||
|
final m = Provider.of<ImageModel>(context);
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(children: buildMonitorList(false)),
|
Row(children: buildMonitorList(context, false)),
|
||||||
supportIndividualWindows ? Divider() : Offstage(),
|
supportIndividualWindows && m.useTextureRender ? Divider() : Offstage(),
|
||||||
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
supportIndividualWindows && m.useTextureRender
|
||||||
|
? chooseDisplayBehavior()
|
||||||
|
: Offstage(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -681,7 +682,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> buildMonitorList(bool isMulti) {
|
List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
|
||||||
final List<Widget> monitorList = [];
|
final List<Widget> monitorList = [];
|
||||||
final pi = ffi.ffiModel.pi;
|
final pi = ffi.ffiModel.pi;
|
||||||
|
|
||||||
@@ -736,7 +737,10 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
for (int i = 0; i < pi.displays.length; i++) {
|
for (int i = 0; i < pi.displays.length; i++) {
|
||||||
monitorList.add(buildMonitorButton(i));
|
monitorList.add(buildMonitorButton(i));
|
||||||
}
|
}
|
||||||
if (supportIndividualWindows && pi.displays.length > 1) {
|
final m = Provider.of<ImageModel>(context);
|
||||||
|
if (supportIndividualWindows &&
|
||||||
|
m.useTextureRender &&
|
||||||
|
pi.displays.length > 1) {
|
||||||
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
||||||
}
|
}
|
||||||
return monitorList;
|
return monitorList;
|
||||||
@@ -819,7 +823,12 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
RxInt display = CurrentDisplayState.find(id);
|
RxInt display = CurrentDisplayState.find(id);
|
||||||
if (display.value != i) {
|
if (display.value != i) {
|
||||||
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
|
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||||
|
bind.mainGetUseTextureRender() &&
|
||||||
|
bind.sessionGetDisplaysAsIndividualWindows(
|
||||||
|
sessionId: ffi.sessionId) ==
|
||||||
|
'Y';
|
||||||
|
if (isChooseDisplayToOpenInNewWindow) {
|
||||||
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
||||||
} else {
|
} else {
|
||||||
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
||||||
@@ -1068,7 +1077,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
id: widget.id,
|
id: widget.id,
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
),
|
),
|
||||||
Divider(),
|
|
||||||
cursorToggles(),
|
cursorToggles(),
|
||||||
Divider(),
|
Divider(),
|
||||||
toggles(),
|
toggles(),
|
||||||
@@ -1220,14 +1228,16 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
final v = data as List<TToggleMenu>;
|
final v = data as List<TToggleMenu>;
|
||||||
if (v.isEmpty) return Offstage();
|
if (v.isEmpty) return Offstage();
|
||||||
return Column(
|
return Column(children: [
|
||||||
children: v
|
Divider(),
|
||||||
|
...v
|
||||||
.map((e) => CkbMenuButton(
|
.map((e) => CkbMenuButton(
|
||||||
value: e.value,
|
value: e.value,
|
||||||
onChanged: e.onChanged,
|
onChanged: e.onChanged,
|
||||||
child: e.child,
|
child: e.child,
|
||||||
ffi: ffi))
|
ffi: ffi))
|
||||||
.toList());
|
.toList(),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1875,7 +1885,7 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
? (value) async {
|
? (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
sessionId: ffi.sessionId, value: kOptionViewOnly);
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
ffiModel.setViewOnly(id, value);
|
ffiModel.setViewOnly(id, value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2019,6 +2029,7 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecordMenu extends StatelessWidget {
|
class _RecordMenu extends StatelessWidget {
|
||||||
const _RecordMenu({Key? key}) : super(key: key);
|
const _RecordMenu({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -2372,18 +2383,18 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
final confLeft = double.tryParse(
|
final confLeft = double.tryParse(
|
||||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-left'));
|
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
|
||||||
if (confLeft == null) {
|
if (confLeft == null) {
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: 'remote-menubar-drag-left', value: left.toString());
|
key: kOptionRemoteMenubarDragLeft, value: left.toString());
|
||||||
} else {
|
} else {
|
||||||
left = confLeft;
|
left = confLeft;
|
||||||
}
|
}
|
||||||
final confRight = double.tryParse(
|
final confRight = double.tryParse(
|
||||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-right'));
|
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
|
||||||
if (confRight == null) {
|
if (confRight == null) {
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: 'remote-menubar-drag-right', value: right.toString());
|
key: kOptionRemoteMenubarDragRight, value: right.toString());
|
||||||
} else {
|
} else {
|
||||||
right = confRight;
|
right = confRight;
|
||||||
}
|
}
|
||||||
@@ -2454,7 +2465,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
Obx(() => Offstage(
|
if (!isMacOS) Obx(() => Offstage(
|
||||||
offstage: isFullscreen.isFalse,
|
offstage: isFullscreen.isFalse,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => widget.setMinimize(),
|
onPressed: () => widget.setMinimize(),
|
||||||
|
|||||||
@@ -323,18 +323,18 @@ class DesktopTab extends StatelessWidget {
|
|||||||
return buildRemoteBlock(
|
return buildRemoteBlock(
|
||||||
child: child,
|
child: child,
|
||||||
use: () async {
|
use: () async {
|
||||||
var access_mode = await bind.mainGetOption(key: 'access-mode');
|
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||||
var option = option2bool(
|
var option = option2bool(
|
||||||
'allow-remote-config-modification',
|
kOptionAllowRemoteConfigModification,
|
||||||
await bind.mainGetOption(
|
await bind.mainGetOption(
|
||||||
key: 'allow-remote-config-modification'));
|
key: kOptionAllowRemoteConfigModification));
|
||||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
List<Widget> _tabWidgets = [];
|
||||||
Widget _buildPageView() {
|
Widget _buildPageView() {
|
||||||
return _buildBlock(
|
final child = _buildBlock(
|
||||||
child: Obx(() => PageView(
|
child: Obx(() => PageView(
|
||||||
controller: state.value.pageController,
|
controller: state.value.pageController,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
@@ -358,6 +358,11 @@ class DesktopTab extends StatelessWidget {
|
|||||||
return newList;
|
return newList;
|
||||||
}
|
}
|
||||||
}())));
|
}())));
|
||||||
|
if (tabType == DesktopTabType.remoteScreen) {
|
||||||
|
return Container(color: kColorCanvas, child: child);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether to show ListView
|
/// Check whether to show ListView
|
||||||
@@ -391,6 +396,16 @@ class DesktopTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
onPanStart: (_) => startDragging(isMainWindow),
|
onPanStart: (_) => startDragging(isMainWindow),
|
||||||
|
onPanCancel: () {
|
||||||
|
if (isMacOS) {
|
||||||
|
setMovable(isMainWindow, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
if (isMacOS) {
|
||||||
|
setMovable(isMainWindow, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -641,14 +656,12 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
}
|
}
|
||||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||||
if (isMacOS && await windowManager.isFullScreen()) {
|
if (isMacOS && await windowManager.isFullScreen()) {
|
||||||
stateGlobal.closeOnFullscreen ??= true;
|
|
||||||
await windowManager.setFullScreen(false);
|
await windowManager.setFullScreen(false);
|
||||||
await macOSWindowClose(
|
await macOSWindowClose(
|
||||||
() async => await windowManager.isFullScreen(),
|
() async => await windowManager.isFullScreen(),
|
||||||
mainWindowClose,
|
mainWindowClose,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.closeOnFullscreen ??= false;
|
|
||||||
await mainWindowClose();
|
await mainWindowClose();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -660,7 +673,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
|
|
||||||
if (await widget.onClose?.call() ?? true) {
|
if (await widget.onClose?.call() ?? true) {
|
||||||
if (await controller.isFullScreen()) {
|
if (await controller.isFullScreen()) {
|
||||||
stateGlobal.closeOnFullscreen ??= true;
|
|
||||||
await controller.setFullscreen(false);
|
await controller.setFullscreen(false);
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
await macOSWindowClose(
|
await macOSWindowClose(
|
||||||
@@ -668,7 +680,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
() async => await notMainWindowClose(controller),
|
() async => await notMainWindowClose(controller),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.closeOnFullscreen ??= false;
|
|
||||||
await notMainWindowClose(controller);
|
await notMainWindowClose(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,6 +796,14 @@ void startDragging(bool isMainWindow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setMovable(bool isMainWindow, bool movable) {
|
||||||
|
if (isMainWindow) {
|
||||||
|
windowManager.setMovable(movable);
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(kWindowId!).setMovable(movable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// return true -> window will be maximize
|
/// return true -> window will be maximize
|
||||||
/// return false -> window will be unmaximize
|
/// return false -> window will be unmaximize
|
||||||
Future<bool> toggleMaximize(bool isMainWindow) async {
|
Future<bool> toggleMaximize(bool isMainWindow) async {
|
||||||
@@ -812,9 +831,9 @@ Future<bool> closeConfirmDialog() async {
|
|||||||
var confirm = true;
|
var confirm = true;
|
||||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||||
submit() {
|
submit() {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
String value = bool2option(kOptionEnableConfirmClosingTabs, confirm);
|
||||||
String value = bool2option(opt, confirm);
|
bind.mainSetLocalOption(
|
||||||
bind.mainSetLocalOption(key: opt, value: value);
|
key: kOptionEnableConfirmClosingTabs, value: value);
|
||||||
close(true);
|
close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +939,7 @@ class _ListView extends StatelessWidget {
|
|||||||
final label = labelGetter == null
|
final label = labelGetter == null
|
||||||
? Rx<String>(tab.label)
|
? Rx<String>(tab.label)
|
||||||
: labelGetter!(tab.label);
|
: labelGetter!(tab.label);
|
||||||
return VisibilityDetector(
|
final child = VisibilityDetector(
|
||||||
key: ValueKey(tab.key),
|
key: ValueKey(tab.key),
|
||||||
onVisibilityChanged: onVisibilityChanged,
|
onVisibilityChanged: onVisibilityChanged,
|
||||||
child: _Tab(
|
child: _Tab(
|
||||||
@@ -953,6 +972,10 @@ class _ListView extends StatelessWidget {
|
|||||||
selectedBorderColor: selectedBorderColor,
|
selectedBorderColor: selectedBorderColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return GestureDetector(
|
||||||
|
onPanStart: (e) {},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}).toList()));
|
}).toList()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ Future<void> main(List<String> args) async {
|
|||||||
desktopType = DesktopType.main;
|
desktopType = DesktopType.main;
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
windowManager.setPreventClose(true);
|
windowManager.setPreventClose(true);
|
||||||
|
windowManager.setMovable(false);
|
||||||
runMainApp(true);
|
runMainApp(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +168,7 @@ void runMultiWindow(
|
|||||||
final title = getWindowName();
|
final title = getWindowName();
|
||||||
// set prevent close to true, we handle close event manually
|
// set prevent close to true, we handle close event manually
|
||||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||||
|
WindowController.fromWindowId(kWindowId!).setMovable(false);
|
||||||
late Widget widget;
|
late Widget widget;
|
||||||
switch (appType) {
|
switch (appType) {
|
||||||
case kAppTypeDesktopRemote:
|
case kAppTypeDesktopRemote:
|
||||||
@@ -338,7 +340,6 @@ void runInstallPage() async {
|
|||||||
windowManager.focus();
|
windowManager.focus();
|
||||||
windowManager.setOpacity(1);
|
windowManager.setOpacity(1);
|
||||||
windowManager.setAlignment(Alignment.center); // ensure
|
windowManager.setAlignment(Alignment.center); // ensure
|
||||||
windowManager.setTitle(getWindowName());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,11 +65,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
|
if (!bind.isCustomClient()) {
|
||||||
Timer(const Duration(seconds: 1), () async {
|
Timer(const Duration(seconds: 1), () async {
|
||||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
if (_updateUrl.isNotEmpty) setState(() {});
|
if (_updateUrl.isNotEmpty) setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_idController.addListener(() {
|
_idController.addListener(() {
|
||||||
_idEmpty.value = _idController.text.isEmpty;
|
_idEmpty.value = _idController.text.isEmpty;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -79,7 +80,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
initSharedStates(widget.id);
|
initSharedStates(widget.id);
|
||||||
gFFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
|
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +90,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay();
|
gFFI.dialogManager.hideMobileActionsOverlay();
|
||||||
gFFI.inputModel.listenToMouse(false);
|
gFFI.inputModel.listenToMouse(false);
|
||||||
|
gFFI.imageModel.disposeImage();
|
||||||
|
gFFI.cursorModel.disposeImages();
|
||||||
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
_mobileFocusNode.dispose();
|
_mobileFocusNode.dispose();
|
||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
@@ -102,6 +105,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
await keyboardSubscription.cancel();
|
await keyboardSubscription.cancel();
|
||||||
removeSharedStates(widget.id);
|
removeSharedStates(widget.id);
|
||||||
|
if (isAndroid) {
|
||||||
|
// Only one client is considered here for now.
|
||||||
|
// TODO: take into account the case where there are multiple clients
|
||||||
|
gFFI.invokeMethod("on_voice_call_closed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
@@ -304,7 +312,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black,
|
color: kColorCanvas,
|
||||||
child: isWebDesktop
|
child: isWebDesktop
|
||||||
? getBodyForDesktopWithListener(keyboard)
|
? getBodyForDesktopWithListener(keyboard)
|
||||||
: SafeArea(
|
: SafeArea(
|
||||||
@@ -369,9 +377,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
clientClose(sessionId, gFFI.dialogManager);
|
clientClose(sessionId, gFFI.dialogManager);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
] +
|
|
||||||
<Widget>[
|
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.tv),
|
icon: Icon(Icons.tv),
|
||||||
@@ -415,12 +421,14 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
: <Widget>[
|
: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.message),
|
icon: isAndroid
|
||||||
onPressed: () {
|
? SvgPicture.asset('assets/chat.svg',
|
||||||
gFFI.chatModel.changeCurrentKey(MessageKey(
|
colorFilter: ColorFilter.mode(
|
||||||
widget.id, ChatModel.clientModeID));
|
Colors.white, BlendMode.srcIn))
|
||||||
gFFI.chatModel.toggleChatOverlay();
|
: Icon(Icons.message),
|
||||||
},
|
onPressed: () => isAndroid
|
||||||
|
? showChatOptions(widget.id)
|
||||||
|
: onPressedTextChat(widget.id),
|
||||||
)
|
)
|
||||||
]) +
|
]) +
|
||||||
[
|
[
|
||||||
@@ -538,6 +546,88 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressedTextChat(String id) {
|
||||||
|
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
|
||||||
|
gFFI.chatModel.toggleChatOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
showChatOptions(String id) async {
|
||||||
|
onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
|
||||||
|
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
|
||||||
|
|
||||||
|
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
|
||||||
|
{TextStyle? labelStyle}) =>
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate(label), style: labelStyle),
|
||||||
|
trailingIcon: Transform.scale(
|
||||||
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
final isInVoice = [
|
||||||
|
VoiceCallStatus.waitingForResponse,
|
||||||
|
VoiceCallStatus.connected
|
||||||
|
].contains(gFFI.chatModel.voiceCallStatus.value);
|
||||||
|
final menus = [
|
||||||
|
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
|
||||||
|
() => onPressedTextChat(widget.id)),
|
||||||
|
isInVoice
|
||||||
|
? makeTextMenu(
|
||||||
|
'End voice call',
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/call_wait.svg',
|
||||||
|
colorFilter:
|
||||||
|
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
|
||||||
|
),
|
||||||
|
onPressEndVoiceCall,
|
||||||
|
labelStyle: TextStyle(color: Colors.redAccent))
|
||||||
|
: makeTextMenu(
|
||||||
|
'Voice call',
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/call_wait.svg',
|
||||||
|
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
|
||||||
|
),
|
||||||
|
onPressVoiceCall),
|
||||||
|
];
|
||||||
|
getChild(TTextMenu menu) {
|
||||||
|
if (menu.trailingIcon != null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
menu.child,
|
||||||
|
menu.trailingIcon!,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return menu.child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final menuItems = menus
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
||||||
|
.toList();
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
final x = 120.0;
|
||||||
|
final y = size.height;
|
||||||
|
var index = await showMenu(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||||
|
items: menuItems,
|
||||||
|
elevation: 8,
|
||||||
|
);
|
||||||
|
if (index != null && index < menus.length) {
|
||||||
|
menus[index].onPressed.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// aka changeTouchMode
|
/// aka changeTouchMode
|
||||||
BottomAppBar getGestureHelp() {
|
BottomAppBar getGestureHelp() {
|
||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
@@ -550,7 +640,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.ffiModel.toggleTouchMode();
|
gFFI.ffiModel.toggleTouchMode();
|
||||||
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||||
bind.sessionPeerOption(
|
bind.sessionPeerOption(
|
||||||
sessionId: sessionId, name: "touch-mode", value: v);
|
sessionId: sessionId, name: kOptionTouchMode, value: v);
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
final approveMode = gFFI.serverModel.approveMode;
|
final approveMode = gFFI.serverModel.approveMode;
|
||||||
final verificationMethod = gFFI.serverModel.verificationMethod;
|
final verificationMethod = gFFI.serverModel.verificationMethod;
|
||||||
final showPasswordOption = approveMode != 'click';
|
final showPasswordOption = approveMode != 'click';
|
||||||
|
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: gFFI.serverModel.connectStatus > 0,
|
enabled: gFFI.serverModel.connectStatus > 0,
|
||||||
@@ -50,16 +51,19 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
value: 'AcceptSessionsViaPassword',
|
value: 'AcceptSessionsViaPassword',
|
||||||
child: listTile(
|
child: listTile(
|
||||||
'Accept sessions via password', approveMode == 'password'),
|
'Accept sessions via password', approveMode == 'password'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'AcceptSessionsViaClick',
|
value: 'AcceptSessionsViaClick',
|
||||||
child:
|
child:
|
||||||
listTile('Accept sessions via click', approveMode == 'click'),
|
listTile('Accept sessions via click', approveMode == 'click'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: "AcceptSessionsViaBoth",
|
value: "AcceptSessionsViaBoth",
|
||||||
child: listTile("Accept sessions via both",
|
child: listTile("Accept sessions via both",
|
||||||
approveMode != 'password' && approveMode != 'click'),
|
approveMode != 'password' && approveMode != 'click'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
if (showPasswordOption) const PopupMenuDivider(),
|
if (showPasswordOption) const PopupMenuDivider(),
|
||||||
if (showPasswordOption &&
|
if (showPasswordOption &&
|
||||||
@@ -107,7 +111,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
} else if (value == kUsePermanentPassword ||
|
} else if (value == kUsePermanentPassword ||
|
||||||
value == kUseTemporaryPassword ||
|
value == kUseTemporaryPassword ||
|
||||||
value == kUseBothPasswords) {
|
value == kUseBothPasswords) {
|
||||||
bind.mainSetOption(key: "verification-method", value: value);
|
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||||
gFFI.serverModel.updatePasswordModel();
|
gFFI.serverModel.updatePasswordModel();
|
||||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||||
value = value.substring("AcceptSessionsVia".length);
|
value = value.substring("AcceptSessionsVia".length);
|
||||||
@@ -116,7 +120,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
} else if (value == "Click") {
|
} else if (value == "Click") {
|
||||||
gFFI.serverModel.setApproveMode('click');
|
gFFI.serverModel.setApproveMode('click');
|
||||||
} else {
|
} else {
|
||||||
gFFI.serverModel.setApproveMode('');
|
gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -637,39 +641,93 @@ class ConnectionManager extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
).marginOnly(bottom: 5),
|
).marginOnly(bottom: 5),
|
||||||
client.authorized
|
client.authorized
|
||||||
? Container(
|
? _buildDisconnectButton(client)
|
||||||
alignment: Alignment.centerRight,
|
: _buildNewConnectionHint(serverModel, client),
|
||||||
child: ElevatedButton.icon(
|
if (client.incomingVoiceCall && !client.inVoiceCall)
|
||||||
style: ButtonStyle(
|
..._buildNewVoiceCallHint(context, serverModel, client),
|
||||||
backgroundColor:
|
])))
|
||||||
MaterialStatePropertyAll(Colors.red)),
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDisconnectButton(Client client) {
|
||||||
|
final disconnectButton = ElevatedButton.icon(
|
||||||
|
style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
bind.cmCloseConnection(connId: client.id);
|
bind.cmCloseConnection(connId: client.id);
|
||||||
gFFI.invokeMethod(
|
gFFI.invokeMethod("cancel_notification", client.id);
|
||||||
"cancel_notification", client.id);
|
|
||||||
},
|
},
|
||||||
label: Text(translate("Disconnect"))))
|
label: Text(translate("Disconnect")),
|
||||||
: Row(
|
);
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
final buttons = [disconnectButton];
|
||||||
children: [
|
if (client.inVoiceCall) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
ElevatedButton.icon(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||||
|
icon: const Icon(Icons.phone),
|
||||||
|
label: Text(translate("Stop")),
|
||||||
|
onPressed: () {
|
||||||
|
bind.cmCloseVoiceCall(id: client.id);
|
||||||
|
gFFI.invokeMethod("cancel_notification", client.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons.length == 1) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: disconnectButton,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
children: buttons,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNewConnectionHint(ServerModel serverModel, Client client) {
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(translate("Dismiss")),
|
child: Text(translate("Dismiss")),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
serverModel.sendLoginResponse(
|
serverModel.sendLoginResponse(client, false);
|
||||||
client, false);
|
|
||||||
}).marginOnly(right: 15),
|
}).marginOnly(right: 15),
|
||||||
if (serverModel.approveMode != 'password')
|
if (serverModel.approveMode != 'password')
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
label: Text(translate("Accept")),
|
label: Text(translate("Accept")),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
serverModel.sendLoginResponse(
|
serverModel.sendLoginResponse(client, true);
|
||||||
client, true);
|
|
||||||
}),
|
}),
|
||||||
]),
|
]);
|
||||||
])))
|
}
|
||||||
.toList());
|
|
||||||
|
List<Widget> _buildNewVoiceCallHint(
|
||||||
|
BuildContext context, ServerModel serverModel, Client client) {
|
||||||
|
return [
|
||||||
|
Text(
|
||||||
|
translate("android_new_voice_call_tip"),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
).marginOnly(bottom: 5),
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(translate("Dismiss")),
|
||||||
|
onPressed: () {
|
||||||
|
serverModel.handleVoiceCall(client, false);
|
||||||
|
}).marginOnly(right: 15),
|
||||||
|
if (serverModel.approveMode != 'password')
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: Text(translate("Accept")),
|
||||||
|
onPressed: () {
|
||||||
|
serverModel.handleVoiceCall(client, true);
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,6 +845,15 @@ void androidChannelInit() {
|
|||||||
gFFI.serverModel.stopService();
|
gFFI.serverModel.stopService();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "msgbox":
|
||||||
|
{
|
||||||
|
var type = arguments["type"] as String;
|
||||||
|
var title = arguments["title"] as String;
|
||||||
|
var text = arguments["text"] as String;
|
||||||
|
var link = (arguments["link"] ?? '') as String;
|
||||||
|
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: "MethodCallHandler err:$e");
|
debugPrintStack(label: "MethodCallHandler err:$e");
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _onlyWhiteList = false;
|
var _onlyWhiteList = false;
|
||||||
var _enableDirectIPAccess = false;
|
var _enableDirectIPAccess = false;
|
||||||
var _enableRecordSession = false;
|
var _enableRecordSession = false;
|
||||||
|
var _enableHardwareCodec = false;
|
||||||
var _autoRecordIncomingSession = false;
|
var _autoRecordIncomingSession = false;
|
||||||
var _allowAutoDisconnect = false;
|
var _allowAutoDisconnect = false;
|
||||||
var _localIP = "";
|
var _localIP = "";
|
||||||
@@ -86,7 +87,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final enableAbrRes = option2bool(
|
final enableAbrRes = option2bool(
|
||||||
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
|
kOptionEnableAbr, await bind.mainGetOption(key: kOptionEnableAbr));
|
||||||
if (enableAbrRes != _enableAbr) {
|
if (enableAbrRes != _enableAbr) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableAbr = enableAbrRes;
|
_enableAbr = enableAbrRes;
|
||||||
@@ -99,30 +100,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_denyLANDiscovery = denyLanDiscovery;
|
_denyLANDiscovery = denyLanDiscovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
final onlyWhiteList =
|
final onlyWhiteList = (await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
defaultOptionWhitelist;
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
update = true;
|
update = true;
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableDirectIPAccess = option2bool(
|
final enableDirectIPAccess = option2bool(kOptionDirectServer,
|
||||||
'direct-server', await bind.mainGetOption(key: 'direct-server'));
|
await bind.mainGetOption(key: kOptionDirectServer));
|
||||||
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableDirectIPAccess = enableDirectIPAccess;
|
_enableDirectIPAccess = enableDirectIPAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableRecordSession = option2bool('enable-record-session',
|
final enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||||
await bind.mainGetOption(key: 'enable-record-session'));
|
await bind.mainGetOption(key: kOptionEnableRecordSession));
|
||||||
if (enableRecordSession != _enableRecordSession) {
|
if (enableRecordSession != _enableRecordSession) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableRecordSession = enableRecordSession;
|
_enableRecordSession = enableRecordSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||||
|
await bind.mainGetOption(key: kOptionEnableHwcodec));
|
||||||
|
if (_enableHardwareCodec != enableHardwareCodec) {
|
||||||
|
update = true;
|
||||||
|
_enableHardwareCodec = enableHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
final autoRecordIncomingSession = option2bool(
|
final autoRecordIncomingSession = option2bool(
|
||||||
'allow-auto-record-incoming',
|
kOptionAllowAutoRecordIncoming,
|
||||||
await bind.mainGetOption(key: 'allow-auto-record-incoming'));
|
await bind.mainGetOption(key: kOptionAllowAutoRecordIncoming));
|
||||||
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
||||||
update = true;
|
update = true;
|
||||||
_autoRecordIncomingSession = autoRecordIncomingSession;
|
_autoRecordIncomingSession = autoRecordIncomingSession;
|
||||||
@@ -135,7 +143,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final directAccessPort =
|
final directAccessPort =
|
||||||
await bind.mainGetOption(key: 'direct-access-port');
|
await bind.mainGetOption(key: kOptionDirectAccessPort);
|
||||||
if (directAccessPort != _directAccessPort) {
|
if (directAccessPort != _directAccessPort) {
|
||||||
update = true;
|
update = true;
|
||||||
_directAccessPort = directAccessPort;
|
_directAccessPort = directAccessPort;
|
||||||
@@ -153,15 +161,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_buildDate = buildDate;
|
_buildDate = buildDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
|
final allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
await bind.mainGetOption(key: 'allow-auto-disconnect'));
|
await bind.mainGetOption(key: kOptionAllowAutoDisconnect));
|
||||||
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
||||||
update = true;
|
update = true;
|
||||||
_allowAutoDisconnect = allowAutoDisconnect;
|
_allowAutoDisconnect = allowAutoDisconnect;
|
||||||
}
|
}
|
||||||
|
|
||||||
final autoDisconnectTimeout =
|
final autoDisconnectTimeout =
|
||||||
await bind.mainGetOption(key: 'auto-disconnect-timeout');
|
await bind.mainGetOption(key: kOptionAutoDisconnectTimeout);
|
||||||
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
||||||
update = true;
|
update = true;
|
||||||
_autoDisconnectTimeout = autoDisconnectTimeout;
|
_autoDisconnectTimeout = autoDisconnectTimeout;
|
||||||
@@ -249,12 +257,14 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Deny LAN discovery')),
|
title: Text(translate('Deny LAN discovery')),
|
||||||
initialValue: _denyLANDiscovery,
|
initialValue: _denyLANDiscovery,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableLanDiscovery)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: "enable-lan-discovery",
|
key: kOptionEnableLanDiscovery,
|
||||||
value: bool2option("enable-lan-discovery", !v));
|
value: bool2option(kOptionEnableLanDiscovery, !v));
|
||||||
final newValue = !option2bool('enable-lan-discovery',
|
final newValue = !option2bool(kOptionEnableLanDiscovery,
|
||||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
await bind.mainGetOption(key: kOptionEnableLanDiscovery));
|
||||||
setState(() {
|
setState(() {
|
||||||
_denyLANDiscovery = newValue;
|
_denyLANDiscovery = newValue;
|
||||||
});
|
});
|
||||||
@@ -273,7 +283,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: (_) async {
|
onToggle: (_) async {
|
||||||
update() async {
|
update() async {
|
||||||
final onlyWhiteList =
|
final onlyWhiteList =
|
||||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
(await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||||
|
defaultOptionWhitelist;
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
@@ -287,9 +298,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
||||||
initialValue: _enableAbr,
|
initialValue: _enableAbr,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||||
await bind.mainSetOption(key: "enable-abr", value: v ? "" : "N");
|
? null
|
||||||
final newValue = await bind.mainGetOption(key: "enable-abr") != "N";
|
: (v) async {
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionEnableAbr, value: v ? defaultOptionYes : "N");
|
||||||
|
final newValue =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableAbr) != "N";
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableAbr = newValue;
|
_enableAbr = newValue;
|
||||||
});
|
});
|
||||||
@@ -298,11 +313,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Enable recording session')),
|
title: Text(translate('Enable recording session')),
|
||||||
initialValue: _enableRecordSession,
|
initialValue: _enableRecordSession,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: "enable-record-session", value: v ? "" : "N");
|
key: kOptionEnableRecordSession,
|
||||||
|
value: v ? defaultOptionYes : "N");
|
||||||
final newValue =
|
final newValue =
|
||||||
await bind.mainGetOption(key: "enable-record-session") != "N";
|
await bind.mainGetOption(key: kOptionEnableRecordSession) !=
|
||||||
|
"N";
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableRecordSession = newValue;
|
_enableRecordSession = newValue;
|
||||||
});
|
});
|
||||||
@@ -333,7 +352,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionDirectAccessPort)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
final port = await changeDirectAccessPort(
|
final port = await changeDirectAccessPort(
|
||||||
_localIP, _directAccessPort);
|
_localIP, _directAccessPort);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -342,10 +363,14 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}))
|
}))
|
||||||
]),
|
]),
|
||||||
initialValue: _enableDirectIPAccess,
|
initialValue: _enableDirectIPAccess,
|
||||||
onToggle: (_) async {
|
onToggle: isOptionFixed(kOptionDirectServer)
|
||||||
|
? null
|
||||||
|
: (_) async {
|
||||||
_enableDirectIPAccess = !_enableDirectIPAccess;
|
_enableDirectIPAccess = !_enableDirectIPAccess;
|
||||||
String value = bool2option('direct-server', _enableDirectIPAccess);
|
String value =
|
||||||
await bind.mainSetOption(key: 'direct-server', value: value);
|
bool2option(kOptionDirectServer, _enableDirectIPAccess);
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionDirectServer, value: value);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -374,7 +399,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
final timeout = await changeAutoDisconnectTimeout(
|
final timeout = await changeAutoDisconnectTimeout(
|
||||||
_autoDisconnectTimeout);
|
_autoDisconnectTimeout);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -383,11 +410,14 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}))
|
}))
|
||||||
]),
|
]),
|
||||||
initialValue: _allowAutoDisconnect,
|
initialValue: _allowAutoDisconnect,
|
||||||
onToggle: (_) async {
|
onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
|
||||||
|
? null
|
||||||
|
: (_) async {
|
||||||
_allowAutoDisconnect = !_allowAutoDisconnect;
|
_allowAutoDisconnect = !_allowAutoDisconnect;
|
||||||
String value =
|
String value = bool2option(
|
||||||
bool2option('allow-auto-disconnect', _allowAutoDisconnect);
|
kOptionAllowAutoDisconnect, _allowAutoDisconnect);
|
||||||
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value);
|
await bind.mainSetOption(
|
||||||
|
key: kOptionAllowAutoDisconnect, value: value);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -513,6 +543,26 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
|
if (isAndroid)
|
||||||
|
SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Text(translate('Enable hardware codec')),
|
||||||
|
initialValue: _enableHardwareCodec,
|
||||||
|
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionEnableHwcodec,
|
||||||
|
value: v ? defaultOptionYes : "N");
|
||||||
|
final newValue =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableHwcodec) !=
|
||||||
|
"N";
|
||||||
|
setState(() {
|
||||||
|
_enableHardwareCodec = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
if (isAndroid && !outgoingOnly)
|
if (isAndroid && !outgoingOnly)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Recording")),
|
title: Text(translate("Recording")),
|
||||||
@@ -527,14 +577,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
child: Text("${translate("Directory")}: ${data.data}")),
|
child: Text("${translate("Directory")}: ${data.data}")),
|
||||||
future: bind.mainVideoSaveDirectory(root: false)),
|
future: bind.mainVideoSaveDirectory(root: false)),
|
||||||
initialValue: _autoRecordIncomingSession,
|
initialValue: _autoRecordIncomingSession,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: "allow-auto-record-incoming",
|
key: kOptionAllowAutoRecordIncoming,
|
||||||
value: bool2option("allow-auto-record-incoming", v));
|
value:
|
||||||
|
bool2option(kOptionAllowAutoRecordIncoming, v));
|
||||||
final newValue = option2bool(
|
final newValue = option2bool(
|
||||||
'allow-auto-record-incoming',
|
kOptionAllowAutoRecordIncoming,
|
||||||
await bind.mainGetOption(
|
await bind.mainGetOption(
|
||||||
key: 'allow-auto-record-incoming'));
|
key: kOptionAllowAutoRecordIncoming));
|
||||||
setState(() {
|
setState(() {
|
||||||
_autoRecordIncomingSession = newValue;
|
_autoRecordIncomingSession = newValue;
|
||||||
});
|
});
|
||||||
@@ -637,29 +690,32 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
|
|||||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||||
try {
|
try {
|
||||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||||
var lang = bind.mainGetLocalOption(key: "lang");
|
var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
setLang(v) async {
|
setLang(v) async {
|
||||||
if (lang != v) {
|
if (lang != v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
lang = v;
|
lang = v;
|
||||||
});
|
});
|
||||||
await bind.mainSetLocalOption(key: "lang", value: v);
|
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
|
||||||
HomePage.homeKey.currentState?.refreshPages();
|
HomePage.homeKey.currentState?.refreshPages();
|
||||||
Future.delayed(Duration(milliseconds: 200), close);
|
Future.delayed(Duration(milliseconds: 200), close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
getRadio(Text(translate('Default')), '', lang, setLang),
|
getRadio(Text(translate('Default')), defaultOptionLang, lang,
|
||||||
|
isOptFixed ? null : setLang),
|
||||||
Divider(color: MyTheme.border),
|
Divider(color: MyTheme.border),
|
||||||
] +
|
] +
|
||||||
langs.map((e) {
|
langs.map((e) {
|
||||||
final key = e[0] as String;
|
final key = e[0] as String;
|
||||||
final name = e[1] as String;
|
final name = e[1] as String;
|
||||||
return getRadio(Text(translate(name)), key, lang, setLang);
|
return getRadio(Text(translate(name)), key, lang,
|
||||||
|
isOptFixed ? null : setLang);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -683,13 +739,15 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
getRadio(
|
getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
|
||||||
Text(translate('Light')), ThemeMode.light, themeMode, setTheme),
|
isOptFixed ? null : setTheme),
|
||||||
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme),
|
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
|
||||||
|
isOptFixed ? null : setTheme),
|
||||||
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
||||||
setTheme)
|
isOptFixed ? null : setTheme)
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
@@ -777,10 +835,13 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
||||||
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
||||||
],
|
],
|
||||||
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
|
getter: () =>
|
||||||
asyncSetter: (value) async {
|
bind.mainGetUserDefaultOption(key: kOptionViewStyle),
|
||||||
|
asyncSetter: isOptionFixed(kOptionViewStyle)
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
await bind.mainSetUserDefaultOption(
|
await bind.mainSetUserDefaultOption(
|
||||||
key: 'view_style', value: value);
|
key: kOptionViewStyle, value: value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_getPopupDialogRadioEntry(
|
_getPopupDialogRadioEntry(
|
||||||
@@ -792,13 +853,16 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
||||||
],
|
],
|
||||||
getter: () {
|
getter: () {
|
||||||
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
|
final v =
|
||||||
|
bind.mainGetUserDefaultOption(key: kOptionImageQuality);
|
||||||
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
||||||
return v;
|
return v;
|
||||||
},
|
},
|
||||||
asyncSetter: (value) async {
|
asyncSetter: isOptionFixed(kOptionImageQuality)
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
await bind.mainSetUserDefaultOption(
|
await bind.mainSetUserDefaultOption(
|
||||||
key: 'image_quality', value: value);
|
key: kOptionImageQuality, value: value);
|
||||||
showCustomImageQuality.value =
|
showCustomImageQuality.value =
|
||||||
value == kRemoteImageQualityCustom;
|
value == kRemoteImageQualityCustom;
|
||||||
},
|
},
|
||||||
@@ -810,10 +874,12 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
title: 'Default Codec',
|
title: 'Default Codec',
|
||||||
list: codecList,
|
list: codecList,
|
||||||
getter: () =>
|
getter: () =>
|
||||||
bind.mainGetUserDefaultOption(key: 'codec-preference'),
|
bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
|
||||||
asyncSetter: (value) async {
|
asyncSetter: isOptionFixed(kOptionCodecPreference)
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
await bind.mainSetUserDefaultOption(
|
await bind.mainSetUserDefaultOption(
|
||||||
key: 'codec-preference', value: value);
|
key: kOptionCodecPreference, value: value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -829,11 +895,15 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
|
|
||||||
SettingsTile otherRow(String label, String key) {
|
SettingsTile otherRow(String label, String key) {
|
||||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||||
|
final isOptFixed = isOptionFixed(key);
|
||||||
return SettingsTile.switchTile(
|
return SettingsTile.switchTile(
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
title: Text(translate(label)),
|
title: Text(translate(label)),
|
||||||
onToggle: (b) async {
|
onToggle: isOptFixed
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
|
? null
|
||||||
|
: (b) async {
|
||||||
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: key, value: b ? 'Y' : defaultOptionNo);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -853,7 +923,7 @@ _getPopupDialogRadioEntry({
|
|||||||
required String title,
|
required String title,
|
||||||
required List<_RadioEntry> list,
|
required List<_RadioEntry> list,
|
||||||
required _RadioEntryGetter getter,
|
required _RadioEntryGetter getter,
|
||||||
required _RadioEntrySetter asyncSetter,
|
required _RadioEntrySetter? asyncSetter,
|
||||||
Widget? tail,
|
Widget? tail,
|
||||||
RxBool? showTail,
|
RxBool? showTail,
|
||||||
String? notCloseValue,
|
String? notCloseValue,
|
||||||
@@ -873,21 +943,23 @@ _getPopupDialogRadioEntry({
|
|||||||
|
|
||||||
void showDialog() async {
|
void showDialog() async {
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
onChanged(String? value) async {
|
final onChanged = asyncSetter == null
|
||||||
|
? null
|
||||||
|
: (String? value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await asyncSetter(value);
|
await asyncSetter(value);
|
||||||
init();
|
init();
|
||||||
if (value != notCloseValue) {
|
if (value != notCloseValue) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Obx(
|
content: Obx(
|
||||||
() => Column(children: [
|
() => Column(children: [
|
||||||
...list
|
...list
|
||||||
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
||||||
groupValue.value, (String? value) => onChanged(value)))
|
groupValue.value, onChanged))
|
||||||
.toList(),
|
.toList(),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage:
|
offstage:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
@@ -509,7 +510,8 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setShouldAsync(bool v) async {
|
void setShouldAsync(bool v) async {
|
||||||
await bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
|
await bind.mainSetLocalOption(
|
||||||
|
key: syncAbOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
_syncAllFromRecent = true;
|
_syncAllFromRecent = true;
|
||||||
_timerCounter = 0;
|
_timerCounter = 0;
|
||||||
}
|
}
|
||||||
@@ -548,7 +550,7 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trySetCurrentToLast() {
|
trySetCurrentToLast() {
|
||||||
final name = bind.getLocalFlutterOption(k: 'current-ab-name');
|
final name = bind.getLocalFlutterOption(k: kOptionCurrentAbName);
|
||||||
if (addressbooks.containsKey(name)) {
|
if (addressbooks.containsKey(name)) {
|
||||||
_currentName.value = name;
|
_currentName.value = name;
|
||||||
}
|
}
|
||||||
@@ -647,6 +649,10 @@ class AbModel {
|
|||||||
return addressbooks.keys.toList();
|
return addressbooks.keys.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String personalAddressBookName() {
|
||||||
|
return _personalAddressBookName;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setCurrentName(String name) async {
|
Future<void> setCurrentName(String name) async {
|
||||||
final oldName = _currentName.value;
|
final oldName = _currentName.value;
|
||||||
if (addressbooks.containsKey(name)) {
|
if (addressbooks.containsKey(name)) {
|
||||||
|
|||||||
@@ -527,10 +527,16 @@ class ChatModel with ChangeNotifier {
|
|||||||
|
|
||||||
void onVoiceCallStarted() {
|
void onVoiceCallStarted() {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.connected;
|
_voiceCallStatus.value = VoiceCallStatus.connected;
|
||||||
|
if (isAndroid) {
|
||||||
|
parent.target?.invokeMethod("on_voice_call_started");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onVoiceCallClosed(String reason) {
|
void onVoiceCallClosed(String reason) {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
|
if (isAndroid) {
|
||||||
|
parent.target?.invokeMethod("on_voice_call_closed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onVoiceCallIncoming() {
|
void onVoiceCallIncoming() {
|
||||||
|
|||||||
@@ -11,15 +11,10 @@ import './platform_model.dart';
|
|||||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||||
|
|
||||||
// Feature flutter_texture_render need to be enabled if feature vram is enabled.
|
|
||||||
final useTextureRender = !isWeb &&
|
|
||||||
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
|
|
||||||
|
|
||||||
class _PixelbufferTexture {
|
class _PixelbufferTexture {
|
||||||
int _textureKey = -1;
|
int _textureKey = -1;
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasPixelbufferTextureRender();
|
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
@@ -28,7 +23,6 @@ class _PixelbufferTexture {
|
|||||||
int get display => _display;
|
int get display => _display;
|
||||||
|
|
||||||
create(int d, SessionID sessionId, FFI ffi) {
|
create(int d, SessionID sessionId, FFI ffi) {
|
||||||
if (support) {
|
|
||||||
_display = d;
|
_display = d;
|
||||||
_textureKey = bind.getNextTextureKey();
|
_textureKey = bind.getNextTextureKey();
|
||||||
_sessionId = sessionId;
|
_sessionId = sessionId;
|
||||||
@@ -44,10 +38,9 @@ class _PixelbufferTexture {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(bool unregisterTexture, FFI ffi) async {
|
||||||
if (!_destroying && support && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
if (unregisterTexture) {
|
||||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class GroupModel {
|
|||||||
GroupModel(this.parent);
|
GroupModel(this.parent);
|
||||||
|
|
||||||
Future<void> pull({force = true, quiet = false}) async {
|
Future<void> pull({force = true, quiet = false}) async {
|
||||||
|
if (bind.isDisableGroupPanel()) return;
|
||||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||||
if (!force && initialized) return;
|
if (!force && initialized) return;
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
RxBool waitForImageDialogShow = true.obs;
|
RxBool waitForImageDialogShow = true.obs;
|
||||||
Timer? waitForImageTimer;
|
Timer? waitForImageTimer;
|
||||||
RxBool waitForFirstImage = true.obs;
|
RxBool waitForFirstImage = true.obs;
|
||||||
|
bool isRefreshing = false;
|
||||||
|
|
||||||
Rect? get rect => _rect;
|
Rect? get rect => _rect;
|
||||||
bool get isOriginalResolutionSet =>
|
bool get isOriginalResolutionSet =>
|
||||||
@@ -244,7 +245,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
handleMsgBox({
|
handleMsgBox({
|
||||||
'type': 'success',
|
'type': 'success',
|
||||||
'title': 'Successful',
|
'title': 'Successful',
|
||||||
'text': 'Connected, waiting for image...',
|
'text': kMsgboxTextWaitingForImage,
|
||||||
'link': '',
|
'link': '',
|
||||||
}, sessionId, peerId);
|
}, sessionId, peerId);
|
||||||
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
||||||
@@ -380,16 +381,27 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handleSyncPeerOption(evt, peerId);
|
_handleSyncPeerOption(evt, peerId);
|
||||||
} else if (name == 'follow_current_display') {
|
} else if (name == 'follow_current_display') {
|
||||||
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||||
|
} else if (name == 'use_texture_render') {
|
||||||
|
_handleUseTextureRender(evt, sessionId, peerId);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Unknown event name: $name');
|
debugPrint('Unknown event name: $name');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleUseTextureRender(
|
||||||
|
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||||
|
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
||||||
|
waitForFirstImage.value = true;
|
||||||
|
isRefreshing = true;
|
||||||
|
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
||||||
|
'success', 'Successful', kMsgboxTextWaitingForImage);
|
||||||
|
}
|
||||||
|
|
||||||
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
|
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
|
||||||
final k = evt['k'];
|
final k = evt['k'];
|
||||||
final v = evt['v'];
|
final v = evt['v'];
|
||||||
if (k == kOptionViewOnly) {
|
if (k == kOptionToggleViewOnly) {
|
||||||
setViewOnly(peer, v as bool);
|
setViewOnly(peer, v as bool);
|
||||||
} else if (k == 'keyboard_mode') {
|
} else if (k == 'keyboard_mode') {
|
||||||
parent.target?.inputModel.updateKeyboardMode();
|
parent.target?.inputModel.updateKeyboardMode();
|
||||||
@@ -572,7 +584,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
showElevationError(sessionId, type, title, text, dialogManager);
|
showElevationError(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||||
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
||||||
} else if (text == 'Connected, waiting for image...') {
|
} else if (text == kMsgboxTextWaitingForImage) {
|
||||||
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
||||||
} else if (title == 'Privacy mode') {
|
} else if (title == 'Privacy mode') {
|
||||||
final hasRetry = evt['hasRetry'] == 'true';
|
final hasRetry = evt['hasRetry'] == 'true';
|
||||||
@@ -667,7 +679,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
waitForImageDialogShow.value = true;
|
waitForImageDialogShow.value = true;
|
||||||
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
||||||
if (waitForFirstImage.isTrue) {
|
if (waitForFirstImage.isTrue && !isRefreshing) {
|
||||||
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -765,7 +777,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
_touchMode = true;
|
_touchMode = true;
|
||||||
} else {
|
} else {
|
||||||
_touchMode = await bind.sessionGetOption(
|
_touchMode = await bind.sessionGetOption(
|
||||||
sessionId: sessionId, arg: 'touch-mode') !=
|
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
if (connType == ConnType.fileTransfer) {
|
if (connType == ConnType.fileTransfer) {
|
||||||
@@ -785,6 +797,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
Map<String, dynamic> features = json.decode(evt['features']);
|
Map<String, dynamic> features = json.decode(evt['features']);
|
||||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||||
@@ -797,7 +810,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
setViewOnly(
|
setViewOnly(
|
||||||
peerId,
|
peerId,
|
||||||
bind.sessionGetToggleOptionSync(
|
bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: kOptionViewOnly));
|
sessionId: sessionId, arg: kOptionToggleViewOnly));
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn) {
|
if (connType == ConnType.defaultConn) {
|
||||||
final platformAdditions = evt['platform_additions'];
|
final platformAdditions = evt['platform_additions'];
|
||||||
@@ -1156,6 +1169,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
late final SessionID sessionId;
|
late final SessionID sessionId;
|
||||||
|
|
||||||
|
bool _useTextureRender = false;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
final List<Function(String)> callbacksOnFirstImage = [];
|
final List<Function(String)> callbacksOnFirstImage = [];
|
||||||
@@ -1164,6 +1179,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get useTextureRender => _useTextureRender;
|
||||||
|
|
||||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||||
|
|
||||||
onRgba(int display, Uint8List rgba) {
|
onRgba(int display, Uint8List rgba) {
|
||||||
@@ -1203,11 +1220,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
if (parent.target != null) {
|
if (parent.target != null) {
|
||||||
await initializeCursorAndCanvas(parent.target!);
|
await initializeCursorAndCanvas(parent.target!);
|
||||||
}
|
}
|
||||||
if (parent.target?.ffiModel.isPeerAndroid ?? false) {
|
|
||||||
bind.sessionSetViewStyle(sessionId: sessionId, value: 'adaptive');
|
|
||||||
parent.target?.canvasModel.updateViewStyle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_image?.dispose();
|
||||||
_image = image;
|
_image = image;
|
||||||
if (image != null) notifyListeners();
|
if (image != null) notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -1231,6 +1245,24 @@ class ImageModel with ChangeNotifier {
|
|||||||
final yscale = size.height / _image!.height;
|
final yscale = size.height / _image!.height;
|
||||||
return min(xscale, yscale) / 1.5;
|
return min(xscale, yscale) / 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUserTextureRender() {
|
||||||
|
final preValue = _useTextureRender;
|
||||||
|
_useTextureRender = isDesktop && bind.mainGetUseTextureRender();
|
||||||
|
if (preValue != _useTextureRender) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUseTextureRender(bool value) {
|
||||||
|
_useTextureRender = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeImage() {
|
||||||
|
_image?.dispose();
|
||||||
|
_image = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ScrollStyle {
|
enum ScrollStyle {
|
||||||
@@ -1667,13 +1699,15 @@ const _forbiddenCursorPng =
|
|||||||
const _defaultCursorPng =
|
const _defaultCursorPng =
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
||||||
|
|
||||||
|
const kPreForbiddenCursorId = -2;
|
||||||
final preForbiddenCursor = PredefinedCursor(
|
final preForbiddenCursor = PredefinedCursor(
|
||||||
png: _forbiddenCursorPng,
|
png: _forbiddenCursorPng,
|
||||||
id: -2,
|
id: kPreForbiddenCursorId,
|
||||||
);
|
);
|
||||||
|
const kPreDefaultCursorId = -1;
|
||||||
final preDefaultCursor = PredefinedCursor(
|
final preDefaultCursor = PredefinedCursor(
|
||||||
png: _defaultCursorPng,
|
png: _defaultCursorPng,
|
||||||
id: -1,
|
id: kPreDefaultCursorId,
|
||||||
hotxGetter: (double w) => w / 2,
|
hotxGetter: (double w) => w / 2,
|
||||||
hotyGetter: (double h) => h / 2,
|
hotyGetter: (double h) => h / 2,
|
||||||
);
|
);
|
||||||
@@ -1698,13 +1732,22 @@ class PredefinedCursor {
|
|||||||
init() {
|
init() {
|
||||||
_image2 = img2.decodePng(base64Decode(png));
|
_image2 = img2.decodePng(base64Decode(png));
|
||||||
if (_image2 != null) {
|
if (_image2 != null) {
|
||||||
|
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||||
|
if (isWindows && id == kPreForbiddenCursorId) {
|
||||||
|
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
||||||
|
}
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
final defaultImg = _image2!;
|
final defaultImg = _image2!;
|
||||||
// This function is called only one time, no need to care about the performance.
|
// This function is called only one time, no need to care about the performance.
|
||||||
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
||||||
|
_image?.dispose();
|
||||||
_image = await img.decodeImageFromPixels(
|
_image = await img.decodeImageFromPixels(
|
||||||
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
||||||
|
if (_image == null) {
|
||||||
|
print("decodeImageFromPixels failed, pre-defined cursor $id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
double scale = 1.0;
|
double scale = 1.0;
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
||||||
@@ -1937,6 +1980,11 @@ class CursorModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposeImages() {
|
||||||
|
_images.forEach((_, v) => v.item1.dispose());
|
||||||
|
_images.clear();
|
||||||
|
}
|
||||||
|
|
||||||
updateCursorData(Map<String, dynamic> evt) async {
|
updateCursorData(Map<String, dynamic> evt) async {
|
||||||
final id = int.parse(evt['id']);
|
final id = int.parse(evt['id']);
|
||||||
final hotx = double.parse(evt['hotx']);
|
final hotx = double.parse(evt['hotx']);
|
||||||
@@ -1947,7 +1995,11 @@ class CursorModel with ChangeNotifier {
|
|||||||
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||||
final image = await img.decodeImageFromPixels(
|
final image = await img.decodeImageFromPixels(
|
||||||
rgba, width, height, ui.PixelFormat.rgba8888);
|
rgba, width, height, ui.PixelFormat.rgba8888);
|
||||||
|
if (image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
||||||
|
_images[id]?.item1.dispose();
|
||||||
_images[id] = Tuple3(image, hotx, hoty);
|
_images[id] = Tuple3(image, hotx, hoty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2062,7 +2114,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
_x = -10000;
|
_x = -10000;
|
||||||
_x = -10000;
|
_x = -10000;
|
||||||
_image = null;
|
_image = null;
|
||||||
_images.clear();
|
disposeImages();
|
||||||
|
|
||||||
_clearCache();
|
_clearCache();
|
||||||
_cache = null;
|
_cache = null;
|
||||||
@@ -2305,6 +2357,7 @@ class FFI {
|
|||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
ffiModel.waitForImageTimer?.cancel();
|
ffiModel.waitForImageTimer?.cancel();
|
||||||
ffiModel.waitForImageTimer = null;
|
ffiModel.waitForImageTimer = null;
|
||||||
@@ -2371,7 +2424,7 @@ class FFI {
|
|||||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||||
ffiModel.pi.currentDisplay = display;
|
ffiModel.pi.currentDisplay = display;
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn && useTextureRender) {
|
if (isDesktop && connType == ConnType.defaultConn) {
|
||||||
textureModel.updateCurrentDisplay(display ?? 0);
|
textureModel.updateCurrentDisplay(display ?? 0);
|
||||||
}
|
}
|
||||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||||
@@ -2393,9 +2446,8 @@ class FFI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasPixelBufferTextureRender = bind.mainHasPixelbufferTextureRender();
|
imageModel.updateUserTextureRender();
|
||||||
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
||||||
|
|
||||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||||
// Preserved for the rgba data.
|
// Preserved for the rgba data.
|
||||||
stream.listen((message) {
|
stream.listen((message) {
|
||||||
@@ -2444,7 +2496,7 @@ class FFI {
|
|||||||
}
|
}
|
||||||
} else if (message is EventToUI_Rgba) {
|
} else if (message is EventToUI_Rgba) {
|
||||||
final display = message.field0;
|
final display = message.field0;
|
||||||
if (hasPixelBufferTextureRender) {
|
if (imageModel.useTextureRender) {
|
||||||
debugPrint("EventToUI_Rgba display:$display");
|
debugPrint("EventToUI_Rgba display:$display");
|
||||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,9 +23,6 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
int get currentTab => _currentTab;
|
int get currentTab => _currentTab;
|
||||||
int _currentTab = 0; // index in tabNames
|
int _currentTab = 0; // index in tabNames
|
||||||
static const int maxTabCount = 5;
|
static const int maxTabCount = 5;
|
||||||
static const String kPeerTabIndex = 'peer-tab-index';
|
|
||||||
static const String kPeerTabOrder = 'peer-tab-order';
|
|
||||||
static const String kPeerTabVisible = 'peer-tab-visible';
|
|
||||||
static const List<String> tabNames = [
|
static const List<String> tabNames = [
|
||||||
'Recent sessions',
|
'Recent sessions',
|
||||||
'Favorites',
|
'Favorites',
|
||||||
@@ -44,7 +42,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
true,
|
true,
|
||||||
!isWeb,
|
!isWeb,
|
||||||
!(bind.isDisableAb() || bind.isDisableAccount()),
|
!(bind.isDisableAb() || bind.isDisableAccount()),
|
||||||
!bind.isDisableAccount(),
|
!(bind.isDisableGroupPanel() || bind.isDisableAccount()),
|
||||||
]);
|
]);
|
||||||
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
||||||
List<bool> get isVisibleEnabled => () {
|
List<bool> get isVisibleEnabled => () {
|
||||||
@@ -72,7 +70,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
PeerTabModel(this.parent) {
|
PeerTabModel(this.parent) {
|
||||||
// visible
|
// visible
|
||||||
try {
|
try {
|
||||||
final option = bind.getLocalFlutterOption(k: kPeerTabVisible);
|
final option = bind.getLocalFlutterOption(k: kOptionPeerTabVisible);
|
||||||
if (option.isNotEmpty) {
|
if (option.isNotEmpty) {
|
||||||
List<dynamic> decodeList = jsonDecode(option);
|
List<dynamic> decodeList = jsonDecode(option);
|
||||||
if (decodeList.length == _isVisible.length) {
|
if (decodeList.length == _isVisible.length) {
|
||||||
@@ -88,7 +86,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// order
|
// order
|
||||||
try {
|
try {
|
||||||
final option = bind.getLocalFlutterOption(k: kPeerTabOrder);
|
final option = bind.getLocalFlutterOption(k: kOptionPeerTabOrder);
|
||||||
if (option.isNotEmpty) {
|
if (option.isNotEmpty) {
|
||||||
List<dynamic> decodeList = jsonDecode(option);
|
List<dynamic> decodeList = jsonDecode(option);
|
||||||
if (decodeList.length == maxTabCount) {
|
if (decodeList.length == maxTabCount) {
|
||||||
@@ -112,7 +110,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// init currentTab
|
// init currentTab
|
||||||
_currentTab =
|
_currentTab =
|
||||||
int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0;
|
int.tryParse(bind.getLocalFlutterOption(k: kOptionPeerTabIndex)) ?? 0;
|
||||||
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
||||||
_currentTab = 0;
|
_currentTab = 0;
|
||||||
}
|
}
|
||||||
@@ -222,7 +220,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kPeerTabVisible, v: jsonEncode(_isVisible));
|
k: kOptionPeerTabVisible, v: jsonEncode(_isVisible));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -258,7 +256,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
for (int i = 0; i < list.length; i++) {
|
for (int i = 0; i < list.length; i++) {
|
||||||
orders[i] = list[i];
|
orders[i] = list[i];
|
||||||
}
|
}
|
||||||
bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders));
|
bind.setLocalFlutterOption(k: kOptionPeerTabOrder, v: jsonEncode(orders));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
String get approveMode => _approveMode;
|
String get approveMode => _approveMode;
|
||||||
|
|
||||||
setVerificationMethod(String method) async {
|
setVerificationMethod(String method) async {
|
||||||
await bind.mainSetOption(key: "verification-method", value: method);
|
await bind.mainSetOption(key: kOptionVerificationMethod, value: method);
|
||||||
/*
|
/*
|
||||||
if (method != kUsePermanentPassword) {
|
if (method != kUsePermanentPassword) {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
@@ -99,7 +99,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setApproveMode(String mode) async {
|
setApproveMode(String mode) async {
|
||||||
await bind.mainSetOption(key: 'approve-mode', value: mode);
|
await bind.mainSetOption(key: kOptionApproveMode, value: mode);
|
||||||
/*
|
/*
|
||||||
if (mode != 'password') {
|
if (mode != 'password') {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
@@ -125,8 +125,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
/*
|
/*
|
||||||
// initital _hideCm at startup
|
// initital _hideCm at startup
|
||||||
final verificationMethod =
|
final verificationMethod =
|
||||||
bind.mainGetOptionSync(key: "verification-method");
|
bind.mainGetOptionSync(key: kOptionVerificationMethod);
|
||||||
final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
|
final approveMode = bind.mainGetOptionSync(key: kOptionApproveMode);
|
||||||
_hideCm = option2bool(
|
_hideCm = option2bool(
|
||||||
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
||||||
if (!(approveMode == 'password' &&
|
if (!(approveMode == 'password' &&
|
||||||
@@ -187,18 +187,19 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (androidVersion < 30 ||
|
if (androidVersion < 30 ||
|
||||||
!await AndroidPermissionManager.check(kRecordAudio)) {
|
!await AndroidPermissionManager.check(kRecordAudio)) {
|
||||||
_audioOk = false;
|
_audioOk = false;
|
||||||
bind.mainSetOption(key: "enable-audio", value: "N");
|
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final audioOption = await bind.mainGetOption(key: 'enable-audio');
|
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||||
_audioOk = audioOption.isEmpty;
|
_audioOk = audioOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// file
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
_fileOk = false;
|
_fileOk = false;
|
||||||
bind.mainSetOption(key: "enable-file-transfer", value: "N");
|
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final fileOption = await bind.mainGetOption(key: 'enable-file-transfer');
|
final fileOption =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||||
_fileOk = fileOption.isEmpty;
|
_fileOk = fileOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,10 +210,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
var update = false;
|
var update = false;
|
||||||
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
||||||
final verificationMethod =
|
final verificationMethod =
|
||||||
await bind.mainGetOption(key: "verification-method");
|
await bind.mainGetOption(key: kOptionVerificationMethod);
|
||||||
final temporaryPasswordLength =
|
final temporaryPasswordLength =
|
||||||
await bind.mainGetOption(key: "temporary-password-length");
|
await bind.mainGetOption(key: "temporary-password-length");
|
||||||
final approveMode = await bind.mainGetOption(key: 'approve-mode');
|
final approveMode = await bind.mainGetOption(key: kOptionApproveMode);
|
||||||
/*
|
/*
|
||||||
var hideCm = option2bool(
|
var hideCm = option2bool(
|
||||||
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
||||||
@@ -283,7 +284,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_audioOk = !_audioOk;
|
_audioOk = !_audioOk;
|
||||||
bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +304,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fileOk = !_fileOk;
|
_fileOk = !_fileOk;
|
||||||
bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableFileTransfer,
|
||||||
|
value: _fileOk ? defaultOptionYes : 'N');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +316,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (_inputOk) {
|
if (_inputOk) {
|
||||||
parent.target?.invokeMethod("stop_input");
|
parent.target?.invokeMethod("stop_input");
|
||||||
bind.mainSetOption(key: "enable-keyboard", value: 'N');
|
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||||
} else {
|
} else {
|
||||||
if (parent.target != null) {
|
if (parent.target != null) {
|
||||||
/// the result of toggle-on depends on user actions in the settings page.
|
/// the result of toggle-on depends on user actions in the settings page.
|
||||||
@@ -363,6 +367,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await checkRequestNotificationPermission();
|
await checkRequestNotificationPermission();
|
||||||
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
|
}
|
||||||
final res = await parent.target?.dialogManager
|
final res = await parent.target?.dialogManager
|
||||||
.show<bool>((setState, close, context) {
|
.show<bool>((setState, close, context) {
|
||||||
submit() => close(true);
|
submit() => close(true);
|
||||||
@@ -445,7 +452,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case "input":
|
case "input":
|
||||||
if (_inputOk != value) {
|
if (_inputOk != value) {
|
||||||
bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableKeyboard,
|
||||||
|
value: value ? defaultOptionYes : 'N');
|
||||||
}
|
}
|
||||||
_inputOk = value;
|
_inputOk = value;
|
||||||
break;
|
break;
|
||||||
@@ -550,37 +559,60 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showLoginDialog(Client client) {
|
void showLoginDialog(Client client) {
|
||||||
|
showClientDialog(
|
||||||
|
client,
|
||||||
|
client.isFileTransfer ? "File Connection" : "Screen Connection",
|
||||||
|
'Do you accept?',
|
||||||
|
'android_new_connection_tip',
|
||||||
|
() => sendLoginResponse(client, false),
|
||||||
|
() => sendLoginResponse(client, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVoiceCall(Client client, bool accept) {
|
||||||
|
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||||
|
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
showVoiceCallDialog(Client client) {
|
||||||
|
showClientDialog(
|
||||||
|
client,
|
||||||
|
'Voice call',
|
||||||
|
'Do you accept?',
|
||||||
|
'android_new_voice_call_tip',
|
||||||
|
() => handleVoiceCall(client, false),
|
||||||
|
() => handleVoiceCall(client, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showClientDialog(Client client, String title, String contentTitle,
|
||||||
|
String content, VoidCallback onCancel, VoidCallback onSubmit) {
|
||||||
parent.target?.dialogManager.show((setState, close, context) {
|
parent.target?.dialogManager.show((setState, close, context) {
|
||||||
cancel() {
|
cancel() {
|
||||||
sendLoginResponse(client, false);
|
onCancel();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
sendLoginResponse(client, true);
|
onSubmit();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title:
|
title:
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
Text(translate(
|
Text(translate(title)),
|
||||||
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
IconButton(onPressed: close, icon: const Icon(Icons.close))
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close))
|
|
||||||
]),
|
]),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(translate("Do you accept?")),
|
Text(translate(contentTitle)),
|
||||||
ClientInfo(client),
|
ClientInfo(client),
|
||||||
Text(
|
Text(
|
||||||
translate("android_new_connection_tip"),
|
translate(content),
|
||||||
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -676,11 +708,15 @@ class ServerModel with ChangeNotifier {
|
|||||||
_clients[index].inVoiceCall = client.inVoiceCall;
|
_clients[index].inVoiceCall = client.inVoiceCall;
|
||||||
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
||||||
if (client.incomingVoiceCall) {
|
if (client.incomingVoiceCall) {
|
||||||
|
if (isAndroid) {
|
||||||
|
showVoiceCallDialog(client);
|
||||||
|
} else {
|
||||||
// Has incoming phone call, let's set the window on top.
|
// Has incoming phone call, let's set the window on top.
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
windowOnTop(null);
|
windowOnTop(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ class StateGlobal {
|
|||||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||||
final RxBool showRemoteToolBar = false.obs;
|
final RxBool showRemoteToolBar = false.obs;
|
||||||
final svcStatus = SvcStatus.notReady.obs;
|
final svcStatus = SvcStatus.notReady.obs;
|
||||||
// Only used for macOS
|
|
||||||
bool? closeOnFullscreen;
|
|
||||||
final RxBool isFocused = false.obs;
|
final RxBool isFocused = false.obs;
|
||||||
|
|
||||||
String _inputSource = '';
|
String _inputSource = '';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
|
||||||
Future<ui.Image> decodeImageFromPixels(
|
Future<ui.Image?> decodeImageFromPixels(
|
||||||
Uint8List pixels,
|
Uint8List pixels,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -18,15 +18,30 @@ Future<ui.Image> decodeImageFromPixels(
|
|||||||
}) async {
|
}) async {
|
||||||
if (targetWidth != null) {
|
if (targetWidth != null) {
|
||||||
assert(allowUpscaling || targetWidth <= width);
|
assert(allowUpscaling || targetWidth <= width);
|
||||||
|
if (!(allowUpscaling || targetWidth <= width)) {
|
||||||
|
print("not allow upscaling but targetWidth > width");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (targetHeight != null) {
|
if (targetHeight != null) {
|
||||||
assert(allowUpscaling || targetHeight <= height);
|
assert(allowUpscaling || targetHeight <= height);
|
||||||
|
if (!(allowUpscaling || targetHeight <= height)) {
|
||||||
|
print("not allow upscaling but targetHeight > height");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ui.ImmutableBuffer buffer =
|
final ui.ImmutableBuffer buffer;
|
||||||
await ui.ImmutableBuffer.fromUint8List(pixels);
|
try {
|
||||||
|
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||||
onPixelsCopied?.call();
|
onPixelsCopied?.call();
|
||||||
final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ui.ImageDescriptor descriptor;
|
||||||
|
try {
|
||||||
|
descriptor = ui.ImageDescriptor.raw(
|
||||||
buffer,
|
buffer,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -41,13 +56,36 @@ Future<ui.Image> decodeImageFromPixels(
|
|||||||
targetHeight = descriptor.height;
|
targetHeight = descriptor.height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("ImageDescriptor.raw failed: $e");
|
||||||
|
buffer.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final ui.Codec codec = await descriptor.instantiateCodec(
|
final ui.Codec codec;
|
||||||
|
try {
|
||||||
|
codec = await descriptor.instantiateCodec(
|
||||||
targetWidth: targetWidth,
|
targetWidth: targetWidth,
|
||||||
targetHeight: targetHeight,
|
targetHeight: targetHeight,
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("instantiateCodec failed: $e");
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ui.FrameInfo frameInfo;
|
||||||
|
try {
|
||||||
|
frameInfo = await codec.getNextFrame();
|
||||||
|
} catch (e) {
|
||||||
|
print("getNextFrame failed: $e");
|
||||||
|
codec.dispose();
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
|
||||||
codec.dispose();
|
codec.dispose();
|
||||||
buffer.dispose();
|
buffer.dispose();
|
||||||
descriptor.dispose();
|
descriptor.dispose();
|
||||||
|
|||||||
@@ -174,7 +174,9 @@ class RustDeskMultiWindowManager {
|
|||||||
windowId: windowId, peerId: remoteId);
|
windowId: windowId, peerId: remoteId);
|
||||||
}
|
}
|
||||||
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
||||||
|
if (methodName != kWindowEventNewRemoteDesktop) {
|
||||||
WindowController.fromWindowId(windowId).show();
|
WindowController.fromWindowId(windowId).show();
|
||||||
|
}
|
||||||
registerActiveWindow(windowId);
|
registerActiveWindow(windowId);
|
||||||
return MultiWindowCallResult(windowId, null);
|
return MultiWindowCallResult(windowId, null);
|
||||||
}
|
}
|
||||||
@@ -334,10 +336,10 @@ class RustDeskMultiWindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeAllSubWindows() async {
|
Future<void> closeAllSubWindows() async {
|
||||||
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
await Future.wait(WindowType.values.map((e) => _closeWindows(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeWindows(WindowType type) async {
|
Future<void> _closeWindows(WindowType type) async {
|
||||||
if (type == WindowType.Main) {
|
if (type == WindowType.Main) {
|
||||||
// skip main window, use window manager instead
|
// skip main window, use window manager instead
|
||||||
return;
|
return;
|
||||||
@@ -345,7 +347,7 @@ class RustDeskMultiWindowManager {
|
|||||||
|
|
||||||
List<int> windows = [];
|
List<int> windows = [];
|
||||||
try {
|
try {
|
||||||
windows = await DesktopMultiWindow.getAllSubWindowIds();
|
windows = _findWindowsByType(type);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
||||||
return;
|
return;
|
||||||
@@ -355,14 +357,9 @@ class RustDeskMultiWindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (final wId in windows) {
|
for (final wId in windows) {
|
||||||
debugPrint("closing multi window: ${type.toString()}");
|
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||||
await saveWindowPosition(type, windowId: wId);
|
await saveWindowPosition(type, windowId: wId);
|
||||||
try {
|
try {
|
||||||
// final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
|
||||||
// if (!ids.contains(wId)) {
|
|
||||||
// // no such window already
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
await WindowController.fromWindowId(wId).setPreventClose(false);
|
await WindowController.fromWindowId(wId).setPreventClose(false);
|
||||||
await WindowController.fromWindowId(wId).close();
|
await WindowController.fromWindowId(wId).close();
|
||||||
_activeWindows.remove(wId);
|
_activeWindows.remove(wId);
|
||||||
@@ -371,7 +368,6 @@ class RustDeskMultiWindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await _notifyActiveWindow();
|
|
||||||
clearWindowType(type);
|
clearWindowType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,14 +400,6 @@ class RustDeskMultiWindowManager {
|
|||||||
await _notifyActiveWindow();
|
await _notifyActiveWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> destroyWindow(int windowId) async {
|
|
||||||
await WindowController.fromWindowId(windowId).setPreventClose(false);
|
|
||||||
await WindowController.fromWindowId(windowId).close();
|
|
||||||
_remoteDesktopWindows.remove(windowId);
|
|
||||||
_fileTransferWindows.remove(windowId);
|
|
||||||
_portForwardWindows.remove(windowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove active window which has [`windowId`]
|
/// Remove active window which has [`windowId`]
|
||||||
///
|
///
|
||||||
/// [Availability]
|
/// [Availability]
|
||||||
|
|||||||
@@ -203,12 +203,6 @@ class RustdeskImpl {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> sessionGetFlutterOptionByPeerId(
|
|
||||||
{required String id, required String k, dynamic hint}) {
|
|
||||||
return Future(
|
|
||||||
() => js.context.callMethod('getByName', ['option:flutter:peer', k]));
|
|
||||||
}
|
|
||||||
|
|
||||||
int getNextTextureKey({dynamic hint}) {
|
int getNextTextureKey({dynamic hint}) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1420,10 +1414,6 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mainHasPixelbufferTextureRender({dynamic hint}) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mainHasFileClipboard({dynamic hint}) {
|
bool mainHasFileClipboard({dynamic hint}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1470,6 +1460,10 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDisableGroupPanel({dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isDisableAccount({dynamic hint}) {
|
bool isDisableAccount({dynamic hint}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1610,5 +1604,13 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mainIsOptionFixed({required String key, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mainGetUseTextureRender({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
|
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.2.4+39
|
version: 1.2.5+39
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ export default class Connection {
|
|||||||
const defaultToggleTrue = [
|
const defaultToggleTrue = [
|
||||||
'show-remote-cursor',
|
'show-remote-cursor',
|
||||||
'privacy-mode',
|
'privacy-mode',
|
||||||
'enable-file-transfer',
|
'enable-file-copy-paste',
|
||||||
'allow_swap_key',
|
'allow_swap_key',
|
||||||
];
|
];
|
||||||
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
|
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
|
||||||
@@ -906,7 +906,7 @@ export default class Connection {
|
|||||||
case "privacy-mode":
|
case "privacy-mode":
|
||||||
option.privacy_mode = v2;
|
option.privacy_mode = v2;
|
||||||
break;
|
break;
|
||||||
case "enable-file-transfer":
|
case "enable-file-copy-paste":
|
||||||
option.enable_file_transfer = v2;
|
option.enable_file_transfer = v2;
|
||||||
break;
|
break;
|
||||||
case "block-input":
|
case "block-input":
|
||||||
@@ -933,7 +933,7 @@ export default class Connection {
|
|||||||
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
|
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
|
||||||
? message.OptionMessage_BoolOption.Yes
|
? message.OptionMessage_BoolOption.Yes
|
||||||
: message.OptionMessage_BoolOption.No;
|
: message.OptionMessage_BoolOption.No;
|
||||||
option.enable_file_transfer = this.getToggleOption("enable-file-transfer")
|
option.enable_file_transfer = this.getToggleOption("enable-file-copy-paste")
|
||||||
? message.OptionMessage_BoolOption.Yes
|
? message.OptionMessage_BoolOption.Yes
|
||||||
: message.OptionMessage_BoolOption.No;
|
: message.OptionMessage_BoolOption.No;
|
||||||
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
|
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
|
||||||
|
|||||||
@@ -116,15 +116,27 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) {
|
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) {
|
||||||
is_cm_page = true;
|
is_cm_page = true;
|
||||||
}
|
}
|
||||||
|
bool is_install_page = false;
|
||||||
|
auto installParam = std::string("--install");
|
||||||
|
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, installParam.size(), installParam.c_str()) == 0) {
|
||||||
|
is_install_page = true;
|
||||||
|
}
|
||||||
|
|
||||||
command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end());
|
command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end());
|
||||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||||
|
|
||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(800, 600);
|
Win32Window::Size size(800, 600);
|
||||||
if (!window.CreateAndShow(
|
std::wstring window_title;
|
||||||
is_cm_page ? app_name + L" - Connection Manager" : app_name, origin,
|
if (is_cm_page) {
|
||||||
size, !is_cm_page)) {
|
window_title = app_name + L" - Connection Manager";
|
||||||
|
} else if (is_install_page) {
|
||||||
|
window_title = app_name + L" - Install";
|
||||||
|
} else {
|
||||||
|
window_title = app_name;
|
||||||
|
}
|
||||||
|
if (!window.CreateAndShow(window_title, origin, size, !is_cm_page)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
flexi_logger = { version = "0.27", features = ["async"] }
|
flexi_logger = { version = "0.27", features = ["async"] }
|
||||||
protobuf = { version = "3.4", features = ["with-bytes"] }
|
protobuf = { version = "3.4", features = ["with-bytes"] }
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.37", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["full"] }
|
tokio-util = { version = "0.7", features = ["full"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
bytes = { version = "1.4", features = ["serde"] }
|
bytes = { version = "1.6", features = ["serde"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||||
zstd = "0.13"
|
zstd = "0.13"
|
||||||
quinn = {version = "0.9", optional = true }
|
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
directories-next = "2.0"
|
directories-next = "2.0"
|
||||||
@@ -26,12 +25,12 @@ serde_derive = "1.0"
|
|||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
confy = { git = "https://github.com/open-trade/confy" }
|
confy = { git = "https://github.com/rustdesk-org/confy" }
|
||||||
dirs-next = "2.0"
|
dirs-next = "2.0"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
sodiumoxide = "0.2"
|
sodiumoxide = "0.2"
|
||||||
regex = "1.8"
|
regex = "1.8"
|
||||||
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
|
tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
@@ -55,9 +54,6 @@ rustls-pki-types = "1.4"
|
|||||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||||
tokio-native-tls ="0.3"
|
tokio-native-tls ="0.3"
|
||||||
|
|
||||||
[features]
|
|
||||||
quic = []
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
protobuf-codegen = { version = "3.4" }
|
protobuf-codegen = { version = "3.4" }
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ const SERIAL: i32 = 3;
|
|||||||
const PASSWORD_ENC_VERSION: &str = "00";
|
const PASSWORD_ENC_VERSION: &str = "00";
|
||||||
const ENCRYPT_MAX_LEN: usize = 128;
|
const ENCRYPT_MAX_LEN: usize = 128;
|
||||||
|
|
||||||
// config2 options
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub const CONFIG_OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref ORG: RwLock<String> = RwLock::new("com.carriez".to_owned());
|
pub static ref ORG: RwLock<String> = RwLock::new("com.carriez".to_owned());
|
||||||
@@ -278,7 +274,7 @@ pub struct PeerConfig {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub disable_clipboard: DisableClipboard,
|
pub disable_clipboard: DisableClipboard,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub enable_file_transfer: EnableFileTransfer,
|
pub enable_file_copy_paste: EnableFileCopyPaste,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub show_quality_monitor: ShowQualityMonitor,
|
pub show_quality_monitor: ShowQualityMonitor,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@@ -355,7 +351,7 @@ impl Default for PeerConfig {
|
|||||||
direct_failures: Default::default(),
|
direct_failures: Default::default(),
|
||||||
disable_audio: Default::default(),
|
disable_audio: Default::default(),
|
||||||
disable_clipboard: Default::default(),
|
disable_clipboard: Default::default(),
|
||||||
enable_file_transfer: Default::default(),
|
enable_file_copy_paste: Default::default(),
|
||||||
show_quality_monitor: Default::default(),
|
show_quality_monitor: Default::default(),
|
||||||
follow_remote_cursor: Default::default(),
|
follow_remote_cursor: Default::default(),
|
||||||
follow_remote_window: Default::default(),
|
follow_remote_window: Default::default(),
|
||||||
@@ -916,7 +912,7 @@ impl Config {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn purify_options(v: &mut HashMap<String, String>) {
|
fn purify_options(v: &mut HashMap<String, String>) {
|
||||||
v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, &DEFAULT_SETTINGS, k, v));
|
v.retain(|k, _| is_option_can_save(&OVERWRITE_SETTINGS, k));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_options(mut v: HashMap<String, String>) {
|
pub fn set_options(mut v: HashMap<String, String>) {
|
||||||
@@ -940,7 +936,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &DEFAULT_SETTINGS, &k, &v) {
|
if !is_option_can_save(&OVERWRITE_SETTINGS, &k) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut config = CONFIG2.write().unwrap();
|
let mut config = CONFIG2.write().unwrap();
|
||||||
@@ -1018,8 +1014,30 @@ impl Config {
|
|||||||
config.store();
|
config.store();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_socks_from_custom_client_advanced_settings(
|
||||||
|
settings: &HashMap<String, String>,
|
||||||
|
) -> Option<Socks5Server> {
|
||||||
|
let url = settings.get(keys::OPTION_PROXY_URL)?;
|
||||||
|
Some(Socks5Server {
|
||||||
|
proxy: url.to_owned(),
|
||||||
|
username: settings
|
||||||
|
.get(keys::OPTION_PROXY_USERNAME)
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
password: settings
|
||||||
|
.get(keys::OPTION_PROXY_PASSWORD)
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_socks() -> Option<Socks5Server> {
|
pub fn get_socks() -> Option<Socks5Server> {
|
||||||
CONFIG2.read().unwrap().socks.clone()
|
Self::get_socks_from_custom_client_advanced_settings(&OVERWRITE_SETTINGS.read().unwrap())
|
||||||
|
.or(CONFIG2.read().unwrap().socks.clone())
|
||||||
|
.or(Self::get_socks_from_custom_client_advanced_settings(
|
||||||
|
&DEFAULT_SETTINGS.read().unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -1028,10 +1046,26 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_network_type() -> NetworkType {
|
pub fn get_network_type() -> NetworkType {
|
||||||
match &CONFIG2.read().unwrap().socks {
|
if OVERWRITE_SETTINGS
|
||||||
None => NetworkType::Direct,
|
.read()
|
||||||
Some(_) => NetworkType::ProxySocks,
|
.unwrap()
|
||||||
|
.get(keys::OPTION_PROXY_URL)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return NetworkType::ProxySocks;
|
||||||
}
|
}
|
||||||
|
if CONFIG2.read().unwrap().socks.is_some() {
|
||||||
|
return NetworkType::ProxySocks;
|
||||||
|
}
|
||||||
|
if DEFAULT_SETTINGS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(keys::OPTION_PROXY_URL)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return NetworkType::ProxySocks;
|
||||||
|
}
|
||||||
|
NetworkType::Direct
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get() -> Config {
|
pub fn get() -> Config {
|
||||||
@@ -1194,36 +1228,36 @@ impl PeerConfig {
|
|||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_view_style,
|
default_view_style,
|
||||||
deserialize_view_style,
|
deserialize_view_style,
|
||||||
UserDefaultConfig::read("view_style")
|
UserDefaultConfig::read(keys::OPTION_VIEW_STYLE)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_scroll_style,
|
default_scroll_style,
|
||||||
deserialize_scroll_style,
|
deserialize_scroll_style,
|
||||||
UserDefaultConfig::read("scroll_style")
|
UserDefaultConfig::read(keys::OPTION_SCROLL_STYLE)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_image_quality,
|
default_image_quality,
|
||||||
deserialize_image_quality,
|
deserialize_image_quality,
|
||||||
UserDefaultConfig::read("image_quality")
|
UserDefaultConfig::read(keys::OPTION_IMAGE_QUALITY)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_reverse_mouse_wheel,
|
default_reverse_mouse_wheel,
|
||||||
deserialize_reverse_mouse_wheel,
|
deserialize_reverse_mouse_wheel,
|
||||||
UserDefaultConfig::read("reverse_mouse_wheel")
|
UserDefaultConfig::read(keys::OPTION_REVERSE_MOUSE_WHEEL)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_displays_as_individual_windows,
|
default_displays_as_individual_windows,
|
||||||
deserialize_displays_as_individual_windows,
|
deserialize_displays_as_individual_windows,
|
||||||
UserDefaultConfig::read("displays_as_individual_windows")
|
UserDefaultConfig::read(keys::OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_use_all_my_displays_for_the_remote_session,
|
default_use_all_my_displays_for_the_remote_session,
|
||||||
deserialize_use_all_my_displays_for_the_remote_session,
|
deserialize_use_all_my_displays_for_the_remote_session,
|
||||||
UserDefaultConfig::read("use_all_my_displays_for_the_remote_session")
|
UserDefaultConfig::read(keys::OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION)
|
||||||
);
|
);
|
||||||
|
|
||||||
fn default_custom_image_quality() -> Vec<i32> {
|
fn default_custom_image_quality() -> Vec<i32> {
|
||||||
let f: f64 = UserDefaultConfig::read("custom_image_quality")
|
let f: f64 = UserDefaultConfig::read(keys::OPTION_CUSTOM_IMAGE_QUALITY)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(50.0);
|
.unwrap_or(50.0);
|
||||||
vec![f as _]
|
vec![f as _]
|
||||||
@@ -1244,12 +1278,12 @@ impl PeerConfig {
|
|||||||
fn default_options() -> HashMap<String, String> {
|
fn default_options() -> HashMap<String, String> {
|
||||||
let mut mp: HashMap<String, String> = Default::default();
|
let mut mp: HashMap<String, String> = Default::default();
|
||||||
[
|
[
|
||||||
"codec-preference",
|
keys::OPTION_CODEC_PREFERENCE,
|
||||||
"custom-fps",
|
keys::OPTION_CUSTOM_FPS,
|
||||||
"zoom-cursor",
|
keys::OPTION_ZOOM_CURSOR,
|
||||||
"touch-mode",
|
keys::OPTION_TOUCH_MODE,
|
||||||
"i444",
|
keys::OPTION_I444,
|
||||||
"swap-left-right-mouse",
|
keys::OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||||
]
|
]
|
||||||
.map(|key| {
|
.map(|key| {
|
||||||
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
||||||
@@ -1290,10 +1324,10 @@ serde_field_bool!(
|
|||||||
"DisableAudio::default_disable_audio"
|
"DisableAudio::default_disable_audio"
|
||||||
);
|
);
|
||||||
serde_field_bool!(
|
serde_field_bool!(
|
||||||
EnableFileTransfer,
|
EnableFileCopyPaste,
|
||||||
"enable_file_transfer",
|
"enable-file-copy-paste",
|
||||||
default_enable_file_transfer,
|
default_enable_file_copy_paste,
|
||||||
"EnableFileTransfer::default_enable_file_transfer"
|
"EnableFileCopyPaste::default_enable_file_copy_paste"
|
||||||
);
|
);
|
||||||
serde_field_bool!(
|
serde_field_bool!(
|
||||||
DisableClipboard,
|
DisableClipboard,
|
||||||
@@ -1415,10 +1449,17 @@ impl LocalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &DEFAULT_LOCAL_SETTINGS, &k, &v) {
|
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||||
|
// The custom client will explictly set "default" as the default language.
|
||||||
|
let is_custom_client_default_lang = k == keys::OPTION_LANGUAGE && v == "default";
|
||||||
|
if is_custom_client_default_lang {
|
||||||
|
config.options.insert(k, "".to_owned());
|
||||||
|
config.store();
|
||||||
|
return;
|
||||||
|
}
|
||||||
let v2 = if v.is_empty() { None } else { Some(&v) };
|
let v2 = if v.is_empty() { None } else { Some(&v) };
|
||||||
if v2 != config.options.get(&k) {
|
if v2 != config.options.get(&k) {
|
||||||
if v2.is_none() {
|
if v2.is_none() {
|
||||||
@@ -1431,11 +1472,13 @@ impl LocalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_flutter_option(k: &str) -> String {
|
pub fn get_flutter_option(k: &str) -> String {
|
||||||
if let Some(v) = LOCAL_CONFIG.read().unwrap().ui_flutter.get(k) {
|
get_or(
|
||||||
v.clone()
|
&OVERWRITE_LOCAL_SETTINGS,
|
||||||
} else {
|
&LOCAL_CONFIG.read().unwrap().ui_flutter,
|
||||||
"".to_owned()
|
&DEFAULT_LOCAL_SETTINGS,
|
||||||
}
|
k,
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_flutter_option(k: String, v: String) {
|
pub fn set_flutter_option(k: String, v: String) {
|
||||||
@@ -1572,15 +1615,19 @@ impl UserDefaultConfig {
|
|||||||
|
|
||||||
pub fn get(&self, key: &str) -> String {
|
pub fn get(&self, key: &str) -> String {
|
||||||
match key {
|
match key {
|
||||||
"view_style" => self.get_string(key, "original", vec!["adaptive"]),
|
keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]),
|
||||||
"scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||||
"image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]),
|
keys::OPTION_IMAGE_QUALITY => {
|
||||||
"codec-preference" => {
|
self.get_string(key, "balanced", vec!["best", "low", "custom"])
|
||||||
|
}
|
||||||
|
keys::OPTION_CODEC_PREFERENCE => {
|
||||||
self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"])
|
self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"])
|
||||||
}
|
}
|
||||||
"custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64),
|
keys::OPTION_CUSTOM_IMAGE_QUALITY => {
|
||||||
"custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
|
self.get_double_string(key, 50.0, 10.0, 0xFFF as f64)
|
||||||
"enable_file_transfer" => self.get_string(key, "Y", vec![""]),
|
}
|
||||||
|
keys::OPTION_CUSTOM_FPS => self.get_double_string(key, 30.0, 5.0, 120.0),
|
||||||
|
keys::OPTION_ENABLE_FILE_COPY_PASTE => self.get_string(key, "Y", vec!["", "N"]),
|
||||||
_ => self
|
_ => self
|
||||||
.get_after(key)
|
.get_after(key)
|
||||||
.map(|v| v.to_string())
|
.map(|v| v.to_string())
|
||||||
@@ -1589,12 +1636,7 @@ impl UserDefaultConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, key: String, value: String) {
|
pub fn set(&mut self, key: String, value: String) {
|
||||||
if !is_option_can_save(
|
if !is_option_can_save(&OVERWRITE_DISPLAY_SETTINGS, &key) {
|
||||||
&OVERWRITE_DISPLAY_SETTINGS,
|
|
||||||
&DEFAULT_DISPLAY_SETTINGS,
|
|
||||||
&key,
|
|
||||||
&value,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
@@ -1917,18 +1959,10 @@ fn get_or(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn is_option_can_save(
|
fn is_option_can_save(overwrite: &RwLock<HashMap<String, String>>, k: &str) -> bool {
|
||||||
overwrite: &RwLock<HashMap<String, String>>,
|
|
||||||
defaults: &RwLock<HashMap<String, String>>,
|
|
||||||
k: &str,
|
|
||||||
v: &str,
|
|
||||||
) -> bool {
|
|
||||||
if overwrite.read().unwrap().contains_key(k) {
|
if overwrite.read().unwrap().contains_key(k) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if defaults.read().unwrap().get(k).map_or(false, |x| x == v) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1984,6 +2018,170 @@ pub fn is_disable_installation() -> bool {
|
|||||||
is_some_hard_opton("disable-installation")
|
is_some_hard_opton("disable-installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod keys {
|
||||||
|
pub const OPTION_VIEW_ONLY: &str = "view_only";
|
||||||
|
pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar";
|
||||||
|
pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_toolbar";
|
||||||
|
pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor";
|
||||||
|
pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor";
|
||||||
|
pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window";
|
||||||
|
pub const OPTION_ZOOM_CURSOR: &str = "zoom-cursor";
|
||||||
|
pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor";
|
||||||
|
pub const OPTION_DISABLE_AUDIO: &str = "disable_audio";
|
||||||
|
pub const OPTION_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste";
|
||||||
|
pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard";
|
||||||
|
pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end";
|
||||||
|
pub const OPTION_PRIVACY_MODE: &str = "privacy_mode";
|
||||||
|
pub const OPTION_TOUCH_MODE: &str = "touch-mode";
|
||||||
|
pub const OPTION_I444: &str = "i444";
|
||||||
|
pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel";
|
||||||
|
pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse";
|
||||||
|
pub const OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS: &str = "displays_as_individual_windows";
|
||||||
|
pub const OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION: &str =
|
||||||
|
"use_all_my_displays_for_the_remote_session";
|
||||||
|
pub const OPTION_VIEW_STYLE: &str = "view_style";
|
||||||
|
pub const OPTION_SCROLL_STYLE: &str = "scroll_style";
|
||||||
|
pub const OPTION_IMAGE_QUALITY: &str = "image_quality";
|
||||||
|
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
|
||||||
|
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
|
||||||
|
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference";
|
||||||
|
pub const OPTION_THEME: &str = "theme";
|
||||||
|
pub const OPTION_LANGUAGE: &str = "lang";
|
||||||
|
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
|
||||||
|
pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right";
|
||||||
|
pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel";
|
||||||
|
pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs";
|
||||||
|
pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str =
|
||||||
|
"enable-open-new-connections-in-tabs";
|
||||||
|
pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render";
|
||||||
|
pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update";
|
||||||
|
pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions";
|
||||||
|
pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags";
|
||||||
|
pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection";
|
||||||
|
pub const OPTION_ACCESS_MODE: &str = "access-mode";
|
||||||
|
pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard";
|
||||||
|
pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard";
|
||||||
|
pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer";
|
||||||
|
pub const OPTION_ENABLE_AUDIO: &str = "enable-audio";
|
||||||
|
pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel";
|
||||||
|
pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart";
|
||||||
|
pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session";
|
||||||
|
pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input";
|
||||||
|
pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification";
|
||||||
|
pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery";
|
||||||
|
pub const OPTION_DIRECT_SERVER: &str = "direct-server";
|
||||||
|
pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port";
|
||||||
|
pub const OPTION_WHITELIST: &str = "whitelist";
|
||||||
|
pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect";
|
||||||
|
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
||||||
|
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
||||||
|
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
||||||
|
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
||||||
|
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
||||||
|
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
||||||
|
pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render";
|
||||||
|
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
||||||
|
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
|
||||||
|
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
|
||||||
|
|
||||||
|
// flutter local options
|
||||||
|
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||||
|
pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting";
|
||||||
|
pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index";
|
||||||
|
pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order";
|
||||||
|
pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible";
|
||||||
|
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
|
||||||
|
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
|
||||||
|
|
||||||
|
// proxy settings
|
||||||
|
// The following options are not real keys, they are just used for custom client advanced settings.
|
||||||
|
// The real keys are in Config2::socks.
|
||||||
|
pub const OPTION_PROXY_URL: &str = "proxy-url";
|
||||||
|
pub const OPTION_PROXY_USERNAME: &str = "proxy-username";
|
||||||
|
pub const OPTION_PROXY_PASSWORD: &str = "proxy-password";
|
||||||
|
|
||||||
|
// DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS
|
||||||
|
pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_VIEW_ONLY,
|
||||||
|
OPTION_SHOW_MONITORS_TOOLBAR,
|
||||||
|
OPTION_COLLAPSE_TOOLBAR,
|
||||||
|
OPTION_SHOW_REMOTE_CURSOR,
|
||||||
|
OPTION_FOLLOW_REMOTE_CURSOR,
|
||||||
|
OPTION_FOLLOW_REMOTE_WINDOW,
|
||||||
|
OPTION_ZOOM_CURSOR,
|
||||||
|
OPTION_SHOW_QUALITY_MONITOR,
|
||||||
|
OPTION_DISABLE_AUDIO,
|
||||||
|
OPTION_ENABLE_FILE_COPY_PASTE,
|
||||||
|
OPTION_DISABLE_CLIPBOARD,
|
||||||
|
OPTION_LOCK_AFTER_SESSION_END,
|
||||||
|
OPTION_PRIVACY_MODE,
|
||||||
|
OPTION_TOUCH_MODE,
|
||||||
|
OPTION_I444,
|
||||||
|
OPTION_REVERSE_MOUSE_WHEEL,
|
||||||
|
OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||||
|
OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS,
|
||||||
|
OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION,
|
||||||
|
OPTION_VIEW_STYLE,
|
||||||
|
OPTION_SCROLL_STYLE,
|
||||||
|
OPTION_IMAGE_QUALITY,
|
||||||
|
OPTION_CUSTOM_IMAGE_QUALITY,
|
||||||
|
OPTION_CUSTOM_FPS,
|
||||||
|
OPTION_CODEC_PREFERENCE,
|
||||||
|
];
|
||||||
|
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
|
||||||
|
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_THEME,
|
||||||
|
OPTION_LANGUAGE,
|
||||||
|
OPTION_ENABLE_CONFIRM_CLOSING_TABS,
|
||||||
|
OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS,
|
||||||
|
OPTION_TEXTURE_RENDER,
|
||||||
|
OPTION_SYNC_AB_WITH_RECENT_SESSIONS,
|
||||||
|
OPTION_SYNC_AB_TAGS,
|
||||||
|
OPTION_FILTER_AB_BY_INTERSECTION,
|
||||||
|
OPTION_REMOTE_MENUBAR_DRAG_LEFT,
|
||||||
|
OPTION_REMOTE_MENUBAR_DRAG_RIGHT,
|
||||||
|
OPTION_HIDE_AB_TAGS_PANEL,
|
||||||
|
OPTION_FLUTTER_REMOTE_MENUBAR_STATE,
|
||||||
|
OPTION_FLUTTER_PEER_SORTING,
|
||||||
|
OPTION_FLUTTER_PEER_TAB_INDEX,
|
||||||
|
OPTION_FLUTTER_PEER_TAB_ORDER,
|
||||||
|
OPTION_FLUTTER_PEER_TAB_VISIBLE,
|
||||||
|
OPTION_FLUTTER_PEER_CARD_UI_TYLE,
|
||||||
|
OPTION_FLUTTER_CURRENT_AB_NAME,
|
||||||
|
];
|
||||||
|
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||||
|
pub const KEYS_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_ACCESS_MODE,
|
||||||
|
OPTION_ENABLE_KEYBOARD,
|
||||||
|
OPTION_ENABLE_CLIPBOARD,
|
||||||
|
OPTION_ENABLE_FILE_TRANSFER,
|
||||||
|
OPTION_ENABLE_AUDIO,
|
||||||
|
OPTION_ENABLE_TUNNEL,
|
||||||
|
OPTION_ENABLE_REMOTE_RESTART,
|
||||||
|
OPTION_ENABLE_RECORD_SESSION,
|
||||||
|
OPTION_ENABLE_BLOCK_INPUT,
|
||||||
|
OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION,
|
||||||
|
OPTION_ENABLE_LAN_DISCOVERY,
|
||||||
|
OPTION_DIRECT_SERVER,
|
||||||
|
OPTION_DIRECT_ACCESS_PORT,
|
||||||
|
OPTION_WHITELIST,
|
||||||
|
OPTION_ALLOW_AUTO_DISCONNECT,
|
||||||
|
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
||||||
|
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
||||||
|
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
||||||
|
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||||
|
OPTION_ENABLE_ABR,
|
||||||
|
OPTION_ALLOW_REMOVE_WALLPAPER,
|
||||||
|
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
||||||
|
OPTION_ALLOW_LINUX_HEADLESS,
|
||||||
|
OPTION_ENABLE_HWCODEC,
|
||||||
|
OPTION_APPROVE_MODE,
|
||||||
|
OPTION_PROXY_URL,
|
||||||
|
OPTION_PROXY_USERNAME,
|
||||||
|
OPTION_PROXY_PASSWORD,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -2022,6 +2220,10 @@ mod tests {
|
|||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert("b".to_string(), "c".to_string());
|
.insert("b".to_string(), "c".to_string());
|
||||||
|
OVERWRITE_SETTINGS
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert("c".to_string(), "f".to_string());
|
||||||
OVERWRITE_SETTINGS
|
OVERWRITE_SETTINGS
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -2042,7 +2244,7 @@ mod tests {
|
|||||||
res.insert("d".to_owned(), "c".to_string());
|
res.insert("d".to_owned(), "c".to_string());
|
||||||
res.insert("c".to_owned(), "a".to_string());
|
res.insert("c".to_owned(), "a".to_string());
|
||||||
res.insert("f".to_owned(), "a".to_string());
|
res.insert("f".to_owned(), "a".to_string());
|
||||||
res.insert("c".to_owned(), "d".to_string());
|
res.insert("e".to_owned(), "d".to_string());
|
||||||
Config::purify_options(&mut res);
|
Config::purify_options(&mut res);
|
||||||
assert!(res.len() == 2);
|
assert!(res.len() == 2);
|
||||||
res.insert("b".to_owned(), "c".to_string());
|
res.insert("b".to_owned(), "c".to_string());
|
||||||
@@ -2055,11 +2257,11 @@ mod tests {
|
|||||||
assert!(res.len() == 2);
|
assert!(res.len() == 2);
|
||||||
let res = Config::get_options();
|
let res = Config::get_options();
|
||||||
assert!(res["a"] == "b");
|
assert!(res["a"] == "b");
|
||||||
assert!(res["c"] == "a");
|
assert!(res["c"] == "f");
|
||||||
assert!(res["b"] == "c");
|
assert!(res["b"] == "c");
|
||||||
assert!(res["d"] == "c");
|
assert!(res["d"] == "c");
|
||||||
assert!(Config::get_option("a") == "b");
|
assert!(Config::get_option("a") == "b");
|
||||||
assert!(Config::get_option("c") == "a");
|
assert!(Config::get_option("c") == "f");
|
||||||
assert!(Config::get_option("b") == "c");
|
assert!(Config::get_option("b") == "c");
|
||||||
assert!(Config::get_option("d") == "c");
|
assert!(Config::get_option("d") == "c");
|
||||||
DEFAULT_SETTINGS.write().unwrap().clear();
|
DEFAULT_SETTINGS.write().unwrap().clear();
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ pub mod udp;
|
|||||||
pub use env_logger;
|
pub use env_logger;
|
||||||
pub use log;
|
pub use log;
|
||||||
pub mod bytes_codec;
|
pub mod bytes_codec;
|
||||||
#[cfg(feature = "quic")]
|
|
||||||
pub mod quic;
|
|
||||||
pub use anyhow::{self, bail};
|
pub use anyhow::{self, bail};
|
||||||
pub use futures_util;
|
pub use futures_util;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
@@ -43,6 +41,7 @@ pub use chrono;
|
|||||||
pub use directories_next;
|
pub use directories_next;
|
||||||
pub use libc;
|
pub use libc;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
|
pub use base64;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub use dlopen;
|
pub use dlopen;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
@@ -50,14 +49,10 @@ pub use machine_uid;
|
|||||||
pub use serde_derive;
|
pub use serde_derive;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
pub use sysinfo;
|
pub use sysinfo;
|
||||||
|
pub use thiserror;
|
||||||
pub use toml;
|
pub use toml;
|
||||||
pub use uuid;
|
pub use uuid;
|
||||||
pub use base64;
|
|
||||||
pub use thiserror;
|
|
||||||
|
|
||||||
#[cfg(feature = "quic")]
|
|
||||||
pub type Stream = quic::Connection;
|
|
||||||
#[cfg(not(feature = "quic"))]
|
|
||||||
pub type Stream = tcp::FramedStream;
|
pub type Stream = tcp::FramedStream;
|
||||||
pub type SessionID = uuid::Uuid;
|
pub type SessionID = uuid::Uuid;
|
||||||
|
|
||||||
@@ -389,7 +384,7 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
|
|||||||
.rotate(
|
.rotate(
|
||||||
Criterion::Age(Age::Day),
|
Criterion::Age(Age::Day),
|
||||||
Naming::Timestamps,
|
Naming::Timestamps,
|
||||||
Cleanup::KeepLogFiles(6),
|
Cleanup::KeepLogFiles(31),
|
||||||
)
|
)
|
||||||
.start()
|
.start()
|
||||||
.ok();
|
.ok();
|
||||||
|
|||||||
@@ -142,9 +142,17 @@ pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
|||||||
_get_values_of_seat0(indices, false)
|
_get_values_of_seat0(indices, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore "3 sessions listed."
|
||||||
|
fn ignore_loginctl_line(line: &str) -> bool {
|
||||||
|
line.contains("sessions") || line.split(" ").count() < 4
|
||||||
|
}
|
||||||
|
|
||||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||||
if let Ok(output) = run_loginctl(None) {
|
if let Ok(output) = run_loginctl(None) {
|
||||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||||
|
if ignore_loginctl_line(line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if line.contains("seat0") {
|
if line.contains("seat0") {
|
||||||
if let Some(sid) = line.split_whitespace().next() {
|
if let Some(sid) = line.split_whitespace().next() {
|
||||||
if is_active(sid) {
|
if is_active(sid) {
|
||||||
@@ -163,6 +171,9 @@ fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<Stri
|
|||||||
|
|
||||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||||
|
if ignore_loginctl_line(line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(sid) = line.split_whitespace().next() {
|
if let Some(sid) = line.split_whitespace().next() {
|
||||||
if is_active(sid) {
|
if is_active(sid) {
|
||||||
let d = get_display_server_of_session(sid);
|
let d = get_display_server_of_session(sid);
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
use crate::{allow_err, anyhow::anyhow, ResultType};
|
|
||||||
use protobuf::Message;
|
|
||||||
use std::{net::SocketAddr, sync::Arc};
|
|
||||||
use tokio::{self, stream::StreamExt, sync::mpsc};
|
|
||||||
|
|
||||||
const QUIC_HBB: &[&[u8]] = &[b"hbb"];
|
|
||||||
const SERVER_NAME: &str = "hbb";
|
|
||||||
|
|
||||||
type Sender = mpsc::UnboundedSender<Value>;
|
|
||||||
type Receiver = mpsc::UnboundedReceiver<Value>;
|
|
||||||
|
|
||||||
pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> {
|
|
||||||
let mut transport_config = quinn::TransportConfig::default();
|
|
||||||
transport_config.stream_window_uni(0);
|
|
||||||
let mut server_config = quinn::ServerConfig::default();
|
|
||||||
server_config.transport = Arc::new(transport_config);
|
|
||||||
let mut server_config = quinn::ServerConfigBuilder::new(server_config);
|
|
||||||
server_config.protocols(QUIC_HBB);
|
|
||||||
// server_config.enable_keylog();
|
|
||||||
// server_config.use_stateless_retry(true);
|
|
||||||
let mut endpoint = quinn::Endpoint::builder();
|
|
||||||
endpoint.listen(server_config.build());
|
|
||||||
let (end, incoming) = endpoint.with_socket(socket)?;
|
|
||||||
Ok((Server { incoming }, end.local_addr()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType<Connection> {
|
|
||||||
let mut endpoint = quinn::Endpoint::builder();
|
|
||||||
let mut client_config = quinn::ClientConfigBuilder::default();
|
|
||||||
client_config.protocols(QUIC_HBB);
|
|
||||||
//client_config.enable_keylog();
|
|
||||||
endpoint.default_client_config(client_config.build());
|
|
||||||
let (endpoint, _) = endpoint.bind(local_addr)?;
|
|
||||||
let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?;
|
|
||||||
Connection::new_for_client(new_conn.connection).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Server {
|
|
||||||
incoming: quinn::Incoming,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
#[inline]
|
|
||||||
pub async fn next(&mut self) -> ResultType<Option<Connection>> {
|
|
||||||
Connection::new_for_server(&mut self.incoming).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Connection {
|
|
||||||
conn: quinn::Connection,
|
|
||||||
tx: quinn::SendStream,
|
|
||||||
rx: Receiver,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Value = ResultType<Vec<u8>>;
|
|
||||||
|
|
||||||
impl Connection {
|
|
||||||
async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType<Option<Self>> {
|
|
||||||
if let Some(conn) = incoming.next().await {
|
|
||||||
let quinn::NewConnection {
|
|
||||||
connection: conn,
|
|
||||||
// uni_streams,
|
|
||||||
mut bi_streams,
|
|
||||||
..
|
|
||||||
} = conn.await?;
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel::<Value>();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let stream = bi_streams.next().await;
|
|
||||||
if let Some(stream) = stream {
|
|
||||||
let stream = match stream {
|
|
||||||
Err(e) => {
|
|
||||||
tx.send(Err(e.into())).ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(s) => s,
|
|
||||||
};
|
|
||||||
let cloned = tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
allow_err!(handle_request(stream.1, cloned).await);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tx.send(Err(anyhow!("Reset by the peer"))).ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::info!("Exit connection outer loop");
|
|
||||||
});
|
|
||||||
let tx = conn.open_uni().await?;
|
|
||||||
Ok(Some(Self { conn, tx, rx }))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn new_for_client(conn: quinn::Connection) -> ResultType<Self> {
|
|
||||||
let (tx, rx_quic) = conn.open_bi().await?;
|
|
||||||
let (tx_mpsc, rx) = mpsc::unbounded_channel::<Value>();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
allow_err!(handle_request(rx_quic, tx_mpsc).await);
|
|
||||||
});
|
|
||||||
Ok(Self { conn, tx, rx })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn next(&mut self) -> Option<Value> {
|
|
||||||
// None is returned when all Sender halves have dropped,
|
|
||||||
// indicating that no further values can be sent on the channel.
|
|
||||||
self.rx.recv().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn remote_address(&self) -> SocketAddr {
|
|
||||||
self.conn.remote_address()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> {
|
|
||||||
self.tx.write_all(bytes).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> {
|
|
||||||
match msg.write_to_bytes() {
|
|
||||||
Ok(bytes) => self.send_raw(&bytes).await?,
|
|
||||||
err => allow_err!(err),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ mod hw {
|
|||||||
let mut encoder = HwRamEncoder::new(
|
let mut encoder = HwRamEncoder::new(
|
||||||
EncoderCfg::HWRAM(HwRamEncoderConfig {
|
EncoderCfg::HWRAM(HwRamEncoderConfig {
|
||||||
name: info.name.clone(),
|
name: info.name.clone(),
|
||||||
|
mc_name: None,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use jni::{
|
|||||||
|
|
||||||
use jni::errors::{Error as JniError, Result as JniResult};
|
use jni::errors::{Error as JniError, Result as JniResult};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use serde::Deserialize;
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
|
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
|
||||||
use std::sync::{Mutex, RwLock};
|
use std::sync::{Mutex, RwLock};
|
||||||
@@ -20,6 +21,7 @@ lazy_static! {
|
|||||||
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
|
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 AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
|
||||||
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
|
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
|
||||||
|
static ref MEDIA_CODEC_INFOS: RwLock<Option<MediaCodecInfos>> = RwLock::new(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
|
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
@@ -63,7 +65,7 @@ impl FrameRaw {
|
|||||||
|
|
||||||
// take inner data as slice
|
// take inner data as slice
|
||||||
// release when success
|
// release when success
|
||||||
fn take<'a>(&mut self) -> Option<&'a [u8]> {
|
fn take<'a>(&mut self, dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||||
if self.enable.not() {
|
if self.enable.not() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,14 @@ impl FrameRaw {
|
|||||||
}
|
}
|
||||||
let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) };
|
let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) };
|
||||||
self.release();
|
self.release();
|
||||||
Some(slice)
|
if last.len() == slice.len() && crate::would_block_if_equal(last, slice).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
dst.resize(slice.len(), 0);
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(slice.as_ptr(), dst.as_mut_ptr(), slice.len());
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +96,12 @@ impl FrameRaw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_video_raw<'a>() -> Option<&'a [u8]> {
|
pub fn get_video_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||||
VIDEO_RAW.lock().ok()?.take()
|
VIDEO_RAW.lock().ok()?.take(dst, last)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_audio_raw<'a>() -> Option<&'a [u8]> {
|
pub fn get_audio_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
|
||||||
AUDIO_RAW.lock().ok()?.take()
|
AUDIO_RAW.lock().ok()?.take(dst, last)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -154,6 +163,52 @@ pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct MediaCodecInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub is_encoder: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hw: Option<bool>, // api 29+
|
||||||
|
pub mime_type: String,
|
||||||
|
pub surface: bool,
|
||||||
|
pub nv12: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub low_latency: Option<bool>, // api 30+, decoder
|
||||||
|
pub min_bitrate: u32,
|
||||||
|
pub max_bitrate: u32,
|
||||||
|
pub min_width: usize,
|
||||||
|
pub max_width: usize,
|
||||||
|
pub min_height: usize,
|
||||||
|
pub max_height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct MediaCodecInfos {
|
||||||
|
pub version: usize,
|
||||||
|
pub w: usize, // aligned
|
||||||
|
pub h: usize, // aligned
|
||||||
|
pub codecs: Vec<MediaCodecInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_ffi_FFI_setCodecInfo(env: JNIEnv, _class: JClass, info: JString) {
|
||||||
|
let mut env = env;
|
||||||
|
if let Ok(info) = env.get_string(&info) {
|
||||||
|
let info: String = info.into();
|
||||||
|
if let Ok(infos) = serde_json::from_str::<MediaCodecInfos>(&info) {
|
||||||
|
*MEDIA_CODEC_INFOS.write().unwrap() = Some(infos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_codec_info() -> Option<MediaCodecInfos> {
|
||||||
|
MEDIA_CODEC_INFOS.read().unwrap().as_ref().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_codec_info() {
|
||||||
|
*MEDIA_CODEC_INFOS.write().unwrap() = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
|
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
|
||||||
if let (Some(jvm), Some(ctx)) = (
|
if let (Some(jvm), Some(ctx)) = (
|
||||||
JVM.read().unwrap().as_ref(),
|
JVM.read().unwrap().as_ref(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::sync::Mutex;
|
|||||||
use std::{io, time::Duration};
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale)
|
pub(crate) static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Capturer {
|
pub struct Capturer {
|
||||||
@@ -36,13 +36,7 @@ impl Capturer {
|
|||||||
|
|
||||||
impl crate::TraitCapturer for Capturer {
|
impl crate::TraitCapturer for Capturer {
|
||||||
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
||||||
if let Some(buf) = get_video_raw() {
|
if get_video_raw(&mut self.rgba, &mut self.saved_raw_data).is_some() {
|
||||||
crate::would_block_if_equal(&mut self.saved_raw_data, buf)?;
|
|
||||||
// Is it safe to directly return buf without copy?
|
|
||||||
self.rgba.resize(buf.len(), 0);
|
|
||||||
unsafe {
|
|
||||||
std::ptr::copy_nonoverlapping(buf.as_ptr(), self.rgba.as_mut_ptr(), buf.len())
|
|
||||||
};
|
|
||||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||||
&self.rgba,
|
&self.rgba,
|
||||||
self.width(),
|
self.width(),
|
||||||
|
|||||||
@@ -296,6 +296,18 @@ impl EncoderApi for AomEncoder {
|
|||||||
fn support_abr(&self) -> bool {
|
fn support_abr(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn support_changing_quality(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latency_free(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hardware(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AomEncoder {
|
impl AomEncoder {
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ pub trait EncoderApi {
|
|||||||
fn bitrate(&self) -> u32;
|
fn bitrate(&self) -> u32;
|
||||||
|
|
||||||
fn support_abr(&self) -> bool;
|
fn support_abr(&self) -> bool;
|
||||||
|
|
||||||
|
fn support_changing_quality(&self) -> bool;
|
||||||
|
|
||||||
|
fn latency_free(&self) -> bool;
|
||||||
|
|
||||||
|
fn is_hardware(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Encoder {
|
pub struct Encoder {
|
||||||
@@ -138,6 +144,9 @@ impl Encoder {
|
|||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("new hw encoder failed: {e:?}, clear config");
|
log::error!("new hw encoder failed: {e:?}, clear config");
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
crate::android::ffi::clear_codec_info();
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
hbb_common::config::HwCodecConfig::clear_ram();
|
hbb_common::config::HwCodecConfig::clear_ram();
|
||||||
Self::update(EncodingUpdate::Check);
|
Self::update(EncodingUpdate::Check);
|
||||||
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
|
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
|
||||||
@@ -260,14 +269,22 @@ impl Encoder {
|
|||||||
.unwrap_or((PreferCodec::Auto.into(), 0));
|
.unwrap_or((PreferCodec::Auto.into(), 0));
|
||||||
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
|
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
// auto: h265 > h264 > vp9/vp8
|
||||||
let mut auto_codec = CodecFormat::VP9;
|
let mut auto_codec = CodecFormat::VP9;
|
||||||
|
if h264_useable {
|
||||||
|
auto_codec = CodecFormat::H264;
|
||||||
|
}
|
||||||
|
if h265_useable {
|
||||||
|
auto_codec = CodecFormat::H265;
|
||||||
|
}
|
||||||
|
if auto_codec == CodecFormat::VP9 {
|
||||||
let mut system = System::new();
|
let mut system = System::new();
|
||||||
system.refresh_memory();
|
system.refresh_memory();
|
||||||
if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 {
|
if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 {
|
||||||
// 4 Gb
|
// 4 Gb
|
||||||
auto_codec = CodecFormat::VP8
|
auto_codec = CodecFormat::VP8
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
*format = match preference {
|
*format = match preference {
|
||||||
PreferCodec::VP8 => CodecFormat::VP8,
|
PreferCodec::VP8 => CodecFormat::VP8,
|
||||||
@@ -346,7 +363,14 @@ impl Encoder {
|
|||||||
EncoderCfg::AOM(_) => CodecFormat::AV1,
|
EncoderCfg::AOM(_) => CodecFormat::AV1,
|
||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
EncoderCfg::HWRAM(hw) => {
|
EncoderCfg::HWRAM(hw) => {
|
||||||
if hw.name.to_lowercase().contains("h264") {
|
let name = hw.name.to_lowercase();
|
||||||
|
if name.contains("vp8") {
|
||||||
|
CodecFormat::VP8
|
||||||
|
} else if name.contains("vp9") {
|
||||||
|
CodecFormat::VP9
|
||||||
|
} else if name.contains("av1") {
|
||||||
|
CodecFormat::AV1
|
||||||
|
} else if name.contains("h264") {
|
||||||
CodecFormat::H264
|
CodecFormat::H264
|
||||||
} else {
|
} else {
|
||||||
CodecFormat::H265
|
CodecFormat::H265
|
||||||
@@ -395,7 +419,7 @@ impl Encoder {
|
|||||||
impl Decoder {
|
impl Decoder {
|
||||||
pub fn supported_decodings(
|
pub fn supported_decodings(
|
||||||
id_for_perfer: Option<&str>,
|
id_for_perfer: Option<&str>,
|
||||||
_flutter: bool,
|
_use_texture_render: bool,
|
||||||
_luid: Option<i64>,
|
_luid: Option<i64>,
|
||||||
mark_unsupported: &Vec<CodecFormat>,
|
mark_unsupported: &Vec<CodecFormat>,
|
||||||
) -> SupportedDecoding {
|
) -> SupportedDecoding {
|
||||||
@@ -430,7 +454,7 @@ impl Decoder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
if enable_vram_option() && _flutter {
|
if enable_vram_option() && _use_texture_render {
|
||||||
decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 {
|
decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
@@ -817,7 +841,7 @@ impl Decoder {
|
|||||||
|
|
||||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||||
pub fn enable_hwcodec_option() -> bool {
|
pub fn enable_hwcodec_option() -> bool {
|
||||||
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(feature = "mediacodec") {
|
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android") {
|
||||||
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
|
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
|
||||||
return v != "N";
|
return v != "N";
|
||||||
}
|
}
|
||||||
@@ -847,6 +871,15 @@ impl Default for Quality {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Quality {
|
||||||
|
pub fn is_custom(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Quality::Custom(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn base_bitrate(width: u32, height: u32) -> u32 {
|
pub fn base_bitrate(width: u32, height: u32) -> u32 {
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut base_bitrate = ((width * height) / 1000) as u32; // same as 1.1.9
|
let mut base_bitrate = ((width * height) / 1000) as u32; // same as 1.1.9
|
||||||
|
|||||||
@@ -13,186 +13,6 @@ use hbb_common::{bail, log, ResultType};
|
|||||||
|
|
||||||
generate_call_macro!(call_yuv, false);
|
generate_call_macro!(call_yuv, false);
|
||||||
|
|
||||||
#[cfg(feature = "hwcodec")]
|
|
||||||
pub mod hw {
|
|
||||||
use super::*;
|
|
||||||
use crate::ImageFormat;
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
use hwcodec::{ffmpeg::AVPixelFormat, ffmpeg_ram::ffmpeg_linesize_offset_length};
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn hw_nv12_to(
|
|
||||||
fmt: ImageFormat,
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
src_y: &[u8],
|
|
||||||
src_uv: &[u8],
|
|
||||||
src_stride_y: usize,
|
|
||||||
src_stride_uv: usize,
|
|
||||||
dst: &mut Vec<u8>,
|
|
||||||
i420: &mut Vec<u8>,
|
|
||||||
align: usize,
|
|
||||||
) -> ResultType<()> {
|
|
||||||
let nv12_stride_y = src_stride_y;
|
|
||||||
let nv12_stride_uv = src_stride_uv;
|
|
||||||
if let Ok((linesize_i420, offset_i420, i420_len)) =
|
|
||||||
ffmpeg_linesize_offset_length(AVPixelFormat::AV_PIX_FMT_YUV420P, width, height, align)
|
|
||||||
{
|
|
||||||
dst.resize(width * height * 4, 0);
|
|
||||||
let i420_stride_y = linesize_i420[0];
|
|
||||||
let i420_stride_u = linesize_i420[1];
|
|
||||||
let i420_stride_v = linesize_i420[2];
|
|
||||||
i420.resize(i420_len as _, 0);
|
|
||||||
|
|
||||||
let i420_offset_y = unsafe { i420.as_ptr().add(0) as _ };
|
|
||||||
let i420_offset_u = unsafe { i420.as_ptr().add(offset_i420[0] as _) as _ };
|
|
||||||
let i420_offset_v = unsafe { i420.as_ptr().add(offset_i420[1] as _) as _ };
|
|
||||||
call_yuv!(NV12ToI420(
|
|
||||||
src_y.as_ptr(),
|
|
||||||
nv12_stride_y as _,
|
|
||||||
src_uv.as_ptr(),
|
|
||||||
nv12_stride_uv as _,
|
|
||||||
i420_offset_y,
|
|
||||||
i420_stride_y,
|
|
||||||
i420_offset_u,
|
|
||||||
i420_stride_u,
|
|
||||||
i420_offset_v,
|
|
||||||
i420_stride_v,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
match fmt {
|
|
||||||
ImageFormat::ARGB => {
|
|
||||||
call_yuv!(I420ToARGB(
|
|
||||||
i420_offset_y,
|
|
||||||
i420_stride_y,
|
|
||||||
i420_offset_u,
|
|
||||||
i420_stride_u,
|
|
||||||
i420_offset_v,
|
|
||||||
i420_stride_v,
|
|
||||||
dst.as_mut_ptr(),
|
|
||||||
(width * 4) as _,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
ImageFormat::ABGR => {
|
|
||||||
call_yuv!(I420ToABGR(
|
|
||||||
i420_offset_y,
|
|
||||||
i420_stride_y,
|
|
||||||
i420_offset_u,
|
|
||||||
i420_stride_u,
|
|
||||||
i420_offset_v,
|
|
||||||
i420_stride_v,
|
|
||||||
dst.as_mut_ptr(),
|
|
||||||
(width * 4) as _,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bail!("unsupported image format");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
bail!("get linesize offset failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn hw_nv12_to(
|
|
||||||
fmt: ImageFormat,
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
src_y: &[u8],
|
|
||||||
src_uv: &[u8],
|
|
||||||
src_stride_y: usize,
|
|
||||||
src_stride_uv: usize,
|
|
||||||
dst: &mut Vec<u8>,
|
|
||||||
_i420: &mut Vec<u8>,
|
|
||||||
_align: usize,
|
|
||||||
) -> ResultType<()> {
|
|
||||||
dst.resize(width * height * 4, 0);
|
|
||||||
match fmt {
|
|
||||||
ImageFormat::ARGB => {
|
|
||||||
call_yuv!(NV12ToARGB(
|
|
||||||
src_y.as_ptr(),
|
|
||||||
src_stride_y as _,
|
|
||||||
src_uv.as_ptr(),
|
|
||||||
src_stride_uv as _,
|
|
||||||
dst.as_mut_ptr(),
|
|
||||||
(width * 4) as _,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
ImageFormat::ABGR => {
|
|
||||||
call_yuv!(NV12ToABGR(
|
|
||||||
src_y.as_ptr(),
|
|
||||||
src_stride_y as _,
|
|
||||||
src_uv.as_ptr(),
|
|
||||||
src_stride_uv as _,
|
|
||||||
dst.as_mut_ptr(),
|
|
||||||
(width * 4) as _,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
_ => bail!("unsupported image format"),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hw_i420_to(
|
|
||||||
fmt: ImageFormat,
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
src_y: &[u8],
|
|
||||||
src_u: &[u8],
|
|
||||||
src_v: &[u8],
|
|
||||||
src_stride_y: usize,
|
|
||||||
src_stride_u: usize,
|
|
||||||
src_stride_v: usize,
|
|
||||||
dst: &mut Vec<u8>,
|
|
||||||
) -> ResultType<()> {
|
|
||||||
let src_y = src_y.as_ptr();
|
|
||||||
let src_u = src_u.as_ptr();
|
|
||||||
let src_v = src_v.as_ptr();
|
|
||||||
dst.resize(width * height * 4, 0);
|
|
||||||
match fmt {
|
|
||||||
ImageFormat::ARGB => {
|
|
||||||
call_yuv!(I420ToARGB(
|
|
||||||
src_y,
|
|
||||||
src_stride_y as _,
|
|
||||||
src_u,
|
|
||||||
src_stride_u as _,
|
|
||||||
src_v,
|
|
||||||
src_stride_v as _,
|
|
||||||
dst.as_mut_ptr(),
|
|
||||||
(width * 4) as _,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
ImageFormat::ABGR => {
|
|
||||||
call_yuv!(I420ToABGR(
|
|
||||||
src_y,
|
|
||||||
src_stride_y as _,
|
|
||||||
src_u,
|
|
||||||
src_stride_u as _,
|
|
||||||
src_v,
|
|
||||||
src_stride_v as _,
|
|
||||||
dst.as_mut_ptr(),
|
|
||||||
(width * 4) as _,
|
|
||||||
width as _,
|
|
||||||
height as _,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
_ => bail!("unsupported image format"),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
pub fn convert_to_yuv(
|
pub fn convert_to_yuv(
|
||||||
captured: &PixelBuffer,
|
captured: &PixelBuffer,
|
||||||
@@ -213,6 +33,14 @@ pub fn convert_to_yuv(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if src_pixfmt == crate::Pixfmt::BGRA || src_pixfmt == crate::Pixfmt::RGBA {
|
if src_pixfmt == crate::Pixfmt::BGRA || src_pixfmt == crate::Pixfmt::RGBA {
|
||||||
|
// stride is calculated, not real, so we need to check it
|
||||||
|
if src_stride[0] < src_width * 4 {
|
||||||
|
bail!(
|
||||||
|
"src_stride[0] < src_width * 4: {} < {}",
|
||||||
|
src_stride[0],
|
||||||
|
src_width * 4
|
||||||
|
);
|
||||||
|
}
|
||||||
if src.len() < src_stride[0] * src_height {
|
if src.len() < src_stride[0] * src_height {
|
||||||
bail!(
|
bail!(
|
||||||
"wrong src len, {} < {} * {}",
|
"wrong src len, {} < {} * {}",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use crate::{
|
|||||||
codec::{
|
codec::{
|
||||||
base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q,
|
base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q,
|
||||||
},
|
},
|
||||||
hw, CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN,
|
convert::*,
|
||||||
|
CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN,
|
||||||
};
|
};
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
anyhow::{anyhow, bail, Context},
|
anyhow::{anyhow, bail, Context},
|
||||||
@@ -14,14 +15,16 @@ use hbb_common::{
|
|||||||
serde_json, ResultType,
|
serde_json, ResultType,
|
||||||
};
|
};
|
||||||
use hwcodec::{
|
use hwcodec::{
|
||||||
common::DataFormat,
|
common::{
|
||||||
|
DataFormat,
|
||||||
|
Quality::{self, *},
|
||||||
|
RateControl::{self, *},
|
||||||
|
},
|
||||||
ffmpeg::AVPixelFormat,
|
ffmpeg::AVPixelFormat,
|
||||||
ffmpeg_ram::{
|
ffmpeg_ram::{
|
||||||
decode::{DecodeContext, DecodeFrame, Decoder},
|
decode::{DecodeContext, DecodeFrame, Decoder},
|
||||||
encode::{EncodeContext, EncodeFrame, Encoder},
|
encode::{EncodeContext, EncodeFrame, Encoder},
|
||||||
CodecInfo,
|
ffmpeg_linesize_offset_length, CodecInfo,
|
||||||
Quality::{self, *},
|
|
||||||
RateControl::{self, *},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,11 +32,13 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12;
|
|||||||
pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30];
|
pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30];
|
||||||
const DEFAULT_GOP: i32 = i32::MAX;
|
const DEFAULT_GOP: i32 = i32::MAX;
|
||||||
const DEFAULT_HW_QUALITY: Quality = Quality_Default;
|
const DEFAULT_HW_QUALITY: Quality = Quality_Default;
|
||||||
const DEFAULT_RC: RateControl = RC_DEFAULT;
|
|
||||||
|
crate::generate_call_macro!(call_yuv, false);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HwRamEncoderConfig {
|
pub struct HwRamEncoderConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub mc_name: Option<String>,
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
pub quality: Q,
|
pub quality: Q,
|
||||||
@@ -42,12 +47,10 @@ pub struct HwRamEncoderConfig {
|
|||||||
|
|
||||||
pub struct HwRamEncoder {
|
pub struct HwRamEncoder {
|
||||||
encoder: Encoder,
|
encoder: Encoder,
|
||||||
name: String,
|
|
||||||
pub format: DataFormat,
|
pub format: DataFormat,
|
||||||
pub pixfmt: AVPixelFormat,
|
pub pixfmt: AVPixelFormat,
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
bitrate: u32, //kbs
|
bitrate: u32, //kbs
|
||||||
|
config: HwRamEncoderConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncoderApi for HwRamEncoder {
|
impl EncoderApi for HwRamEncoder {
|
||||||
@@ -57,24 +60,28 @@ impl EncoderApi for HwRamEncoder {
|
|||||||
{
|
{
|
||||||
match cfg {
|
match cfg {
|
||||||
EncoderCfg::HWRAM(config) => {
|
EncoderCfg::HWRAM(config) => {
|
||||||
let b = Self::convert_quality(config.quality);
|
let rc = Self::rate_control(&config);
|
||||||
|
let b = Self::convert_quality(&config.name, config.quality);
|
||||||
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
||||||
let mut bitrate = base_bitrate * b / 100;
|
let mut bitrate = base_bitrate * b / 100;
|
||||||
if base_bitrate <= 0 {
|
if base_bitrate <= 0 {
|
||||||
bitrate = base_bitrate;
|
bitrate = base_bitrate;
|
||||||
}
|
}
|
||||||
|
bitrate = Self::check_bitrate_range(&config, bitrate);
|
||||||
let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32;
|
let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32;
|
||||||
let ctx = EncodeContext {
|
let ctx = EncodeContext {
|
||||||
name: config.name.clone(),
|
name: config.name.clone(),
|
||||||
|
mc_name: config.mc_name.clone(),
|
||||||
width: config.width as _,
|
width: config.width as _,
|
||||||
height: config.height as _,
|
height: config.height as _,
|
||||||
pixfmt: DEFAULT_PIXFMT,
|
pixfmt: DEFAULT_PIXFMT,
|
||||||
align: HW_STRIDE_ALIGN as _,
|
align: HW_STRIDE_ALIGN as _,
|
||||||
bitrate: bitrate as i32 * 1000,
|
kbs: bitrate as i32,
|
||||||
timebase: DEFAULT_TIME_BASE,
|
timebase: DEFAULT_TIME_BASE,
|
||||||
gop,
|
gop,
|
||||||
quality: DEFAULT_HW_QUALITY,
|
quality: DEFAULT_HW_QUALITY,
|
||||||
rc: DEFAULT_RC,
|
rc,
|
||||||
|
q: -1,
|
||||||
thread_count: codec_thread_num(16) as _, // ffmpeg's thread_count is used for cpu
|
thread_count: codec_thread_num(16) as _, // ffmpeg's thread_count is used for cpu
|
||||||
};
|
};
|
||||||
let format = match Encoder::format_from_name(config.name.clone()) {
|
let format = match Encoder::format_from_name(config.name.clone()) {
|
||||||
@@ -89,12 +96,10 @@ impl EncoderApi for HwRamEncoder {
|
|||||||
match Encoder::new(ctx.clone()) {
|
match Encoder::new(ctx.clone()) {
|
||||||
Ok(encoder) => Ok(HwRamEncoder {
|
Ok(encoder) => Ok(HwRamEncoder {
|
||||||
encoder,
|
encoder,
|
||||||
name: config.name,
|
|
||||||
format,
|
format,
|
||||||
pixfmt: ctx.pixfmt,
|
pixfmt: ctx.pixfmt,
|
||||||
width: ctx.width as _,
|
|
||||||
height: ctx.height as _,
|
|
||||||
bitrate,
|
bitrate,
|
||||||
|
config,
|
||||||
}),
|
}),
|
||||||
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
||||||
}
|
}
|
||||||
@@ -166,12 +171,14 @@ impl EncoderApi for HwRamEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> {
|
fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> {
|
||||||
let b = Self::convert_quality(quality);
|
let b = Self::convert_quality(&self.config.name, quality);
|
||||||
let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100;
|
let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100;
|
||||||
if bitrate > 0 {
|
if bitrate > 0 {
|
||||||
self.encoder.set_bitrate((bitrate * 1000) as _).ok();
|
bitrate = Self::check_bitrate_range(&self.config, self.bitrate);
|
||||||
|
self.encoder.set_bitrate(bitrate as _).ok();
|
||||||
self.bitrate = bitrate;
|
self.bitrate = bitrate;
|
||||||
}
|
}
|
||||||
|
self.config.quality = quality;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +187,23 @@ impl EncoderApi for HwRamEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn support_abr(&self) -> bool {
|
fn support_abr(&self) -> bool {
|
||||||
["qsv", "vaapi"].iter().all(|&x| !self.name.contains(x))
|
["qsv", "vaapi", "mediacodec"]
|
||||||
|
.iter()
|
||||||
|
.all(|&x| !self.config.name.contains(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn support_changing_quality(&self) -> bool {
|
||||||
|
["vaapi", "mediacodec"]
|
||||||
|
.iter()
|
||||||
|
.all(|&x| !self.config.name.contains(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latency_free(&self) -> bool {
|
||||||
|
!self.config.name.contains("mediacodec")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hardware(&self) -> bool {
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,14 +240,53 @@ impl HwRamEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_quality(quality: crate::codec::Quality) -> u32 {
|
fn rate_control(_config: &HwRamEncoderConfig) -> RateControl {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
if _config.name.contains("mediacodec") {
|
||||||
|
return RC_VBR;
|
||||||
|
}
|
||||||
|
RC_CBR
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_quality(name: &str, quality: crate::codec::Quality) -> u32 {
|
||||||
use crate::codec::Quality;
|
use crate::codec::Quality;
|
||||||
match quality {
|
let quality = match quality {
|
||||||
Quality::Best => 150,
|
Quality::Best => 150,
|
||||||
Quality::Balanced => 100,
|
Quality::Balanced => 100,
|
||||||
Quality::Low => 50,
|
Quality::Low => 50,
|
||||||
Quality::Custom(b) => b,
|
Quality::Custom(b) => b,
|
||||||
|
};
|
||||||
|
let factor = if name.contains("mediacodec") {
|
||||||
|
// https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3
|
||||||
|
5
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
quality * factor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
if _config.name.contains("mediacodec") {
|
||||||
|
let info = crate::android::ffi::get_codec_info();
|
||||||
|
if let Some(info) = info {
|
||||||
|
if let Some(codec) = info
|
||||||
|
.codecs
|
||||||
|
.iter()
|
||||||
|
.find(|c| Some(c.name.clone()) == _config.mc_name && c.is_encoder)
|
||||||
|
{
|
||||||
|
if codec.max_bitrate > codec.min_bitrate {
|
||||||
|
if bitrate > codec.max_bitrate {
|
||||||
|
return codec.max_bitrate;
|
||||||
|
}
|
||||||
|
if bitrate < codec.min_bitrate {
|
||||||
|
return codec.min_bitrate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bitrate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +347,10 @@ impl HwRamDecoder {
|
|||||||
match Decoder::new(ctx) {
|
match Decoder::new(ctx) {
|
||||||
Ok(decoder) => Ok(HwRamDecoder { decoder, info }),
|
Ok(decoder) => Ok(HwRamDecoder { decoder, info }),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
HwCodecConfig::clear_ram();
|
#[cfg(target_os = "android")]
|
||||||
|
crate::android::ffi::clear_codec_info();
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
hbb_common::config::HwCodecConfig::clear_ram();
|
||||||
Err(anyhow!(format!("Failed to create decoder")))
|
Err(anyhow!(format!("Failed to create decoder")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,54 +371,100 @@ impl HwRamDecoderImage<'_> {
|
|||||||
// rgb [in/out] fmt and stride must be set in ImageRgb
|
// rgb [in/out] fmt and stride must be set in ImageRgb
|
||||||
pub fn to_fmt(&self, rgb: &mut ImageRgb, i420: &mut Vec<u8>) -> ResultType<()> {
|
pub fn to_fmt(&self, rgb: &mut ImageRgb, i420: &mut Vec<u8>) -> ResultType<()> {
|
||||||
let frame = self.frame;
|
let frame = self.frame;
|
||||||
rgb.w = frame.width as _;
|
let width = frame.width;
|
||||||
rgb.h = frame.height as _;
|
let height = frame.height;
|
||||||
// take dst_stride into account when you convert
|
rgb.w = width as _;
|
||||||
let dst_stride = rgb.stride();
|
rgb.h = height as _;
|
||||||
|
let dst_align = rgb.align();
|
||||||
|
let bytes_per_row = (rgb.w * 4 + dst_align - 1) & !(dst_align - 1);
|
||||||
|
rgb.raw.resize(rgb.h * bytes_per_row, 0);
|
||||||
match frame.pixfmt {
|
match frame.pixfmt {
|
||||||
AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to(
|
AVPixelFormat::AV_PIX_FMT_NV12 => {
|
||||||
rgb.fmt(),
|
// I420ToARGB is much faster than NV12ToARGB in tests on Windows
|
||||||
frame.width as _,
|
if cfg!(windows) {
|
||||||
frame.height as _,
|
let Ok((linesize_i420, offset_i420, len_i420)) = ffmpeg_linesize_offset_length(
|
||||||
&frame.data[0],
|
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||||
&frame.data[1],
|
width as _,
|
||||||
frame.linesize[0] as _,
|
height as _,
|
||||||
frame.linesize[1] as _,
|
|
||||||
&mut rgb.raw as _,
|
|
||||||
i420,
|
|
||||||
HW_STRIDE_ALIGN,
|
HW_STRIDE_ALIGN,
|
||||||
)?,
|
) else {
|
||||||
|
bail!("failed to get i420 linesize, offset, length");
|
||||||
|
};
|
||||||
|
i420.resize(len_i420 as _, 0);
|
||||||
|
let i420_offset_y = unsafe { i420.as_ptr().add(0) as _ };
|
||||||
|
let i420_offset_u = unsafe { i420.as_ptr().add(offset_i420[0] as _) as _ };
|
||||||
|
let i420_offset_v = unsafe { i420.as_ptr().add(offset_i420[1] as _) as _ };
|
||||||
|
call_yuv!(NV12ToI420(
|
||||||
|
frame.data[0].as_ptr(),
|
||||||
|
frame.linesize[0],
|
||||||
|
frame.data[1].as_ptr(),
|
||||||
|
frame.linesize[1],
|
||||||
|
i420_offset_y,
|
||||||
|
linesize_i420[0],
|
||||||
|
i420_offset_u,
|
||||||
|
linesize_i420[1],
|
||||||
|
i420_offset_v,
|
||||||
|
linesize_i420[2],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
));
|
||||||
|
let f = match rgb.fmt() {
|
||||||
|
ImageFormat::ARGB => I420ToARGB,
|
||||||
|
ImageFormat::ABGR => I420ToABGR,
|
||||||
|
_ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()),
|
||||||
|
};
|
||||||
|
call_yuv!(f(
|
||||||
|
i420_offset_y,
|
||||||
|
linesize_i420[0],
|
||||||
|
i420_offset_u,
|
||||||
|
linesize_i420[1],
|
||||||
|
i420_offset_v,
|
||||||
|
linesize_i420[2],
|
||||||
|
rgb.raw.as_mut_ptr(),
|
||||||
|
bytes_per_row as _,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let f = match rgb.fmt() {
|
||||||
|
ImageFormat::ARGB => NV12ToARGB,
|
||||||
|
ImageFormat::ABGR => NV12ToABGR,
|
||||||
|
_ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()),
|
||||||
|
};
|
||||||
|
call_yuv!(f(
|
||||||
|
frame.data[0].as_ptr(),
|
||||||
|
frame.linesize[0],
|
||||||
|
frame.data[1].as_ptr(),
|
||||||
|
frame.linesize[1],
|
||||||
|
rgb.raw.as_mut_ptr(),
|
||||||
|
bytes_per_row as _,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
AVPixelFormat::AV_PIX_FMT_YUV420P => {
|
AVPixelFormat::AV_PIX_FMT_YUV420P => {
|
||||||
hw::hw_i420_to(
|
let f = match rgb.fmt() {
|
||||||
rgb.fmt(),
|
ImageFormat::ARGB => I420ToARGB,
|
||||||
frame.width as _,
|
ImageFormat::ABGR => I420ToABGR,
|
||||||
frame.height as _,
|
_ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()),
|
||||||
&frame.data[0],
|
};
|
||||||
&frame.data[1],
|
call_yuv!(f(
|
||||||
&frame.data[2],
|
frame.data[0].as_ptr(),
|
||||||
frame.linesize[0] as _,
|
frame.linesize[0],
|
||||||
frame.linesize[1] as _,
|
frame.data[1].as_ptr(),
|
||||||
frame.linesize[2] as _,
|
frame.linesize[1],
|
||||||
&mut rgb.raw as _,
|
frame.data[2].as_ptr(),
|
||||||
)?;
|
frame.linesize[2],
|
||||||
|
rgb.raw.as_mut_ptr(),
|
||||||
|
bytes_per_row as _,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bgra(&self, bgra: &mut Vec<u8>, i420: &mut Vec<u8>) -> ResultType<()> {
|
|
||||||
let mut rgb = ImageRgb::new(ImageFormat::ARGB, 1);
|
|
||||||
self.to_fmt(&mut rgb, i420)?;
|
|
||||||
*bgra = rgb.raw;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rgba(&self, rgba: &mut Vec<u8>, i420: &mut Vec<u8>) -> ResultType<()> {
|
|
||||||
let mut rgb = ImageRgb::new(ImageFormat::ABGR, 1);
|
|
||||||
self.to_fmt(&mut rgb, i420)?;
|
|
||||||
*rgba = rgb.raw;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
@@ -363,24 +474,103 @@ struct Available {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_config() -> ResultType<Available> {
|
fn get_config() -> ResultType<Available> {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let info = crate::android::ffi::get_codec_info();
|
||||||
|
log::info!("all codec info: {info:?}");
|
||||||
|
struct T {
|
||||||
|
name_prefix: &'static str,
|
||||||
|
data_format: DataFormat,
|
||||||
|
}
|
||||||
|
let ts = vec![
|
||||||
|
T {
|
||||||
|
name_prefix: "h264",
|
||||||
|
data_format: DataFormat::H264,
|
||||||
|
},
|
||||||
|
T {
|
||||||
|
name_prefix: "hevc",
|
||||||
|
data_format: DataFormat::H265,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut e = vec![];
|
||||||
|
if let Some(info) = info {
|
||||||
|
ts.iter().for_each(|t| {
|
||||||
|
let codecs: Vec<_> = info
|
||||||
|
.codecs
|
||||||
|
.iter()
|
||||||
|
.filter(|c| {
|
||||||
|
c.is_encoder
|
||||||
|
&& c.mime_type.as_str() == get_mime_type(t.data_format)
|
||||||
|
&& c.nv12
|
||||||
|
&& c.hw == Some(true) //only use hardware codec
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
log::debug!("available {:?} encoders: {codecs:?}", t.data_format);
|
||||||
|
let screen_wh = std::cmp::max(info.w, info.h);
|
||||||
|
let mut best = None;
|
||||||
|
if let Some(codec) = codecs
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.max_width >= screen_wh && c.max_height >= screen_wh)
|
||||||
|
{
|
||||||
|
best = Some(codec.name.clone());
|
||||||
|
} else {
|
||||||
|
// find the max resolution
|
||||||
|
let mut max_area = 0;
|
||||||
|
for codec in codecs.iter() {
|
||||||
|
if codec.max_width * codec.max_height > max_area {
|
||||||
|
best = Some(codec.name.clone());
|
||||||
|
max_area = codec.max_width * codec.max_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(best) = best {
|
||||||
|
e.push(CodecInfo {
|
||||||
|
name: format!("{}_mediacodec", t.name_prefix),
|
||||||
|
mc_name: Some(best),
|
||||||
|
format: t.data_format,
|
||||||
|
hwdevice: hwcodec::ffmpeg::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||||
|
priority: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log::debug!("e: {e:?}");
|
||||||
|
Ok(Available { e, d: vec![] })
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
match serde_json::from_str(&HwCodecConfig::load().ram) {
|
match serde_json::from_str(&HwCodecConfig::load().ram) {
|
||||||
Ok(v) => Ok(v),
|
Ok(v) => Ok(v),
|
||||||
Err(e) => Err(anyhow!("Failed to get config:{e:?}")),
|
Err(e) => Err(anyhow!("Failed to get config:{e:?}")),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn get_mime_type(codec: DataFormat) -> &'static str {
|
||||||
|
match codec {
|
||||||
|
DataFormat::VP8 => "video/x-vnd.on2.vp8",
|
||||||
|
DataFormat::VP9 => "video/x-vnd.on2.vp9",
|
||||||
|
DataFormat::AV1 => "video/av01",
|
||||||
|
DataFormat::H264 => "video/avc",
|
||||||
|
DataFormat::H265 => "video/hevc",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_available_hwcodec() {
|
pub fn check_available_hwcodec() {
|
||||||
let ctx = EncodeContext {
|
let ctx = EncodeContext {
|
||||||
name: String::from(""),
|
name: String::from(""),
|
||||||
|
mc_name: None,
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
pixfmt: DEFAULT_PIXFMT,
|
pixfmt: DEFAULT_PIXFMT,
|
||||||
align: HW_STRIDE_ALIGN as _,
|
align: HW_STRIDE_ALIGN as _,
|
||||||
bitrate: 0,
|
kbs: 0,
|
||||||
timebase: DEFAULT_TIME_BASE,
|
timebase: DEFAULT_TIME_BASE,
|
||||||
gop: DEFAULT_GOP,
|
gop: DEFAULT_GOP,
|
||||||
quality: DEFAULT_HW_QUALITY,
|
quality: DEFAULT_HW_QUALITY,
|
||||||
rc: DEFAULT_RC,
|
rc: RC_CBR,
|
||||||
|
q: -1,
|
||||||
thread_count: 4,
|
thread_count: 4,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ pub mod record;
|
|||||||
mod vpx;
|
mod vpx;
|
||||||
|
|
||||||
#[repr(usize)]
|
#[repr(usize)]
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum ImageFormat {
|
pub enum ImageFormat {
|
||||||
Raw,
|
Raw,
|
||||||
ABGR,
|
ABGR,
|
||||||
@@ -65,17 +65,17 @@ pub struct ImageRgb {
|
|||||||
pub w: usize,
|
pub w: usize,
|
||||||
pub h: usize,
|
pub h: usize,
|
||||||
pub fmt: ImageFormat,
|
pub fmt: ImageFormat,
|
||||||
pub stride: usize,
|
pub align: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageRgb {
|
impl ImageRgb {
|
||||||
pub fn new(fmt: ImageFormat, stride: usize) -> Self {
|
pub fn new(fmt: ImageFormat, align: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
raw: Vec::new(),
|
raw: Vec::new(),
|
||||||
w: 0,
|
w: 0,
|
||||||
h: 0,
|
h: 0,
|
||||||
fmt,
|
fmt,
|
||||||
stride,
|
align,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +85,13 @@ impl ImageRgb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stride(&self) -> usize {
|
pub fn align(&self) -> usize {
|
||||||
self.stride
|
self.align
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_align(&mut self, align: usize) {
|
||||||
|
self.align = align;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,20 +378,20 @@ pub trait GoogleImage {
|
|||||||
fn stride(&self) -> Vec<i32>;
|
fn stride(&self) -> Vec<i32>;
|
||||||
fn planes(&self) -> Vec<*mut u8>;
|
fn planes(&self) -> Vec<*mut u8>;
|
||||||
fn chroma(&self) -> Chroma;
|
fn chroma(&self) -> Chroma;
|
||||||
fn get_bytes_per_row(w: usize, fmt: ImageFormat, stride: usize) -> usize {
|
fn get_bytes_per_row(w: usize, fmt: ImageFormat, align: usize) -> usize {
|
||||||
let bytes_per_pixel = match fmt {
|
let bytes_per_pixel = match fmt {
|
||||||
ImageFormat::Raw => 3,
|
ImageFormat::Raw => 3,
|
||||||
ImageFormat::ARGB | ImageFormat::ABGR => 4,
|
ImageFormat::ARGB | ImageFormat::ABGR => 4,
|
||||||
};
|
};
|
||||||
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L128
|
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L128
|
||||||
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L129
|
// https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L129
|
||||||
(w * bytes_per_pixel + stride - 1) & !(stride - 1)
|
(w * bytes_per_pixel + align - 1) & !(align - 1)
|
||||||
}
|
}
|
||||||
// rgb [in/out] fmt and stride must be set in ImageRgb
|
// rgb [in/out] fmt and stride must be set in ImageRgb
|
||||||
fn to(&self, rgb: &mut ImageRgb) {
|
fn to(&self, rgb: &mut ImageRgb) {
|
||||||
rgb.w = self.width();
|
rgb.w = self.width();
|
||||||
rgb.h = self.height();
|
rgb.h = self.height();
|
||||||
let bytes_per_row = Self::get_bytes_per_row(rgb.w, rgb.fmt, rgb.stride());
|
let bytes_per_row = Self::get_bytes_per_row(rgb.w, rgb.fmt, rgb.align());
|
||||||
rgb.raw.resize(rgb.h * bytes_per_row, 0);
|
rgb.raw.resize(rgb.h * bytes_per_row, 0);
|
||||||
let stride = self.stride();
|
let stride = self.stride();
|
||||||
let planes = self.planes();
|
let planes = self.planes();
|
||||||
@@ -481,3 +486,8 @@ pub trait GoogleImage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn screen_size() -> (u16, u16, u16) {
|
||||||
|
SCREEN_SIZE.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|||||||
@@ -235,6 +235,17 @@ impl EncoderApi for VpxEncoder {
|
|||||||
fn support_abr(&self) -> bool {
|
fn support_abr(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
fn support_changing_quality(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latency_free(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hardware(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VpxEncoder {
|
impl VpxEncoder {
|
||||||
|
|||||||
@@ -180,6 +180,18 @@ impl EncoderApi for VRamEncoder {
|
|||||||
fn support_abr(&self) -> bool {
|
fn support_abr(&self) -> bool {
|
||||||
self.config.device.vendor_id != ADAPTER_VENDOR_INTEL as u32
|
self.config.device.vendor_id != ADAPTER_VENDOR_INTEL as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn support_changing_quality(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latency_free(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hardware(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VRamEncoder {
|
impl VRamEncoder {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.2.4
|
pkgver=1.2.5
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.2.4
|
Version: 1.2.5
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.2.4
|
Version: 1.2.5
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.2.4
|
Version: 1.2.5
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ use hbb_common::{
|
|||||||
},
|
},
|
||||||
get_version_number, log,
|
get_version_number, log,
|
||||||
message_proto::{option_message::BoolOption, *},
|
message_proto::{option_message::BoolOption, *},
|
||||||
protobuf::Message as _,
|
protobuf::{Message as _, MessageField},
|
||||||
rand,
|
rand,
|
||||||
rendezvous_proto::*,
|
rendezvous_proto::*,
|
||||||
socket_client,
|
socket_client,
|
||||||
@@ -61,6 +61,7 @@ use crate::{
|
|||||||
check_port,
|
check_port,
|
||||||
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
||||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp,
|
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp,
|
||||||
|
ui_interface::use_texture_render,
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1035,16 +1036,23 @@ pub struct VideoHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VideoHandler {
|
impl VideoHandler {
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
pub fn get_adapter_luid() -> Option<i64> {
|
||||||
|
crate::flutter::get_adapter_luid()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "flutter"))]
|
||||||
|
pub fn get_adapter_luid() -> Option<i64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new video handler.
|
/// Create a new video handler.
|
||||||
pub fn new(format: CodecFormat, _display: usize) -> Self {
|
pub fn new(format: CodecFormat, _display: usize) -> Self {
|
||||||
#[cfg(all(feature = "vram", feature = "flutter"))]
|
let luid = Self::get_adapter_luid();
|
||||||
let luid = crate::flutter::get_adapter_luid();
|
|
||||||
#[cfg(not(all(feature = "vram", feature = "flutter")))]
|
|
||||||
let luid = Default::default();
|
|
||||||
log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}");
|
log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}");
|
||||||
VideoHandler {
|
VideoHandler {
|
||||||
decoder: Decoder::new(format, luid),
|
decoder: Decoder::new(format, luid),
|
||||||
rgb: ImageRgb::new(ImageFormat::ARGB, crate::DST_STRIDE_RGBA),
|
rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()),
|
||||||
texture: std::ptr::null_mut(),
|
texture: std::ptr::null_mut(),
|
||||||
recorder: Default::default(),
|
recorder: Default::default(),
|
||||||
record: false,
|
record: false,
|
||||||
@@ -1096,10 +1104,9 @@ impl VideoHandler {
|
|||||||
|
|
||||||
/// Reset the decoder, change format if it is Some
|
/// Reset the decoder, change format if it is Some
|
||||||
pub fn reset(&mut self, format: Option<CodecFormat>) {
|
pub fn reset(&mut self, format: Option<CodecFormat>) {
|
||||||
#[cfg(all(feature = "flutter", feature = "vram"))]
|
#[cfg(target_os = "macos")]
|
||||||
let luid = crate::flutter::get_adapter_luid();
|
self.rgb.set_align(crate::get_dst_align_rgba());
|
||||||
#[cfg(not(all(feature = "flutter", feature = "vram")))]
|
let luid = Self::get_adapter_luid();
|
||||||
let luid = None;
|
|
||||||
let format = format.unwrap_or(self.decoder.format());
|
let format = format.unwrap_or(self.decoder.format());
|
||||||
self.decoder = Decoder::new(format, luid);
|
self.decoder = Decoder::new(format, luid);
|
||||||
self.fail_counter = 0;
|
self.fail_counter = 0;
|
||||||
@@ -1503,9 +1510,9 @@ impl LoginConfigHandler {
|
|||||||
BoolOption::Yes
|
BoolOption::Yes
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
} else if name == "enable-file-transfer" {
|
} else if name == "enable-file-copy-paste" {
|
||||||
config.enable_file_transfer.v = !config.enable_file_transfer.v;
|
config.enable_file_copy_paste.v = !config.enable_file_copy_paste.v;
|
||||||
option.enable_file_transfer = (if config.enable_file_transfer.v {
|
option.enable_file_transfer = (if config.enable_file_copy_paste.v {
|
||||||
BoolOption::Yes
|
BoolOption::Yes
|
||||||
} else {
|
} else {
|
||||||
BoolOption::No
|
BoolOption::No
|
||||||
@@ -1538,7 +1545,7 @@ impl LoginConfigHandler {
|
|||||||
option.disable_keyboard = f(false);
|
option.disable_keyboard = f(false);
|
||||||
option.disable_clipboard = f(self.get_toggle_option("disable-clipboard"));
|
option.disable_clipboard = f(self.get_toggle_option("disable-clipboard"));
|
||||||
option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor"));
|
option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor"));
|
||||||
option.enable_file_transfer = f(self.config.enable_file_transfer.v);
|
option.enable_file_transfer = f(self.config.enable_file_copy_paste.v);
|
||||||
option.lock_after_session_end = f(self.config.lock_after_session_end.v);
|
option.lock_after_session_end = f(self.config.lock_after_session_end.v);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1631,20 +1638,23 @@ impl LoginConfigHandler {
|
|||||||
if self.get_toggle_option("disable-audio") {
|
if self.get_toggle_option("disable-audio") {
|
||||||
msg.disable_audio = BoolOption::Yes.into();
|
msg.disable_audio = BoolOption::Yes.into();
|
||||||
}
|
}
|
||||||
if !view_only && self.get_toggle_option("enable-file-transfer") {
|
if !view_only && self.get_toggle_option(config::keys::OPTION_ENABLE_FILE_COPY_PASTE) {
|
||||||
msg.enable_file_transfer = BoolOption::Yes.into();
|
msg.enable_file_transfer = BoolOption::Yes.into();
|
||||||
}
|
}
|
||||||
if view_only || self.get_toggle_option("disable-clipboard") {
|
if view_only || self.get_toggle_option("disable-clipboard") {
|
||||||
msg.disable_clipboard = BoolOption::Yes.into();
|
msg.disable_clipboard = BoolOption::Yes.into();
|
||||||
}
|
}
|
||||||
msg.supported_decoding =
|
msg.supported_decoding = MessageField::some(self.get_supported_decoding());
|
||||||
hbb_common::protobuf::MessageField::some(Decoder::supported_decodings(
|
Some(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_supported_decoding(&self) -> SupportedDecoding {
|
||||||
|
Decoder::supported_decodings(
|
||||||
Some(&self.id),
|
Some(&self.id),
|
||||||
cfg!(feature = "flutter"),
|
use_texture_render(),
|
||||||
self.adapter_luid,
|
self.adapter_luid,
|
||||||
&self.mark_unsupported,
|
&self.mark_unsupported,
|
||||||
));
|
)
|
||||||
Some(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
|
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
|
||||||
@@ -1704,8 +1714,8 @@ impl LoginConfigHandler {
|
|||||||
self.config.lock_after_session_end.v
|
self.config.lock_after_session_end.v
|
||||||
} else if name == "privacy-mode" {
|
} else if name == "privacy-mode" {
|
||||||
self.config.privacy_mode.v
|
self.config.privacy_mode.v
|
||||||
} else if name == "enable-file-transfer" {
|
} else if name == config::keys::OPTION_ENABLE_FILE_COPY_PASTE {
|
||||||
self.config.enable_file_transfer.v
|
self.config.enable_file_copy_paste.v
|
||||||
} else if name == "disable-audio" {
|
} else if name == "disable-audio" {
|
||||||
self.config.disable_audio.v
|
self.config.disable_audio.v
|
||||||
} else if name == "disable-clipboard" {
|
} else if name == "disable-clipboard" {
|
||||||
@@ -2036,7 +2046,7 @@ impl LoginConfigHandler {
|
|||||||
pub fn update_supported_decodings(&self) -> Message {
|
pub fn update_supported_decodings(&self) -> Message {
|
||||||
let decoding = scrap::codec::Decoder::supported_decodings(
|
let decoding = scrap::codec::Decoder::supported_decodings(
|
||||||
Some(&self.id),
|
Some(&self.id),
|
||||||
cfg!(feature = "flutter"),
|
use_texture_render(),
|
||||||
self.adapter_luid,
|
self.adapter_luid,
|
||||||
&self.mark_unsupported,
|
&self.mark_unsupported,
|
||||||
);
|
);
|
||||||
@@ -2065,7 +2075,7 @@ pub enum MediaData {
|
|||||||
VideoFrame(Box<VideoFrame>),
|
VideoFrame(Box<VideoFrame>),
|
||||||
AudioFrame(Box<AudioFrame>),
|
AudioFrame(Box<AudioFrame>),
|
||||||
AudioFormat(AudioFormat),
|
AudioFormat(AudioFormat),
|
||||||
Reset(usize),
|
Reset(Option<usize>),
|
||||||
RecordScreen(bool, usize, i32, i32, String),
|
RecordScreen(bool, usize, i32, i32, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2241,9 +2251,17 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediaData::Reset(display) => {
|
MediaData::Reset(display) => {
|
||||||
if let Some(handler_controler) = handler_controller_map.get_mut(&display) {
|
if let Some(display) = display {
|
||||||
|
if let Some(handler_controler) =
|
||||||
|
handler_controller_map.get_mut(&display)
|
||||||
|
{
|
||||||
handler_controler.handler.reset(None);
|
handler_controler.handler.reset(None);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (_, handler_controler) in handler_controller_map.iter_mut() {
|
||||||
|
handler_controler.handler.reset(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MediaData::RecordScreen(start, display, w, h, id) => {
|
MediaData::RecordScreen(start, display, w, h, id) => {
|
||||||
log::info!("record screen command: start: {start}, display: {display}");
|
log::info!("record screen command: start: {start}, display: {display}");
|
||||||
@@ -2945,6 +2963,7 @@ pub enum Data {
|
|||||||
ElevateWithLogon(String, String),
|
ElevateWithLogon(String, String),
|
||||||
NewVoiceCall,
|
NewVoiceCall,
|
||||||
CloseVoiceCall,
|
CloseVoiceCall,
|
||||||
|
ResetDecoder(Option<usize>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keycode for key events.
|
/// Keycode for key events.
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
let server_file_transfer_enabled =
|
let server_file_transfer_enabled =
|
||||||
*self.handler.server_file_transfer_enabled.read().unwrap();
|
*self.handler.server_file_transfer_enabled.read().unwrap();
|
||||||
let file_transfer_enabled =
|
let file_transfer_enabled =
|
||||||
self.handler.lc.read().unwrap().enable_file_transfer.v;
|
self.handler.lc.read().unwrap().enable_file_copy_paste.v;
|
||||||
let view_only = self.handler.lc.read().unwrap().view_only.v;
|
let view_only = self.handler.lc.read().unwrap().view_only.v;
|
||||||
let stop = is_stopping_allowed
|
let stop = is_stopping_allowed
|
||||||
&& (view_only
|
&& (view_only
|
||||||
@@ -843,6 +843,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
.on_voice_call_closed("Closed manually by the peer");
|
.on_voice_call_closed("Closed manually by the peer");
|
||||||
allow_err!(peer.send(&msg).await);
|
allow_err!(peer.send(&msg).await);
|
||||||
}
|
}
|
||||||
|
Data::ResetDecoder(display) => {
|
||||||
|
self.video_sender.send(MediaData::Reset(display)).ok();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -1027,13 +1030,15 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
ctl.last_auto_fps = Some(auto_fps);
|
ctl.last_auto_fps = Some(auto_fps);
|
||||||
}
|
}
|
||||||
// send refresh
|
// send refresh
|
||||||
|
let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2);
|
||||||
if ctl.refresh_times < 10 // enough
|
if ctl.refresh_times < 10 // enough
|
||||||
&& (len > video_queue.capacity() / 2
|
&& (len > tolerable
|
||||||
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 30))
|
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10))
|
||||||
{
|
{
|
||||||
// Refresh causes client set_display, left frames cause flickering.
|
// Refresh causes client set_display, left frames cause flickering.
|
||||||
while let Some(_) = video_queue.pop() {}
|
while let Some(_) = video_queue.pop() {}
|
||||||
self.handler.refresh_video(*display as _);
|
self.handler.refresh_video(*display as _);
|
||||||
|
log::info!("Refresh display {} to reduce delay", display);
|
||||||
ctl.refresh_times += 1;
|
ctl.refresh_times += 1;
|
||||||
ctl.last_refresh_instant = Instant::now();
|
ctl.last_refresh_instant = Instant::now();
|
||||||
}
|
}
|
||||||
@@ -1369,7 +1374,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
Some(misc::Union::SwitchDisplay(s)) => {
|
Some(misc::Union::SwitchDisplay(s)) => {
|
||||||
self.handler.handle_peer_switch_display(&s);
|
self.handler.handle_peer_switch_display(&s);
|
||||||
self.video_sender
|
self.video_sender
|
||||||
.send(MediaData::Reset(s.display as _))
|
.send(MediaData::Reset(Some(s.display as _)))
|
||||||
.ok();
|
.ok();
|
||||||
if s.width > 0 && s.height > 0 {
|
if s.width > 0 && s.height > 0 {
|
||||||
self.handler.set_display(
|
self.handler.set_display(
|
||||||
@@ -1760,7 +1765,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
let enabled = *self.handler.server_file_transfer_enabled.read().unwrap()
|
let enabled = *self.handler.server_file_transfer_enabled.read().unwrap()
|
||||||
&& self.handler.lc.read().unwrap().enable_file_transfer.v;
|
&& self.handler.lc.read().unwrap().enable_file_copy_paste.v;
|
||||||
ContextSend::enable(enabled);
|
ContextSend::enable(enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1783,7 +1788,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let is_stopping_allowed = clip.is_stopping_allowed_from_peer();
|
let is_stopping_allowed = clip.is_stopping_allowed_from_peer();
|
||||||
let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_transfer.v;
|
let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v;
|
||||||
let stop = is_stopping_allowed && !file_transfer_enabled;
|
let stop = is_stopping_allowed && !file_transfer_enabled;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}",
|
"Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}",
|
||||||
|
|||||||
149
src/common.rs
149
src/common.rs
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
collections::HashMap,
|
||||||
future::Future,
|
future::Future,
|
||||||
sync::{Arc, Mutex, RwLock},
|
sync::{Arc, Mutex, RwLock},
|
||||||
task::Poll,
|
task::Poll,
|
||||||
@@ -157,13 +158,6 @@ pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future<Out
|
|||||||
pub const CLIPBOARD_NAME: &'static str = "clipboard";
|
pub const CLIPBOARD_NAME: &'static str = "clipboard";
|
||||||
pub const CLIPBOARD_INTERVAL: u64 = 333;
|
pub const CLIPBOARD_INTERVAL: u64 = 333;
|
||||||
|
|
||||||
#[cfg(all(target_os = "macos", feature = "flutter_texture_render"))]
|
|
||||||
// https://developer.apple.com/forums/thread/712709
|
|
||||||
// Memory alignment should be multiple of 64.
|
|
||||||
pub const DST_STRIDE_RGBA: usize = 64;
|
|
||||||
#[cfg(not(all(target_os = "macos", feature = "flutter_texture_render")))]
|
|
||||||
pub const DST_STRIDE_RGBA: usize = 1;
|
|
||||||
|
|
||||||
// the executable name of the portable version
|
// the executable name of the portable version
|
||||||
pub const PORTABLE_APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
pub const PORTABLE_APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
||||||
|
|
||||||
@@ -1571,6 +1565,74 @@ pub fn load_custom_client() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_custom_client_advanced_settings(
|
||||||
|
settings: serde_json::Value,
|
||||||
|
map_display_settings: &HashMap<String, &&str>,
|
||||||
|
map_local_settings: &HashMap<String, &&str>,
|
||||||
|
map_settings: &HashMap<String, &&str>,
|
||||||
|
is_override: bool,
|
||||||
|
) {
|
||||||
|
let mut display_settings = if is_override {
|
||||||
|
config::OVERWRITE_DISPLAY_SETTINGS.write().unwrap()
|
||||||
|
} else {
|
||||||
|
config::DEFAULT_DISPLAY_SETTINGS.write().unwrap()
|
||||||
|
};
|
||||||
|
let mut local_settings = if is_override {
|
||||||
|
config::OVERWRITE_LOCAL_SETTINGS.write().unwrap()
|
||||||
|
} else {
|
||||||
|
config::DEFAULT_LOCAL_SETTINGS.write().unwrap()
|
||||||
|
};
|
||||||
|
let mut server_settings = if is_override {
|
||||||
|
config::OVERWRITE_SETTINGS.write().unwrap()
|
||||||
|
} else {
|
||||||
|
config::DEFAULT_SETTINGS.write().unwrap()
|
||||||
|
};
|
||||||
|
if let Some(settings) = settings.as_object() {
|
||||||
|
for (k, v) in settings {
|
||||||
|
let Some(v) = v.as_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(k2) = map_display_settings.get(k) {
|
||||||
|
display_settings.insert(k2.to_string(), v.to_owned());
|
||||||
|
} else if let Some(k2) = map_local_settings.get(k) {
|
||||||
|
local_settings.insert(k2.to_string(), v.to_owned());
|
||||||
|
} else if let Some(k2) = map_settings.get(k) {
|
||||||
|
server_settings.insert(k2.to_string(), v.to_owned());
|
||||||
|
} else {
|
||||||
|
let k2 = k.replace("_", "-");
|
||||||
|
let k = k2.replace("-", "_");
|
||||||
|
// display
|
||||||
|
display_settings.insert(k.clone(), v.to_owned());
|
||||||
|
display_settings.insert(k2.clone(), v.to_owned());
|
||||||
|
// local
|
||||||
|
local_settings.insert(k.clone(), v.to_owned());
|
||||||
|
local_settings.insert(k2.clone(), v.to_owned());
|
||||||
|
// server
|
||||||
|
server_settings.insert(k.clone(), v.to_owned());
|
||||||
|
server_settings.insert(k2.clone(), v.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn get_dst_align_rgba() -> usize {
|
||||||
|
// https://developer.apple.com/forums/thread/712709
|
||||||
|
// Memory alignment should be multiple of 64.
|
||||||
|
if crate::ui_interface::use_texture_render() {
|
||||||
|
64
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub fn get_dst_align_rgba() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_custom_client(config: &str) {
|
pub fn read_custom_client(config: &str) {
|
||||||
let Ok(data) = decode64(config) else {
|
let Ok(data) = decode64(config) else {
|
||||||
log::error!("Failed to decode custom client config");
|
log::error!("Failed to decode custom client config");
|
||||||
@@ -1597,55 +1659,36 @@ pub fn read_custom_client(config: &str) {
|
|||||||
*config::APP_NAME.write().unwrap() = app_name.to_owned();
|
*config::APP_NAME.write().unwrap() = app_name.to_owned();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut map_display_settings = HashMap::new();
|
||||||
|
for s in config::keys::KEYS_DISPLAY_SETTINGS {
|
||||||
|
map_display_settings.insert(s.replace("_", "-"), s);
|
||||||
|
}
|
||||||
|
let mut map_local_settings = HashMap::new();
|
||||||
|
for s in config::keys::KEYS_LOCAL_SETTINGS {
|
||||||
|
map_local_settings.insert(s.replace("_", "-"), s);
|
||||||
|
}
|
||||||
|
let mut map_settings = HashMap::new();
|
||||||
|
for s in config::keys::KEYS_SETTINGS {
|
||||||
|
map_settings.insert(s.replace("_", "-"), s);
|
||||||
|
}
|
||||||
if let Some(default_settings) = data.remove("default-settings") {
|
if let Some(default_settings) = data.remove("default-settings") {
|
||||||
if let Some(default_settings) = default_settings.as_object() {
|
read_custom_client_advanced_settings(
|
||||||
for (k, v) in default_settings {
|
default_settings,
|
||||||
let Some(v) = v.as_str() else {
|
&map_display_settings,
|
||||||
continue;
|
&map_local_settings,
|
||||||
};
|
&map_settings,
|
||||||
if k.starts_with("$$") {
|
false,
|
||||||
config::DEFAULT_DISPLAY_SETTINGS
|
);
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(k.clone(), v[2..].to_owned());
|
|
||||||
} else if k.starts_with("$") {
|
|
||||||
config::DEFAULT_LOCAL_SETTINGS
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(k.clone(), v[1..].to_owned());
|
|
||||||
} else {
|
|
||||||
config::DEFAULT_SETTINGS
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(k.clone(), v.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(overwrite_settings) = data.remove("override-settings") {
|
if let Some(overwrite_settings) = data.remove("override-settings") {
|
||||||
if let Some(overwrite_settings) = overwrite_settings.as_object() {
|
read_custom_client_advanced_settings(
|
||||||
for (k, v) in overwrite_settings {
|
overwrite_settings,
|
||||||
let Some(v) = v.as_str() else {
|
&map_display_settings,
|
||||||
continue;
|
&map_local_settings,
|
||||||
};
|
&map_settings,
|
||||||
if k.starts_with("$$") {
|
true,
|
||||||
config::OVERWRITE_DISPLAY_SETTINGS
|
);
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(k.clone(), v[2..].to_owned());
|
|
||||||
} else if k.starts_with("$") {
|
|
||||||
config::OVERWRITE_LOCAL_SETTINGS
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(k.clone(), v[1..].to_owned());
|
|
||||||
} else {
|
|
||||||
config::OVERWRITE_SETTINGS
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(k.clone(), v.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (k, v) in data {
|
for (k, v) in data {
|
||||||
if let Some(v) = v.as_str() {
|
if let Some(v) = v.as_str() {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
_is_quick_support |= !crate::platform::is_installed()
|
_is_quick_support |= !crate::platform::is_installed()
|
||||||
&& args.is_empty()
|
&& args.is_empty()
|
||||||
&& (arg_exe.to_lowercase().contains("-qs-")
|
&& (arg_exe.to_lowercase().contains("-qs-")
|
||||||
|
|| config::LocalConfig::get_option("pre-elevate-service") == "Y"
|
||||||
|| (!click_setup && crate::platform::is_elevated(None).unwrap_or(false)));
|
|| (!click_setup && crate::platform::is_elevated(None).unwrap_or(false)));
|
||||||
crate::portable_service::client::set_quick_support(_is_quick_support);
|
crate::portable_service::client::set_quick_support(_is_quick_support);
|
||||||
}
|
}
|
||||||
@@ -565,7 +566,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<Strin
|
|||||||
{
|
{
|
||||||
use winapi::um::winuser::WM_USER;
|
use winapi::um::winuser::WM_USER;
|
||||||
let res = crate::platform::send_message_to_hnwd(
|
let res = crate::platform::send_message_to_hnwd(
|
||||||
"FLUTTER_RUNNER_WIN32_WINDOW",
|
&crate::platform::FLUTTER_RUNNER_WIN32_WINDOW_CLASS,
|
||||||
&crate::get_app_name(),
|
&crate::get_app_name(),
|
||||||
(WM_USER + 2) as _, // referred from unilinks desktop pub
|
(WM_USER + 2) as _, // referred from unilinks desktop pub
|
||||||
uni_links.as_str(),
|
uni_links.as_str(),
|
||||||
|
|||||||
273
src/flutter.rs
273
src/flutter.rs
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
ui_session_interface::{io_loop, InvokeUiSession, Session},
|
ui_session_interface::{io_loop, InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
use flutter_rust_bridge::StreamSink;
|
use flutter_rust_bridge::StreamSink;
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use hbb_common::dlopen::{
|
use hbb_common::dlopen::{
|
||||||
symbor::{Library, Symbol},
|
symbor::{Library, Symbol},
|
||||||
Error as LibError,
|
Error as LibError,
|
||||||
@@ -16,15 +16,15 @@ use hbb_common::{
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
use std::os::raw::c_void;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ffi::CString,
|
ffi::CString,
|
||||||
os::raw::{c_char, c_int},
|
os::raw::{c_char, c_int, c_void},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, RwLock},
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, RwLock,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
||||||
@@ -48,22 +48,22 @@ lazy_static::lazy_static! {
|
|||||||
static ref GLOBAL_EVENT_STREAM: RwLock<HashMap<String, StreamSink<String>>> = Default::default(); // rust to dart event channel
|
static ref GLOBAL_EVENT_STREAM: RwLock<HashMap<String, StreamSink<String>>> = Default::default(); // rust to dart event channel
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "windows", feature = "flutter_texture_render"))]
|
#[cfg(target_os = "windows")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result<Library, LibError> = Library::open("texture_rgba_renderer_plugin.dll");
|
pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result<Library, LibError> = Library::open("texture_rgba_renderer_plugin.dll");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", feature = "flutter_texture_render"))]
|
#[cfg(target_os = "linux")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result<Library, LibError> = Library::open("libtexture_rgba_renderer_plugin.so");
|
pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result<Library, LibError> = Library::open("libtexture_rgba_renderer_plugin.so");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "macos", feature = "flutter_texture_render"))]
|
#[cfg(target_os = "macos")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result<Library, LibError> = Library::open_self();
|
pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result<Library, LibError> = Library::open_self();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "windows", feature = "vram"))]
|
#[cfg(target_os = "windows")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result<Library, LibError> = Library::open("flutter_gpu_texture_renderer_plugin.dll");
|
pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result<Library, LibError> = Library::open("flutter_gpu_texture_renderer_plugin.dll");
|
||||||
}
|
}
|
||||||
@@ -168,11 +168,9 @@ pub unsafe extern "C" fn get_rustdesk_app_name(buffer: *mut u16, length: i32) ->
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct SessionHandler {
|
struct SessionHandler {
|
||||||
event_stream: Option<StreamSink<EventToUI>>,
|
event_stream: Option<StreamSink<EventToUI>>,
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
renderer: VideoRenderer,
|
renderer: VideoRenderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
enum RenderType {
|
enum RenderType {
|
||||||
PixelBuffer,
|
PixelBuffer,
|
||||||
@@ -180,22 +178,32 @@ enum RenderType {
|
|||||||
Texture,
|
Texture,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FlutterHandler {
|
pub struct FlutterHandler {
|
||||||
// ui session id -> display handler data
|
// ui session id -> display handler data
|
||||||
session_handlers: Arc<RwLock<HashMap<SessionID, SessionHandler>>>,
|
session_handlers: Arc<RwLock<HashMap<SessionID, SessionHandler>>>,
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
display_rgbas: Arc<RwLock<HashMap<usize, RgbaData>>>,
|
display_rgbas: Arc<RwLock<HashMap<usize, RgbaData>>>,
|
||||||
peer_info: Arc<RwLock<PeerInfo>>,
|
peer_info: Arc<RwLock<PeerInfo>>,
|
||||||
#[cfg(any(
|
|
||||||
not(feature = "flutter_texture_render"),
|
|
||||||
all(feature = "flutter_texture_render", feature = "plugin_framework")
|
|
||||||
))]
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
hooks: Arc<RwLock<HashMap<String, SessionHook>>>,
|
hooks: Arc<RwLock<HashMap<String, SessionHook>>>,
|
||||||
|
use_texture_render: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FlutterHandler {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
session_handlers: Default::default(),
|
||||||
|
display_rgbas: Default::default(),
|
||||||
|
peer_info: Default::default(),
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
hooks: Default::default(),
|
||||||
|
use_texture_render: Arc::new(
|
||||||
|
AtomicBool::new(crate::ui_interface::use_texture_render()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
struct RgbaData {
|
struct RgbaData {
|
||||||
// SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`.
|
// SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`.
|
||||||
@@ -204,7 +212,6 @@ struct RgbaData {
|
|||||||
valid: bool,
|
valid: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(
|
pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(
|
||||||
texture_rgba: *mut c_void,
|
texture_rgba: *mut c_void,
|
||||||
buffer: *const u8,
|
buffer: *const u8,
|
||||||
@@ -221,15 +228,11 @@ pub type FlutterGpuTextureRendererPluginCApiSetTexture =
|
|||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
||||||
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
pub(super) type TextureRgbaPtr = usize;
|
pub(super) type TextureRgbaPtr = usize;
|
||||||
|
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
struct DisplaySessionInfo {
|
struct DisplaySessionInfo {
|
||||||
// TextureRgba pointer in flutter native.
|
// TextureRgba pointer in flutter native.
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
texture_rgba_ptr: TextureRgbaPtr,
|
texture_rgba_ptr: TextureRgbaPtr,
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
size: (usize, usize),
|
size: (usize, usize),
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
gpu_output_ptr: usize,
|
gpu_output_ptr: usize,
|
||||||
@@ -237,21 +240,19 @@ struct DisplaySessionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Video Texture Renderer in Flutter
|
// Video Texture Renderer in Flutter
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct VideoRenderer {
|
struct VideoRenderer {
|
||||||
is_support_multi_ui_session: bool,
|
is_support_multi_ui_session: bool,
|
||||||
map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
|
map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
impl Default for VideoRenderer {
|
impl Default for VideoRenderer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
||||||
Ok(lib) => {
|
Ok(lib) => {
|
||||||
let find_sym_res = unsafe {
|
let find_sym_res = unsafe {
|
||||||
@@ -295,7 +296,7 @@ impl Default for VideoRenderer {
|
|||||||
Self {
|
Self {
|
||||||
map_display_sessions: Default::default(),
|
map_display_sessions: Default::default(),
|
||||||
is_support_multi_ui_session: false,
|
is_support_multi_ui_session: false,
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
on_rgba_func,
|
on_rgba_func,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
on_texture_func,
|
on_texture_func,
|
||||||
@@ -303,10 +304,8 @@ impl Default for VideoRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "flutter_texture_render", feature = "vram"))]
|
|
||||||
impl VideoRenderer {
|
impl VideoRenderer {
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get_mut(&display) {
|
||||||
@@ -326,7 +325,6 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
@@ -378,7 +376,6 @@ impl VideoRenderer {
|
|||||||
if info.gpu_output_ptr != usize::default() {
|
if info.gpu_output_ptr != usize::default() {
|
||||||
info.gpu_output_ptr = usize::default();
|
info.gpu_output_ptr = usize::default();
|
||||||
}
|
}
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
if info.texture_rgba_ptr != usize::default() {
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -400,9 +397,7 @@ impl VideoRenderer {
|
|||||||
sessions_lock.insert(
|
sessions_lock.insert(
|
||||||
display,
|
display,
|
||||||
DisplaySessionInfo {
|
DisplaySessionInfo {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
texture_rgba_ptr: usize::default(),
|
texture_rgba_ptr: usize::default(),
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
size: (0, 0),
|
size: (0, 0),
|
||||||
gpu_output_ptr: ptr,
|
gpu_output_ptr: ptr,
|
||||||
notify_render_type: None,
|
notify_render_type: None,
|
||||||
@@ -413,7 +408,7 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let mut write_lock = self.map_display_sessions.write().unwrap();
|
||||||
let opt_info = if !self.is_support_multi_ui_session {
|
let opt_info = if !self.is_support_multi_ui_session {
|
||||||
@@ -450,7 +445,7 @@ impl VideoRenderer {
|
|||||||
rgba.raw.len() as _,
|
rgba.raw.len() as _,
|
||||||
rgba.w as _,
|
rgba.w as _,
|
||||||
rgba.h as _,
|
rgba.h as _,
|
||||||
rgba.stride() as _,
|
rgba.align() as _,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -498,10 +493,7 @@ impl VideoRenderer {
|
|||||||
|
|
||||||
impl SessionHandler {
|
impl SessionHandler {
|
||||||
pub fn on_waiting_for_image_dialog_show(&self) {
|
pub fn on_waiting_for_image_dialog_show(&self) {
|
||||||
#[cfg(any(feature = "flutter_texture_render"))]
|
|
||||||
{
|
|
||||||
self.renderer.reset_all_display_render_type();
|
self.renderer.reset_all_display_render_type();
|
||||||
}
|
|
||||||
// rgba array render will notify every frame
|
// rgba array render will notify every frame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,6 +575,11 @@ impl FlutterHandler {
|
|||||||
let _ = hooks.remove(key);
|
let _ = hooks.remove(key);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_use_texture_render(&self) {
|
||||||
|
self.use_texture_render
|
||||||
|
.store(crate::ui_interface::use_texture_render(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InvokeUiSession for FlutterHandler {
|
impl InvokeUiSession for FlutterHandler {
|
||||||
@@ -769,60 +766,27 @@ impl InvokeUiSession for FlutterHandler {
|
|||||||
fn adapt_size(&self) {}
|
fn adapt_size(&self) {}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
|
|
||||||
// Give a chance for plugins or etc to hook a rgba data.
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
for (key, hook) in self.hooks.read().unwrap().iter() {
|
fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
|
||||||
match hook {
|
if self.use_texture_render.load(Ordering::Relaxed) {
|
||||||
SessionHook::OnSessionRgba(cb) => {
|
self.on_rgba_flutter_texture_render(display, rgba);
|
||||||
cb(key.to_owned(), rgba);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the current rgba is not fetched by flutter, i.e., is valid.
|
|
||||||
// We give up sending a new event to flutter.
|
|
||||||
let mut rgba_write_lock = self.display_rgbas.write().unwrap();
|
|
||||||
if let Some(rgba_data) = rgba_write_lock.get_mut(&display) {
|
|
||||||
if rgba_data.valid {
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
rgba_data.valid = true;
|
self.on_rgba_soft_render(display, rgba);
|
||||||
}
|
|
||||||
// Return the rgba buffer to the video handler for reusing allocated rgba buffer.
|
|
||||||
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut rgba_data.data);
|
|
||||||
} else {
|
|
||||||
let mut rgba_data = RgbaData::default();
|
|
||||||
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut rgba_data.data);
|
|
||||||
rgba_data.valid = true;
|
|
||||||
rgba_write_lock.insert(display, rgba_data);
|
|
||||||
}
|
|
||||||
drop(rgba_write_lock);
|
|
||||||
|
|
||||||
// Non-texture-render UI does not support multiple displays in the one UI session.
|
|
||||||
// It's Ok to notify each session for now.
|
|
||||||
for h in self.session_handlers.read().unwrap().values() {
|
|
||||||
if let Some(stream) = &h.event_stream {
|
|
||||||
stream.add(EventToUI::Rgba(display));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
|
fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
|
||||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
self.on_rgba_soft_render(display, rgba);
|
||||||
if session.renderer.on_rgba(display, rgba) {
|
|
||||||
if let Some(stream) = &session.event_stream {
|
|
||||||
stream.add(EventToUI::Rgba(display));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
fn on_texture(&self, display: usize, texture: *mut c_void) {
|
fn on_texture(&self, display: usize, texture: *mut c_void) {
|
||||||
|
if !self.use_texture_render.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
if session.renderer.on_texture(display, texture) {
|
if session.renderer.on_texture(display, texture) {
|
||||||
if let Some(stream) = &session.event_stream {
|
if let Some(stream) = &session.event_stream {
|
||||||
@@ -845,17 +809,17 @@ impl InvokeUiSession for FlutterHandler {
|
|||||||
let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned());
|
let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned());
|
||||||
let resolutions = serialize_resolutions(&pi.resolutions.resolutions);
|
let resolutions = serialize_resolutions(&pi.resolutions.resolutions);
|
||||||
*self.peer_info.write().unwrap() = pi.clone();
|
*self.peer_info.write().unwrap() = pi.clone();
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
{
|
let is_support_multi_ui_session = crate::common::is_support_multi_ui_session(&pi.version);
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
let is_support_multi_ui_session = false;
|
||||||
self.session_handlers
|
self.session_handlers
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.values_mut()
|
.values_mut()
|
||||||
.for_each(|h| {
|
.for_each(|h| {
|
||||||
h.renderer.is_support_multi_ui_session =
|
h.renderer.is_support_multi_ui_session = is_support_multi_ui_session;
|
||||||
crate::common::is_support_multi_ui_session(&pi.version);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
self.push_event(
|
self.push_event(
|
||||||
"peer_info",
|
"peer_info",
|
||||||
&[
|
&[
|
||||||
@@ -1028,7 +992,6 @@ impl InvokeUiSession for FlutterHandler {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_rgba(&self, _display: usize) -> *const u8 {
|
fn get_rgba(&self, _display: usize) -> *const u8 {
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
if let Some(rgba_data) = self.display_rgbas.read().unwrap().get(&_display) {
|
if let Some(rgba_data) = self.display_rgbas.read().unwrap().get(&_display) {
|
||||||
if rgba_data.valid {
|
if rgba_data.valid {
|
||||||
return rgba_data.data.as_ptr();
|
return rgba_data.data.as_ptr();
|
||||||
@@ -1039,13 +1002,65 @@ impl InvokeUiSession for FlutterHandler {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next_rgba(&self, _display: usize) {
|
fn next_rgba(&self, _display: usize) {
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
if let Some(rgba_data) = self.display_rgbas.write().unwrap().get_mut(&_display) {
|
if let Some(rgba_data) = self.display_rgbas.write().unwrap().get_mut(&_display) {
|
||||||
rgba_data.valid = false;
|
rgba_data.valid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FlutterHandler {
|
||||||
|
#[inline]
|
||||||
|
fn on_rgba_soft_render(&self, display: usize, rgba: &mut scrap::ImageRgb) {
|
||||||
|
// Give a chance for plugins or etc to hook a rgba data.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
for (key, hook) in self.hooks.read().unwrap().iter() {
|
||||||
|
match hook {
|
||||||
|
SessionHook::OnSessionRgba(cb) => {
|
||||||
|
cb(key.to_owned(), rgba);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the current rgba is not fetched by flutter, i.e., is valid.
|
||||||
|
// We give up sending a new event to flutter.
|
||||||
|
let mut rgba_write_lock = self.display_rgbas.write().unwrap();
|
||||||
|
if let Some(rgba_data) = rgba_write_lock.get_mut(&display) {
|
||||||
|
if rgba_data.valid {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
rgba_data.valid = true;
|
||||||
|
}
|
||||||
|
// Return the rgba buffer to the video handler for reusing allocated rgba buffer.
|
||||||
|
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut rgba_data.data);
|
||||||
|
} else {
|
||||||
|
let mut rgba_data = RgbaData::default();
|
||||||
|
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut rgba_data.data);
|
||||||
|
rgba_data.valid = true;
|
||||||
|
rgba_write_lock.insert(display, rgba_data);
|
||||||
|
}
|
||||||
|
drop(rgba_write_lock);
|
||||||
|
|
||||||
|
// Non-texture-render UI does not support multiple displays in the one UI session.
|
||||||
|
// It's Ok to notify each session for now.
|
||||||
|
for h in self.session_handlers.read().unwrap().values() {
|
||||||
|
if let Some(stream) = &h.event_stream {
|
||||||
|
stream.add(EventToUI::Rgba(display));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn on_rgba_flutter_texture_render(&self, display: usize, rgba: &mut scrap::ImageRgb) {
|
||||||
|
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
|
if session.renderer.on_rgba(display, rgba) {
|
||||||
|
if let Some(stream) = &session.event_stream {
|
||||||
|
stream.add(EventToUI::Rgba(display));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This function is only used for the default connection session.
|
// This function is only used for the default connection session.
|
||||||
pub fn session_add_existed(peer_id: String, session_id: SessionID) -> ResultType<()> {
|
pub fn session_add_existed(peer_id: String, session_id: SessionID) -> ResultType<()> {
|
||||||
sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id);
|
sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id);
|
||||||
@@ -1116,17 +1131,12 @@ pub fn session_add(
|
|||||||
Some(switch_uuid.to_string())
|
Some(switch_uuid.to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "vram")]
|
|
||||||
let adapter_luid = get_adapter_luid();
|
|
||||||
#[cfg(not(feature = "vram"))]
|
|
||||||
let adapter_luid = None;
|
|
||||||
|
|
||||||
session.lc.write().unwrap().initialize(
|
session.lc.write().unwrap().initialize(
|
||||||
id.to_owned(),
|
id.to_owned(),
|
||||||
conn_type,
|
conn_type,
|
||||||
switch_uuid,
|
switch_uuid,
|
||||||
force_relay,
|
force_relay,
|
||||||
adapter_luid,
|
get_adapter_luid(),
|
||||||
shared_password,
|
shared_password,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1172,14 +1182,11 @@ pub fn session_start_(
|
|||||||
if let Some(session) = sessions::get_session_by_session_id(session_id) {
|
if let Some(session) = sessions::get_session_by_session_id(session_id) {
|
||||||
let is_first_ui_session = session.session_handlers.read().unwrap().len() == 1;
|
let is_first_ui_session = session.session_handlers.read().unwrap().len() == 1;
|
||||||
if !is_connected && is_first_ui_session {
|
if !is_connected && is_first_ui_session {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Session {} start, render by flutter texture rgba plugin",
|
"Session {} start, use texture render: {}",
|
||||||
id
|
id,
|
||||||
|
session.use_texture_render.load(Ordering::Relaxed)
|
||||||
);
|
);
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
log::info!("Session {} start, render by flutter paint widget", id);
|
|
||||||
|
|
||||||
let session = (*session).clone();
|
let session = (*session).clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let round = session.connection_round_state.lock().unwrap().new_round();
|
let round = session.connection_round_state.lock().unwrap().new_round();
|
||||||
@@ -1239,11 +1246,11 @@ pub mod connection_manager {
|
|||||||
fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
|
fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
|
||||||
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
||||||
// send to Android service, active notification no matter UI is shown or not.
|
// send to Android service, active notification no matter UI is shown or not.
|
||||||
#[cfg(any(target_os = "android"))]
|
#[cfg(target_os = "android")]
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
call_main_service_set_by_name("add_connection", Some(&client_json), None)
|
call_main_service_set_by_name("add_connection", Some(&client_json), None)
|
||||||
{
|
{
|
||||||
log::debug!("call_service_set_by_name fail,{}", e);
|
log::debug!("call_main_service_set_by_name fail,{}", e);
|
||||||
}
|
}
|
||||||
// send to UI, refresh widget
|
// send to UI, refresh widget
|
||||||
self.push_event("add_connection", &[("client", &client_json)]);
|
self.push_event("add_connection", &[("client", &client_json)]);
|
||||||
@@ -1277,6 +1284,13 @@ pub mod connection_manager {
|
|||||||
|
|
||||||
fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
|
fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
|
||||||
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
||||||
|
// send to Android service, active notification no matter UI is shown or not.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
if let Err(e) =
|
||||||
|
call_main_service_set_by_name("update_voice_call_state", Some(&client_json), None)
|
||||||
|
{
|
||||||
|
log::debug!("call_main_service_set_by_name fail,{}", e);
|
||||||
|
}
|
||||||
self.push_event("update_voice_call_state", &[("client", &client_json)]);
|
self.push_event("update_voice_call_state", &[("client", &client_json)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1430,14 +1444,13 @@ fn char_to_session_id(c: *const char) -> ResultType<SessionID> {
|
|||||||
SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e))
|
SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_get_rgba_size(_session_id: SessionID, _display: usize) -> usize {
|
pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> usize {
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||||
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
|
|
||||||
return session
|
return session
|
||||||
.display_rgbas
|
.display_rgbas
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(&_display)
|
.get(&display)
|
||||||
.map_or(0, |rgba| rgba.data.len());
|
.map_or(0, |rgba| rgba.data.len());
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
@@ -1461,34 +1474,32 @@ pub fn session_next_rgba(session_id: SessionID, display: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, _height: usize) {
|
pub fn session_set_size(session_id: SessionID, display: usize, width: usize, height: usize) {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
for s in sessions::get_sessions() {
|
for s in sessions::get_sessions() {
|
||||||
if let Some(h) = s
|
if let Some(h) = s
|
||||||
.ui_handler
|
.ui_handler
|
||||||
.session_handlers
|
.session_handlers
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_mut(&_session_id)
|
.get_mut(&session_id)
|
||||||
{
|
{
|
||||||
h.renderer.set_size(_display, _width, _height);
|
h.renderer.set_size(display, width, height);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn session_register_pixelbuffer_texture(_session_id: SessionID, _display: usize, _ptr: usize) {
|
pub fn session_register_pixelbuffer_texture(session_id: SessionID, display: usize, ptr: usize) {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
for s in sessions::get_sessions() {
|
for s in sessions::get_sessions() {
|
||||||
if let Some(h) = s
|
if let Some(h) = s
|
||||||
.ui_handler
|
.ui_handler
|
||||||
.session_handlers
|
.session_handlers
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(&_session_id)
|
.get(&session_id)
|
||||||
{
|
{
|
||||||
h.renderer.register_pixelbuffer_texture(_display, _ptr);
|
h.renderer.register_pixelbuffer_texture(display, ptr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1511,8 +1522,17 @@ pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _ou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(feature = "vram"))]
|
||||||
|
pub fn get_adapter_luid() -> Option<i64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn get_adapter_luid() -> Option<i64> {
|
pub fn get_adapter_luid() -> Option<i64> {
|
||||||
|
if !crate::ui_interface::use_texture_render() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let get_adapter_luid_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
let get_adapter_luid_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||||
Ok(lib) => {
|
Ok(lib) => {
|
||||||
let find_sym_res = unsafe {
|
let find_sym_res = unsafe {
|
||||||
@@ -1706,7 +1726,6 @@ pub fn try_sync_peer_option(
|
|||||||
|
|
||||||
// sessions mod is used to avoid the big lock of sessions' map.
|
// sessions mod is used to avoid the big lock of sessions' map.
|
||||||
pub mod sessions {
|
pub mod sessions {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1774,14 +1793,6 @@ pub mod sessions {
|
|||||||
for (peer_key, s) in SESSIONS.write().unwrap().iter_mut() {
|
for (peer_key, s) in SESSIONS.write().unwrap().iter_mut() {
|
||||||
let mut write_lock = s.ui_handler.session_handlers.write().unwrap();
|
let mut write_lock = s.ui_handler.session_handlers.write().unwrap();
|
||||||
let remove_ret = write_lock.remove(id);
|
let remove_ret = write_lock.remove(id);
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
if remove_ret.is_some() {
|
|
||||||
if write_lock.is_empty() {
|
|
||||||
remove_peer_key = Some(peer_key.clone());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
match remove_ret {
|
match remove_ret {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
if write_lock.is_empty() {
|
if write_lock.is_empty() {
|
||||||
@@ -1797,7 +1808,6 @@ pub mod sessions {
|
|||||||
SESSIONS.write().unwrap().remove(&remove_peer_key?)
|
SESSIONS.write().unwrap().remove(&remove_peer_key?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
fn check_remove_unused_displays(
|
fn check_remove_unused_displays(
|
||||||
current: Option<usize>,
|
current: Option<usize>,
|
||||||
session_id: &SessionID,
|
session_id: &SessionID,
|
||||||
@@ -1845,7 +1855,6 @@ pub mod sessions {
|
|||||||
s.capture_displays(vec![], vec![], value);
|
s.capture_displays(vec![], vec![], value);
|
||||||
} else {
|
} else {
|
||||||
// Check if other displays are needed.
|
// Check if other displays are needed.
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
if value.len() == 1 {
|
if value.len() == 1 {
|
||||||
check_remove_unused_displays(
|
check_remove_unused_displays(
|
||||||
Some(value[0] as _),
|
Some(value[0] as _),
|
||||||
@@ -1885,16 +1894,14 @@ pub mod sessions {
|
|||||||
session_id: SessionID,
|
session_id: SessionID,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(s) = SESSIONS.read().unwrap().get(&(peer_id, conn_type)) {
|
if let Some(s) = SESSIONS.read().unwrap().get(&(peer_id, conn_type)) {
|
||||||
#[cfg(not(feature = "flutter_texture_render"))]
|
|
||||||
let h = SessionHandler::default();
|
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
|
||||||
let mut h = SessionHandler::default();
|
let mut h = SessionHandler::default();
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
{
|
let is_support_multi_ui_session = crate::common::is_support_multi_ui_session(
|
||||||
h.renderer.is_support_multi_ui_session = crate::common::is_support_multi_ui_session(
|
|
||||||
&s.ui_handler.peer_info.read().unwrap().version,
|
&s.ui_handler.peer_info.read().unwrap().version,
|
||||||
);
|
);
|
||||||
}
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
let is_support_multi_ui_session = false;
|
||||||
|
h.renderer.is_support_multi_ui_session = is_support_multi_ui_session;
|
||||||
let _ = s
|
let _ = s
|
||||||
.ui_handler
|
.ui_handler
|
||||||
.session_handlers
|
.session_handlers
|
||||||
|
|||||||
@@ -278,15 +278,6 @@ pub fn session_set_flutter_option(session_id: SessionID, k: String, v: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is only used for the default connection session.
|
|
||||||
pub fn session_get_flutter_option_by_peer_id(id: String, k: String) -> Option<String> {
|
|
||||||
if let Some(session) = sessions::get_session_by_peer_id(id, ConnType::DEFAULT_CONN) {
|
|
||||||
Some(session.get_flutter_option(k))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_next_texture_key() -> SyncReturn<i32> {
|
pub fn get_next_texture_key() -> SyncReturn<i32> {
|
||||||
let k = TEXTURE_RENDER_KEY.fetch_add(1, Ordering::SeqCst) + 1;
|
let k = TEXTURE_RENDER_KEY.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
SyncReturn(k)
|
SyncReturn(k)
|
||||||
@@ -720,9 +711,8 @@ pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, _height: usize) {
|
pub fn session_set_size(session_id: SessionID, display: usize, width: usize, height: usize) {
|
||||||
#[cfg(feature = "flutter_texture_render")]
|
super::flutter::session_set_size(session_id, display, width, height)
|
||||||
super::flutter::session_set_size(_session_id, _display, _width, _height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_send_selected_session_id(session_id: SessionID, sid: String) {
|
pub fn session_send_selected_session_id(session_id: SessionID, sid: String) {
|
||||||
@@ -775,7 +765,7 @@ pub fn main_get_error() -> String {
|
|||||||
|
|
||||||
pub fn main_show_option(_key: String) -> SyncReturn<bool> {
|
pub fn main_show_option(_key: String) -> SyncReturn<bool> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if _key.eq(config::CONFIG_OPTION_ALLOW_LINUX_HEADLESS) {
|
if _key.eq(config::keys::OPTION_ALLOW_LINUX_HEADLESS) {
|
||||||
return SyncReturn(true);
|
return SyncReturn(true);
|
||||||
}
|
}
|
||||||
SyncReturn(false)
|
SyncReturn(false)
|
||||||
@@ -783,13 +773,13 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
|
|||||||
|
|
||||||
pub fn main_set_option(key: String, value: String) {
|
pub fn main_set_option(key: String, value: String) {
|
||||||
if key.eq("custom-rendezvous-server") {
|
if key.eq("custom-rendezvous-server") {
|
||||||
set_option(key, value);
|
set_option(key, value.clone());
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||||
crate::common::test_rendezvous_server();
|
crate::common::test_rendezvous_server();
|
||||||
} else {
|
} else {
|
||||||
set_option(key, value);
|
set_option(key, value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,12 +891,25 @@ pub fn main_get_local_option(key: String) -> SyncReturn<String> {
|
|||||||
SyncReturn(get_local_option(key))
|
SyncReturn(get_local_option(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_get_use_texture_render() -> SyncReturn<bool> {
|
||||||
|
SyncReturn(use_texture_render())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_get_env(key: String) -> SyncReturn<String> {
|
pub fn main_get_env(key: String) -> SyncReturn<String> {
|
||||||
SyncReturn(std::env::var(key).unwrap_or_default())
|
SyncReturn(std::env::var(key).unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_set_local_option(key: String, value: String) {
|
pub fn main_set_local_option(key: String, value: String) {
|
||||||
set_local_option(key, value)
|
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
||||||
|
set_local_option(key, value.clone());
|
||||||
|
if is_texture_render_key {
|
||||||
|
let session_event = [("v", &value)];
|
||||||
|
for session in sessions::get_sessions() {
|
||||||
|
session.push_event("use_texture_render", &session_event, &[]);
|
||||||
|
session.use_texture_render_changed();
|
||||||
|
session.ui_handler.update_use_texture_render();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do use use `main_get_local_option` and `main_set_local_option`.
|
// We do use use `main_get_local_option` and `main_set_local_option`.
|
||||||
@@ -939,7 +942,6 @@ pub fn main_handle_wayland_screencast_restore_token(_key: String, _value: String
|
|||||||
} else {
|
} else {
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_get_input_source() -> SyncReturn<String> {
|
pub fn main_get_input_source() -> SyncReturn<String> {
|
||||||
@@ -1194,6 +1196,23 @@ pub fn main_handle_relay_id(id: String) -> String {
|
|||||||
handle_relay_id(&id).to_owned()
|
handle_relay_id(&id).to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_is_option_fixed(key: String) -> SyncReturn<bool> {
|
||||||
|
SyncReturn(
|
||||||
|
config::OVERWRITE_DISPLAY_SETTINGS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.contains_key(&key)
|
||||||
|
|| config::OVERWRITE_LOCAL_SETTINGS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.contains_key(&key)
|
||||||
|
|| config::OVERWRITE_SETTINGS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.contains_key(&key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_get_main_display() -> SyncReturn<String> {
|
pub fn main_get_main_display() -> SyncReturn<String> {
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
let display_info = "".to_owned();
|
let display_info = "".to_owned();
|
||||||
@@ -1817,10 +1836,6 @@ pub fn main_hide_docker() -> SyncReturn<bool> {
|
|||||||
SyncReturn(true)
|
SyncReturn(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_has_pixelbuffer_texture_render() -> SyncReturn<bool> {
|
|
||||||
SyncReturn(cfg!(feature = "flutter_texture_render"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main_has_file_clipboard() -> SyncReturn<bool> {
|
pub fn main_has_file_clipboard() -> SyncReturn<bool> {
|
||||||
let ret = cfg!(any(
|
let ret = cfg!(any(
|
||||||
target_os = "windows",
|
target_os = "windows",
|
||||||
@@ -1890,6 +1905,10 @@ pub fn is_disable_account() -> SyncReturn<bool> {
|
|||||||
SyncReturn(config::is_disable_account())
|
SyncReturn(config::is_disable_account())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_disable_group_panel() -> SyncReturn<bool> {
|
||||||
|
SyncReturn(LocalConfig::get_option("disable-group-panel") == "Y")
|
||||||
|
}
|
||||||
|
|
||||||
// windows only
|
// windows only
|
||||||
pub fn is_disable_installation() -> SyncReturn<bool> {
|
pub fn is_disable_installation() -> SyncReturn<bool> {
|
||||||
SyncReturn(config::is_disable_installation())
|
SyncReturn(config::is_disable_installation())
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ async fn start_hbbs_sync_async() {
|
|||||||
v["version"] = json!(crate::VERSION);
|
v["version"] = json!(crate::VERSION);
|
||||||
v["id"] = json!(id);
|
v["id"] = json!(id);
|
||||||
v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
|
v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
|
||||||
|
let ab_name = Config::get_option("preset-address-book-name");
|
||||||
|
if !ab_name.is_empty() {
|
||||||
|
v["preset-address-book-name"] = json!(ab_name);
|
||||||
|
}
|
||||||
|
let ab_tag = Config::get_option("preset-address-book-tag");
|
||||||
|
if !ab_tag.is_empty() {
|
||||||
|
v["preset-address-book-tag"] = json!(ab_tag);
|
||||||
|
}
|
||||||
match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await {
|
match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await {
|
||||||
Ok(x) => {
|
Ok(x) => {
|
||||||
if x == "SYSINFO_UPDATED" {
|
if x == "SYSINFO_UPDATED" {
|
||||||
|
|||||||
@@ -970,6 +970,12 @@ pub async fn test_rendezvous_server() -> ResultType<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
pub async fn test_ipc_connection() -> ResultType<()> {
|
||||||
|
connect(1000, "").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
pub async fn send_url_scheme(url: String) -> ResultType<()> {
|
pub async fn send_url_scheme(url: String) -> ResultType<()> {
|
||||||
connect(1_000, "_url")
|
connect(1_000, "_url")
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", "清除 Wayland 的屏幕选择"),
|
("Clear Wayland screen selection", "清除 Wayland 的屏幕选择"),
|
||||||
("clear_Wayland_screen_selection_tip", "清除 Wayland 的屏幕选择后,您可以重新选择分享的屏幕。"),
|
("clear_Wayland_screen_selection_tip", "清除 Wayland 的屏幕选择后,您可以重新选择分享的屏幕。"),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", "是否确认清除 Wayland 的分享屏幕选择?"),
|
("confirm_clear_Wayland_screen_selection_tip", "是否确认清除 Wayland 的分享屏幕选择?"),
|
||||||
|
("android_new_voice_call_tip", "收到新的语音呼叫请求。如果您接受,音频将切换为语音通信。"),
|
||||||
|
("texture_render_tip", "使用纹理渲染,使图片更加流畅。 如果您遭遇渲染问题,可尝试关闭此选项。"),
|
||||||
|
("Use texture rendering", "使用纹理渲染"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", "Vymazat výběr obrazovky Wayland"),
|
("Clear Wayland screen selection", "Vymazat výběr obrazovky Wayland"),
|
||||||
("clear_Wayland_screen_selection_tip", "Po vymazání výběru obrazovky můžete znovu vybrat obrazovku, kterou chcete sdílet."),
|
("clear_Wayland_screen_selection_tip", "Po vymazání výběru obrazovky můžete znovu vybrat obrazovku, kterou chcete sdílet."),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", "Opravdu chcete vymazat výběr obrazovky Wayland?"),
|
("confirm_clear_Wayland_screen_selection_tip", "Opravdu chcete vymazat výběr obrazovky Wayland?"),
|
||||||
|
("android_new_voice_call_tip", "Byl přijat nový požadavek na hlasové volání. Pokud hovor přijmete, přepne se zvuk na hlasovou komunikaci."),
|
||||||
|
("texture_render_tip", "Použít vykreslování textur, aby byly obrázky hladší."),
|
||||||
|
("Use texture rendering", "Použít vykreslování textur"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Logging in...", "Anmelden …"),
|
("Logging in...", "Anmelden …"),
|
||||||
("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"),
|
("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"),
|
||||||
("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"),
|
("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"),
|
||||||
("Enable direct IP access", "Direkten IP-Zugriff aktivieren"),
|
("Enable direct IP access", "Direkten IP-Zugang aktivieren"),
|
||||||
("Rename", "Umbenennen"),
|
("Rename", "Umbenennen"),
|
||||||
("Space", "Speicherplatz"),
|
("Space", "Speicherplatz"),
|
||||||
("Create desktop shortcut", "Desktop-Verknüpfung erstellen"),
|
("Create desktop shortcut", "Desktop-Verknüpfung erstellen"),
|
||||||
@@ -271,7 +271,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Chat", "Chat"),
|
("Chat", "Chat"),
|
||||||
("Total", "Gesamt"),
|
("Total", "Gesamt"),
|
||||||
("items", "Einträge"),
|
("items", "Einträge"),
|
||||||
("Selected", "Ausgewählt"),
|
("Selected", "ausgewählt"),
|
||||||
("Screen Capture", "Bildschirmaufnahme"),
|
("Screen Capture", "Bildschirmaufnahme"),
|
||||||
("Input Control", "Eingabesteuerung"),
|
("Input Control", "Eingabesteuerung"),
|
||||||
("Audio Capture", "Audioaufnahme"),
|
("Audio Capture", "Audioaufnahme"),
|
||||||
@@ -508,7 +508,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Open", "Öffnen"),
|
("Open", "Öffnen"),
|
||||||
("logout_tip", "Sind Sie sicher, dass Sie sich abmelden wollen?"),
|
("logout_tip", "Sind Sie sicher, dass Sie sich abmelden wollen?"),
|
||||||
("Service", "Vermittlungsdienst"),
|
("Service", "Vermittlungsdienst"),
|
||||||
("Start", "Start"),
|
("Start", "Starten"),
|
||||||
("Stop", "Stopp"),
|
("Stop", "Stopp"),
|
||||||
("exceed_max_devices", "Sie haben die maximale Anzahl der verwalteten Geräte erreicht."),
|
("exceed_max_devices", "Sie haben die maximale Anzahl der verwalteten Geräte erreicht."),
|
||||||
("Sync with recent sessions", "Synchronisierung mit den letzten Sitzungen"),
|
("Sync with recent sessions", "Synchronisierung mit den letzten Sitzungen"),
|
||||||
@@ -532,7 +532,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("HSV Color", "HSV-Farbe"),
|
("HSV Color", "HSV-Farbe"),
|
||||||
("Installation Successful!", "Installation erfolgreich!"),
|
("Installation Successful!", "Installation erfolgreich!"),
|
||||||
("Installation failed!", "Installation fehlgeschlagen!"),
|
("Installation failed!", "Installation fehlgeschlagen!"),
|
||||||
("Reverse mouse wheel", "Mausrad rückwärts drehen"),
|
("Reverse mouse wheel", "Mausrad rückwärtsdrehen"),
|
||||||
("{} sessions", "{} Sitzungen"),
|
("{} sessions", "{} Sitzungen"),
|
||||||
("scam_title", "Sie werden möglicherweise BETROGEN!"),
|
("scam_title", "Sie werden möglicherweise BETROGEN!"),
|
||||||
("scam_text1", "Wenn Sie mit jemandem telefonieren, den Sie NICHT KENNEN, dem Sie NICHT VERTRAUEN und der Sie gebeten hat, RustDesk zu benutzen und den Dienst zu starten, fahren Sie nicht fort und legen Sie sofort auf."),
|
("scam_text1", "Wenn Sie mit jemandem telefonieren, den Sie NICHT KENNEN, dem Sie NICHT VERTRAUEN und der Sie gebeten hat, RustDesk zu benutzen und den Dienst zu starten, fahren Sie nicht fort und legen Sie sofort auf."),
|
||||||
@@ -541,13 +541,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("I Agree", "Ich bin einverstanden"),
|
("I Agree", "Ich bin einverstanden"),
|
||||||
("Decline", "Ablehnen"),
|
("Decline", "Ablehnen"),
|
||||||
("Timeout in minutes", "Zeitüberschreitung in Minuten"),
|
("Timeout in minutes", "Zeitüberschreitung in Minuten"),
|
||||||
("auto_disconnect_option_tip", "Automatisches Schließen eingehender Sitzungen bei Inaktivität des Benutzers"),
|
("auto_disconnect_option_tip", "Eingehende Sitzungen bei Benutzer-Inaktivität automatisch schließen"),
|
||||||
("Connection failed due to inactivity", "Automatische Trennung der Verbindung aufgrund von Inaktivität"),
|
("Connection failed due to inactivity", "Automatische Trennung der Verbindung aufgrund von Inaktivität"),
|
||||||
("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"),
|
("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"),
|
||||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Bitte aktualisieren Sie RustDesk Server Pro auf die Version {} oder neuer!"),
|
("upgrade_rustdesk_server_pro_to_{}_tip", "Bitte aktualisieren Sie RustDesk Server Pro auf die Version {} oder neuer!"),
|
||||||
("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"),
|
("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"),
|
||||||
("Filter by intersection", "Nach Schnittmenge filtern"),
|
("Filter by intersection", "Nach Schnittmenge filtern"),
|
||||||
("Remove wallpaper during incoming sessions", "Hintergrundbild während eingehender Sitzungen entfernen"),
|
("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"),
|
||||||
("Test", "Test"),
|
("Test", "Test"),
|
||||||
("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."),
|
("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."),
|
||||||
("No displays", "Keine Bildschirme"),
|
("No displays", "Keine Bildschirme"),
|
||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", "Wayland-Bildschirmauswahl löschen"),
|
("Clear Wayland screen selection", "Wayland-Bildschirmauswahl löschen"),
|
||||||
("clear_Wayland_screen_selection_tip", "Nachdem Sie die Bildschirmauswahl gelöscht haben, können Sie den freizugebenden Bildschirm erneut auswählen."),
|
("clear_Wayland_screen_selection_tip", "Nachdem Sie die Bildschirmauswahl gelöscht haben, können Sie den freizugebenden Bildschirm erneut auswählen."),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", "Sind Sie sicher, dass Sie die Auswahl des Wayland-Bildschirms löschen möchten?"),
|
("confirm_clear_Wayland_screen_selection_tip", "Sind Sie sicher, dass Sie die Auswahl des Wayland-Bildschirms löschen möchten?"),
|
||||||
|
("android_new_voice_call_tip", "Eine neue Sprachanrufanfrage wurde empfangen. Wenn Sie die Anfrage annehmen, wird der Ton auf Sprachkommunikation umgeschaltet."),
|
||||||
|
("texture_render_tip", "Verwenden Sie Textur-Rendering, um die Bilder glatter zu machen."),
|
||||||
|
("Use texture rendering", "Textur-Rendering verwenden"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,5 +227,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("no_audio_input_device_tip", "No audio input device found."),
|
("no_audio_input_device_tip", "No audio input device found."),
|
||||||
("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."),
|
("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", "Are you sure to clear the Wayland screen selection?"),
|
("confirm_clear_Wayland_screen_selection_tip", "Are you sure to clear the Wayland screen selection?"),
|
||||||
|
("android_new_voice_call_tip", "A new voice call request was received. If you accept, the audio will switch to voice communication."),
|
||||||
|
("texture_render_tip", "Use texture rendering to make the pictures smoother. You could try disabling this option if you encounter rendering issues."),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,12 +605,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."),
|
("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."),
|
||||||
("Follow remote cursor", "Seguir cursor remoto"),
|
("Follow remote cursor", "Seguir cursor remoto"),
|
||||||
("Follow remote window focus", "Seguir ventana remota activa"),
|
("Follow remote window focus", "Seguir ventana remota activa"),
|
||||||
("default_proxy_tip", ""),
|
("default_proxy_tip", "El protocolo y puerto predeterminados es Socks5 y 1080"),
|
||||||
("no_audio_input_device_tip", ""),
|
("no_audio_input_device_tip", "No se ha encontrado ningún dispositivo de entrada de autio."),
|
||||||
("Incoming", ""),
|
("Incoming", "Entrante"),
|
||||||
("Outgoing", ""),
|
("Outgoing", "Saliente"),
|
||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", "Borrar la selección de pantalla Wayland"),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", "Tras borrar la selección de pantalla, puedes volver a seleccionarla para compartir."),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", "¿Seguro que deseas borrar la selección de pantalla Wayland?"),
|
||||||
|
("android_new_voice_call_tip", "Se ha recibido una nueva solicitud de llamada de voz. Si aceptas el audio cambiará a comunicación de voz."),
|
||||||
|
("texture_render_tip", "Usar renderizado de texturas para hacer las imágenes más suaves."),
|
||||||
|
("Use texture rendering", "Usar renderizado de texturas"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,16 +601,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("share_warning_tip", "فیلدهای بالا به اشتراک گذاشته شده و برای دیگران قابل مشاهده است"),
|
("share_warning_tip", "فیلدهای بالا به اشتراک گذاشته شده و برای دیگران قابل مشاهده است"),
|
||||||
("Everyone", "هر کس"),
|
("Everyone", "هر کس"),
|
||||||
("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"),
|
("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"),
|
||||||
("allow-only-conn-window-open-tip", ""),
|
("allow-only-conn-window-open-tip", "باز است اتصال برقرار شود RustDesk زمانی که"),
|
||||||
("no_need_privacy_mode_no_physical_displays_tip", ""),
|
("no_need_privacy_mode_no_physical_displays_tip", "بدون نمایشگر فیزیکی نیازی به استفاده از حالت خصوصی نیست"),
|
||||||
("Follow remote cursor", ""),
|
("Follow remote cursor", "مکان نما ریموت را دنبال کنید"),
|
||||||
("Follow remote window focus", ""),
|
("Follow remote window focus", ""),
|
||||||
("default_proxy_tip", ""),
|
("default_proxy_tip", "و پورت 1080 می باشد Sock5 پرونکل پیش فرض"),
|
||||||
("no_audio_input_device_tip", ""),
|
("no_audio_input_device_tip", "دستگاه ورودی صوتی پیدا نشد"),
|
||||||
("Incoming", ""),
|
("Incoming", "ورودی"),
|
||||||
("Outgoing", ""),
|
("Outgoing", "خروجی"),
|
||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", "پس از پاک کردن صفحه انتخابی، می توانید صفحه را برای اشتراک گذاری مجدد انتخاب کنید"),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", "را پاک می کنید؟ Wayland آیا مطمئن هستید که انتخاب صفحه"),
|
||||||
|
("android_new_voice_call_tip", "یک درخواست تماس صوتی جدید دریافت شد. اگر بپذیرید، صدا به ارتباط صوتی تغییر خواهد کرد."),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,41 +576,44 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("capture_display_elevated_connections_tip", "La capture de plusieurs écrans n'est pas prise en charge en mode utilisateur avec privilièges. Veuillez réessayer après l'installation si vous souhaitez contrôler plusieurs écrans."),
|
("capture_display_elevated_connections_tip", "La capture de plusieurs écrans n'est pas prise en charge en mode utilisateur avec privilièges. Veuillez réessayer après l'installation si vous souhaitez contrôler plusieurs écrans."),
|
||||||
("Swap control-command key", "Échanger la touche de controle-commande"),
|
("Swap control-command key", "Échanger la touche de controle-commande"),
|
||||||
("swap-left-right-mouse", "Intervertir le bouton gauche et droit de la souris"),
|
("swap-left-right-mouse", "Intervertir le bouton gauche et droit de la souris"),
|
||||||
("2FA code", ""),
|
("2FA code", "code 2FA"),
|
||||||
("More", ""),
|
("More", "Plus"),
|
||||||
("enable-2fa-title", ""),
|
("enable-2fa-title", "Activer l'authentification à double facteur"),
|
||||||
("enable-2fa-desc", ""),
|
("enable-2fa-desc", "Veuillez configurer votre authentificateur maintenant. Vous pouvez utiliser une application d’authentification telle qu’Authy, Microsoft ou Google Authenticator sur votre téléphone ou votre ordinateur de bureau.nnScannez le code QR avec votre application et entrez le code affiché par votre application pour activer l’authentification à deux facteurs."),
|
||||||
("wrong-2fa-code", ""),
|
("wrong-2fa-code", "Impossible de vérifier le code. Vérifiez que le code et les paramètres d’heure locale sont corrects"),
|
||||||
("enter-2fa-title", ""),
|
("enter-2fa-title", "Authentification à deux facteurs"),
|
||||||
("Email verification code must be 6 characters.", ""),
|
("Email verification code must be 6 characters.", "Le code de vérification email doit comporter 6 caractères"),
|
||||||
("2FA code must be 6 digits.", ""),
|
("2FA code must be 6 digits.", "le code 2FA doit comporter 6 chiffres"),
|
||||||
("Multiple Windows sessions found", ""),
|
("Multiple Windows sessions found", "Plusieurs sessions Windows trouvées"),
|
||||||
("Please select the session you want to connect to", ""),
|
("Please select the session you want to connect to", "Merci de sélectionner la session Windows à laquelle vous voulez vous connecter"),
|
||||||
("powered_by_me", ""),
|
("powered_by_me", ""),
|
||||||
("outgoing_only_desk_tip", ""),
|
("outgoing_only_desk_tip", "Il s’agit d’une édition personnalisée.\nVous pouvez vous connecter à d’autres appareils, mais les autres appareils ne peuvent pas se connecter à votre appareil."),
|
||||||
("preset_password_warning", ""),
|
("preset_password_warning", "Cette édition personnalisée est livrée avec un mot de passe prédéfini. Toute personne connaissant ce mot de passe pourrait prendre le contrôle total de votre appareil. Si vous ne vous y attendiez pas, désinstallez immédiatement le logiciel."),
|
||||||
("Security Alert", ""),
|
("Security Alert", "Alerte de sécurité"),
|
||||||
("My address book", ""),
|
("My address book", "Mon carnet d'adresse"),
|
||||||
("Personal", ""),
|
("Personal", "Personnel"),
|
||||||
("Owner", ""),
|
("Owner", "Propriétaire"),
|
||||||
("Set shared password", ""),
|
("Set shared password", "Définir le mot de passe partagé"),
|
||||||
("Exist in", ""),
|
("Exist in", "Existe dans"),
|
||||||
("Read-only", ""),
|
("Read-only", "Lecture-seule"),
|
||||||
("Read/Write", ""),
|
("Read/Write", "Lecture/Écriture"),
|
||||||
("Full Control", ""),
|
("Full Control", "Control Total"),
|
||||||
("share_warning_tip", ""),
|
("share_warning_tip", "Les champs ci-dessus sont partagés et visibles par les autres."),
|
||||||
("Everyone", ""),
|
("Everyone", "Tout le monde"),
|
||||||
("ab_web_console_tip", ""),
|
("ab_web_console_tip", "Plus sur la console Web"),
|
||||||
("allow-only-conn-window-open-tip", ""),
|
("allow-only-conn-window-open-tip", "N’autoriser la connexion que si la fenêtre RustDesk est ouverte"),
|
||||||
("no_need_privacy_mode_no_physical_displays_tip", ""),
|
("no_need_privacy_mode_no_physical_displays_tip", "Pas d’affichage physique, pas besoin d’utiliser le mode confidentialité."),
|
||||||
("Follow remote cursor", ""),
|
("Follow remote cursor", "Suivre le curseur distant"),
|
||||||
("Follow remote window focus", ""),
|
("Follow remote window focus", ""),
|
||||||
("default_proxy_tip", ""),
|
("default_proxy_tip", "Le protocole et le port par défaut sont Socks5 et 1080"),
|
||||||
("no_audio_input_device_tip", ""),
|
("no_audio_input_device_tip", "Aucun périphérique d’entrée audio trouvé."),
|
||||||
("Incoming", ""),
|
("Incoming", "Entrant"),
|
||||||
("Outgoing", ""),
|
("Outgoing", "Sortant"),
|
||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", "Une nouvelle demande d’appel vocal a été reçue. Si vous acceptez, l’audio passera à la communication vocale."),
|
||||||
|
("texture_render_tip", "Utilisez le rendu des textures pour rendre les images plus fluides."),
|
||||||
|
("Use texture rendering", "Utiliser le rendu de texture"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,5 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", ""),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", ""),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||||
|
("android_new_voice_call_tip", ""),
|
||||||
|
("texture_render_tip", ""),
|
||||||
|
("Use texture rendering", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user